Files
einfach-produktiv-invoicing/src/einvoice/renderEInvoice.ts
T
Marco 63e3a02212 Phase 3: actual ZUGFeRD/Factur-X e-invoice generation
renderInvoiceEInvoice()/renderCorrectionInvoiceEInvoice() produce a
Factur-X-EN16931 hybrid PDF/A-3 (existing @react-pdf/renderer PDF +
embedded EN16931 XML) via @e-invoice-eu/core, instead of a plain PDF.
The plain renderInvoicePdf()/renderCorrectionInvoicePdf() stay unchanged
and are still what Live Preview uses — this only adds a post-processing
step for the actual send/download paths.

buildEInvoiceData()/buildCorrectionEInvoiceData() map InvoiceOrder/
CorrectionInvoiceOrder + InvoiceSeller into the library's raw UBL-shaped
Invoice object, reusing computeTaxBreakdown() for the tax math — one
implementation feeding both the human-readable and machine-readable side
of the same document. Verified end-to-end: rendered a sample invoice and
correction invoice, inflated the embedded XML stream out of the resulting
PDF/A-3 by hand (the library has no attachment-reading API to check
against), confirmed correct CrossIndustryInvoice XML, EN16931 guideline
reference, per-rate tax breakdown matching the order totals exactly, and
payment means with the IBAN from Phase 2 — not just "it didn't throw."

Found only through actually running it (not documented anywhere): every
EN16931 amount field requires a sibling `*@currencyID` key via the
library's runtime ajv validation, invisible in its TypeScript types.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:28:03 +00:00

50 lines
2.9 KiB
TypeScript

import { InvoiceService } from "@e-invoice-eu/core";
import { renderInvoicePdf, type InvoiceOrder } from "../invoicePdf";
import { renderCorrectionInvoicePdf, type CorrectionInvoiceKind, type CorrectionInvoiceOrder } from "../correctionInvoicePdf";
import type { InvoiceSeller } from "../seller";
import { buildEInvoiceData, buildCorrectionEInvoiceData } from "./buildEInvoiceData";
// `console` satisfies @e-invoice-eu/core's minimal Logger interface
// (debug/info/warn/error) without needing a dedicated logger dependency —
// this only ever runs server-side (Node.js), console output lands in the
// same place `payload.logger`/plain `console.error` calls already do
// elsewhere in both consuming repos.
const invoiceService = new InvoiceService(console);
// Renders the existing @react-pdf/renderer visual PDF unchanged, then
// passes that buffer through @e-invoice-eu/core's Factur-X embed step to
// get back a PDF/A-3 file with the EN16931 XML attached (`factur-x.xml`)
// — same visual document a human opens, but AP software that understands
// Factur-X/ZUGFeRD can also extract and process it automatically. Format
// 'Factur-X-EN16931' — the "Comfort" profile, the minimum EN16931-compliant
// Factur-X level (see this package's README/the project's e-invoicing
// migration plan for why EN16931 over a lower profile like BASIC).
export async function renderInvoiceEInvoice(order: InvoiceOrder, seller: InvoiceSeller): Promise<Uint8Array> {
const pdf = await renderInvoicePdf(order, seller);
const invoiceData = buildEInvoiceData(order, seller);
const result = await invoiceService.generate(invoiceData, {
format: "Factur-X-EN16931",
lang: "de",
pdf: { buffer: pdf, filename: `Rechnung-${order.invoiceNumber}.pdf`, mimetype: "application/pdf" },
});
// Factur-X formats always return the PDF/A-3 bytes, never the bare XML
// string variant `generate()`'s return type also allows for XML-only
// formats (XRechnung/UBL/CII) — this function only ever requests a
// Factur-X format, so this case is unreachable in practice, just
// satisfying the union type.
if (typeof result === "string") throw new Error("Expected a PDF/A-3 buffer from Factur-X generation, got a string.");
return result;
}
export async function renderCorrectionInvoiceEInvoice(kind: CorrectionInvoiceKind, order: CorrectionInvoiceOrder, seller: InvoiceSeller): Promise<Uint8Array> {
const pdf = await renderCorrectionInvoicePdf(kind, order, seller);
const invoiceData = buildCorrectionEInvoiceData(kind, order, seller);
const result = await invoiceService.generate(invoiceData, {
format: "Factur-X-EN16931",
lang: "de",
pdf: { buffer: pdf, filename: `${kind === "storno" ? "Stornorechnung" : "Gutschrift"}-${order.correctionInvoiceNumber}.pdf`, mimetype: "application/pdf" },
});
if (typeof result === "string") throw new Error("Expected a PDF/A-3 buffer from Factur-X generation, got a string.");
return result;
}