Repository navigation
Issue ticket invoices as ZUGFeRD / Factur-X e-invoices (EN 16931) - #284
claude[bot] wants to merge 6 commits into
Conversation
Every Rechnung, Rechnungsberichtigung and Stornorechnung is now a hybrid e-invoice: a PDF/A-3b with the CII XML (profile EN 16931) embedded as factur-x.xml (AFRelationship Alternative, Factur-X XMP extension schema). The XML is built from the same InvoiceData as the printed page and only carries printed amounts; data that would contradict them is refused. Type codes: 380 invoice, 384 corrected invoice, 381 credit note with positive amounts for the storno; corrections and stornos reference the original via BT-25/BT-26. The rendered page is pixel-identical. pdfkit already supports PDF/A-3b and file attachments, so no new runtime dependency. Samples of every document type are validated with Mustang (veraPDF, CII XSD, EN 16931 + Factur-X Schematron) in a new CI job. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
|
LGTM on the infrastructure side - but we need accounting review for the actual feature ... |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fed782c027
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
🟡 Changes recommended
Checkout-accepted pricing can block paid-order fulfillment, and VAT prefix validation and failure notifications need correction.
3 open issues
What changed in this PR
Adds hybrid ZUGFeRD / Factur-X e-invoices to the existing ticket invoice pipeline, building on the service-date and billing-country changes in #280 and #281.
Changes:
- Embeds EN 16931 XML and Factur-X metadata in invoices, corrections, and cancellations.
- Validates invoice amounts before rendering and exposes data errors through the lifecycle endpoint.
- Adds sample documents, mapping tests, and a dedicated Mustang validation job.
| File | Description |
|---|---|
| docs/tickets.md | Documents hybrid invoices and PDF/A requirements. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/TESTING.md | Explains sample generation and validation. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/museo-font.ts | Supports font loading from source and built locations. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/invoice-service.ts | Passes structured service dates and warns on country fallback. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/invoice-generator.ts | Embeds XML and metadata in PDF/A-3b output. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/einvoice.ts | Implements XML mapping, amount checks, and Factur-X metadata. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/einvoice-samples.ts | Defines six representative invoice samples. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/cli/validate-einvoices.ts | Runs the pinned Mustang validator and reports results. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/__tests__/pdf-inspect.ts | Adds helpers for inspecting PDF attachments and metadata. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/__tests__/invoice-service.test.ts | Tests country warnings and corrects cancellation fixtures. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/__tests__/invoice-generator.test.ts | Uses negative amounts for cancellation fixtures. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/__tests__/einvoice.test.ts | Tests XML mapping, validation, and PDF embedding. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/shared/__tests__/einvoice-samples.test.ts | Renders and optionally exports samples with the embedded font. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/invoice-lifecycle/index.ts | Returns HTTP 400 for e-invoice data errors. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/src/invoice-lifecycle/__tests__/index.test.ts | Tests rejection before upload for inconsistent amounts. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/package.json | Adds sample and validation commands. |
| directus-cms/extensions/directus-extension-programmierbar-bundle/.gitignore | Excludes generated samples and validator cache. |
| .github/workflows/run_tests.yml | Adds validation with cached Mustang and uploaded artifacts. |
Review effort: Balanced
E-invoice data problems no longer block invoice issuance or the paid-order flow: the document is rendered without the embedded XML instead, and Slack is alerted with invoice/order number and reason. - Unknown buyer country: no false BT-55 "DE" any more. Private buyers without address get a plain PDF without alert (no E-Rechnung needed). - Negative Rabatt (discount code priced above the list price) and hand-edited amounts fall back the same way. - Legacy orders missing total_gross_cents/subtotal/VAT: derive the missing amounts consistently (total_cents is the net total), so corrections and cancellations work. - BT-48 only with a prefix BR-CO-09 accepts (ISO 3166-1 alpha-2, EL, XI, 1A). - Paid-order hook: any processing failure alerts Slack with order/invoice. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw
| total_gross_cents: order.total_gross_cents ?? order.total_cents ?? 0, | ||
| discount_amount_cents: discountCents, | ||
| vat_amount_cents: vatCents, | ||
| total_gross_cents: grossCents ?? netCents + vatCents, |
There was a problem hiding this comment.
Checked prod read-only: of the 72 orders that already have an invoice, 0 have null total_gross_cents, vat_amount_cents, subtotal_cents or total_cents. So no legacy invoice exists whose backfill would derive a different total. The checkout (create-checkout buildOrderPayload) always writes all five amounts, and new invoices get their snapshot when issued, not through the backfill. Per the edge-case policy I left the code unchanged; leaving this thread open for Jan.
Generated by Claude Code


Requested by Richard Hartmann · Slack thread
Before: Ticket invoices, corrections (Rechnungsberichtigung) and cancellations (Stornorechnung) were plain PDFs. From 2027 B2B invoices must be structured e-invoices (§ 14 UStG as amended by the Wachstumschancengesetz). A plain PDF no longer counts as an invoice for business buyers.
After: Every invoice document is a hybrid ZUGFeRD 2.x / Factur-X e-invoice in profile EN 16931 (COMFORT). The file is a PDF/A-3b with the CII XML embedded as
factur-x.xml(AFRelationshipAlternative, Factur-X XMP extension schema, DocumentType INVOICE, Version 1.0, ConformanceLevel EN 16931). Private buyers still get the same readable PDF; the rendered page is pixel-identical to before. Business buyers get the same file, and it is a valid E-Rechnung. No schema changes.How:
Library: none added. pdfkit (already a dependency, MIT, ~6.6M weekly downloads) supports
subset: 'PDF/A-3b'(sRGB output intent andpdfaidXMP), file attachments withAFRelationshipplus the catalog/AFentry, and custom XMP throughappendXML. That is everything Factur-X needs. The npm options all re-render or post-process with pdf-lib (MIT, but no release since 2022):node-zugferd(MIT, 0.1.x beta),@stafyniaksacha/facturx(MIT, ~2k weekly), and@e-invoice-eu/core, whose WTFPL licence is unusual. The XML itself is a small hand-written CII template inshared/einvoice.ts. Museo Sans is embedded as a subset CFF font. With the old Helvetica fallback (font file missing) the PDF would not be PDF/A, as before.Mapping: the XML is built from the same
InvoiceDatathe page is rendered from, and it carries only printed amounts (no recomputation):EM).EInvoiceDataError. The lifecycle endpoint returns that as a 400. Orders priced by the checkout can't trigger it.Type codes:
Buyer country (BT-55): taken from
normalizeBillingCountry. When an order has no mappable country (private buyers may skip the address), the XML saysDEand a warning is logged.Validation:
npm run einvoice:samplesrenders every document type, plus a private buyer, a 0 € invoice and a rounding case, through the real snapshot → render path.npm run einvoice:validateruns each PDF through Mustang 2.26.0 (Apache-2.0; embedded veraPDF 1.30.2 for PDF/A-3b, the CII XSD, and the EN 16931 + Factur-X Schematron). This runs in the neweinvoice-validationjob (Java 21, jar cached, SHA-256 pinned, samples uploaded as an artifact). All 6 samples pass, with 0 errors and 0 PDF/A failures. Locally they also pass the official CEN validation artefacts 1.3.17 (CII XSD +EN16931-CII-validation.xslt) with 0 failed asserts. The remaining Mustang warnings:ApplicableHeaderTradeDeliverywhen the service period goes into BG-14.Rounding (BR-CO-17): the checkout rounds the gross price per ticket. Printed VAT is therefore
n × round(19 % × net), notround(19 % × n × net). With whole-euro net prices the two are always equal (verified for 1–1000 € × 1–10 tickets). With cent prices they can differ by up to 5 cents per order. Examples:The CEN schematron tolerates up to 1.00, so these still validate. PDF and XML stay identical because the XML uses the printed VAT. Removing the deviation would change the gross amount customers pay, so it is left for a decision: compute VAT on the order net total in the checkout, which changes nothing for whole-euro prices.
Depends on #280 and #281.
A tax advisor should sign off one sample invoice of each document type before this goes live. The open points are 381 for the Storno, the
DEfallback for buyers without a country, and the payment-terms text on the Storno. CI uploads the samples as theeinvoice-samplesartifact.🤖 Generated with Claude Code
https://claude.ai/code/session_01FBHiR6NFRe6QR5SBjSv4nw