Files
einfach-produktiv-invoicing/scripts/generate-einvoice-fixtures.mts
T
Marco 29ea9fafd8
Validate e-invoices / mustang (push) Failing after 1m30s
Phase 4: Mustang EN16931/PDF-A-3 validation in CI
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.
2026-07-23 12:12:51 +00:00

110 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});