Phase 4: Mustang EN16931/PDF-A-3 validation in CI
Validate e-invoices / mustang (push) Failing after 1m30s

Adds a Gitea Actions workflow that generates realistic e-invoice fixtures (multi-VAT-rate original invoice, Storno, Gutschrift) and validates them against the reference Mustang validator on every push, catching a broken EN16931 mapping before it reaches production instead of relying on manual spot-checks.
This commit is contained in:
Marco
2026-07-23 12:12:51 +00:00
parent 68d7054557
commit 29ea9fafd8
6 changed files with 674 additions and 1 deletions
+109
View File
@@ -0,0 +1,109 @@
// Generates a small set of realistic Factur-X-EN16931 PDFs into
// .mustang-fixtures/ (gitignored) — run by CI (see
// .gitea/workflows/validate-einvoice.yml) as the input to Mustang's own
// EN16931/PDF-A-3 conformance check. Kept as a separate script rather than
// re-exporting SAMPLE_INVOICE_ORDER's single-VAT-rate fixture, so this can
// exercise the shapes that fixture doesn't: multiple simultaneous VAT
// rates, a discount + shipping cost together, and both correction-invoice
// kinds (Storno = full reversal, Gutschrift = partial return).
import { writeFile, mkdir } from "node:fs/promises";
import { renderInvoiceEInvoice, renderCorrectionInvoiceEInvoice } from "../src/einvoice/index.js";
import type { InvoiceOrder } from "../src/invoicePdf.js";
import type { CorrectionInvoiceOrder } from "../src/correctionInvoicePdf.js";
import type { InvoiceSeller } from "../src/seller.js";
const OUT_DIR = new URL("../.mustang-fixtures/", import.meta.url);
// A fully-populated seller — including bankName/iban/bic and the
// registered-legal-form fields — so the generated XML exercises every
// optional branch buildEInvoiceData.ts has (PaymentMeans, register
// court/number, managing director), not just the sole-proprietorship
// minimum this shop's own production data happens to use today.
const SELLER: InvoiceSeller = {
sellerName: "Mustang Test GmbH",
sellerStreet: "Teststraße 1",
sellerZip: "10115",
sellerCity: "Berlin",
sellerCountry: "Deutschland",
sellerEmail: "rechnung@example.com",
vatId: "DE123456789",
taxRatePercent: 19,
bankName: "Test Bank",
iban: "DE89370400440532013000",
bic: "COBADEFFXXX",
registerCourt: "Amtsgericht Berlin (Charlottenburg)",
registerNumber: "HRB 123456",
managingDirector: "Max Mustermann",
};
// Two simultaneous VAT rates (19% + 7%) plus a discount and a nonzero
// shipping cost — the combination SAMPLE_INVOICE_ORDER doesn't cover, and
// exactly where a tax-breakdown/rounding bug would first show up.
const ORDER: InvoiceOrder = {
orderNumber: "#EP-MUSTANG-0001",
invoiceNumber: "RE-MUSTANG-0001",
invoiceIssuedAt: new Date().toISOString(),
customerFirstName: "Erika",
customerLastName: "Musterfrau",
deliveryMethod: "address",
street: "Kundenweg 2",
zip: "80331",
city: "München",
country: "Deutschland",
paymentMethodTitle: "Kreditkarte",
items: [
{ productName: "ToDo-Karten Set", quantity: 2, unitPrice: 12.9, taxRatePercent: 19, bundleContents: null },
{ productName: "Fachbuch Produktivität", quantity: 1, unitPrice: 24.0, taxRatePercent: 7, bundleContents: null },
],
subtotal: 49.8,
shippingCost: 4.95,
discountAmount: 5,
discountCode: "MUSTANG5",
total: 49.75,
};
const CORRECTION_ORDER: CorrectionInvoiceOrder = {
orderNumber: ORDER.orderNumber,
invoiceNumber: ORDER.invoiceNumber,
invoiceIssuedAt: ORDER.invoiceIssuedAt,
correctionInvoiceNumber: "RK-MUSTANG-0001",
correctionInvoiceIssuedAt: new Date().toISOString(),
customerFirstName: ORDER.customerFirstName,
customerLastName: ORDER.customerLastName,
deliveryMethod: ORDER.deliveryMethod,
street: ORDER.street,
zip: ORDER.zip,
city: ORDER.city,
country: ORDER.country,
items: [
{ productName: "ToDo-Karten Set", quantity: 2, unitPrice: 12.9, taxRatePercent: 19, bundleContents: null, returnQuantity: 1 },
{ productName: "Fachbuch Produktivität", quantity: 1, unitPrice: 24.0, taxRatePercent: 7, bundleContents: null, returnQuantity: 0 },
],
subtotal: ORDER.subtotal,
shippingCost: ORDER.shippingCost,
discountAmount: ORDER.discountAmount,
total: ORDER.total,
};
async function main() {
await mkdir(OUT_DIR, { recursive: true });
const invoice = await renderInvoiceEInvoice(ORDER, SELLER);
await writeFile(new URL("original-invoice.pdf", OUT_DIR), invoice);
// Storno: every item at full ordered quantity (resolveLineItems ignores
// returnQuantity for this kind) — CORRECTION_ORDER's returnQuantity
// values are only meaningful for the Gutschrift render below.
const storno = await renderCorrectionInvoiceEInvoice("storno", CORRECTION_ORDER, SELLER);
await writeFile(new URL("stornorechnung.pdf", OUT_DIR), storno);
const gutschrift = await renderCorrectionInvoiceEInvoice("gutschrift", CORRECTION_ORDER, SELLER);
await writeFile(new URL("gutschrift.pdf", OUT_DIR), gutschrift);
console.log(`Wrote 3 e-invoice fixtures to ${OUT_DIR.pathname}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});