Files
einfach-produktiv-invoicing/scripts/generate-einvoice-fixtures.mts
T
Marco 51ae8a9c0a
Validate e-invoices / mustang (push) Successful in 43s
Add Kleinunternehmerregelung (§19 UStG) support
InvoiceOrder/CorrectionInvoiceOrder gain an optional order-level
kleinunternehmer flag, snapshotted per order (same pattern as vatExempt)
so a later toggle of the tenant's setting never rewrites an
already-issued invoice. Takes precedence over vatExempt.

- Visual PDF: "enthält X% MwSt." becomes the §19 UStG notice.
- E-invoice XML: new UNTDID 5305 category "E" with a free-text exemption
  reason (no VATEX code — §19 UStG has none, BR-E-10 allows text alone).
- New Mustang CI fixture + buildEInvoiceData unit test for the "E" path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 12:51:13 +00:00

142 lines
5.8 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,
};
// §19 UStG Kleinunternehmer — every item snapshotted at 0% (exactly what
// api/checkout/route.ts forces when the tenant has company-settings.
// kleinunternehmer set), UNTDID 5305 category "E" with a free-text
// exemption reason instead of "S"/percent-rated lines. Own fixture rather
// than a variant of ORDER above, since a real Kleinunternehmer order never
// has a positive rate to begin with — mixing that into the 19%/7% fixture
// would test a combination (kleinunternehmer + a nonzero catalog rate)
// that can't actually happen in production.
const KLEINUNTERNEHMER_ORDER: InvoiceOrder = {
orderNumber: "#EP-MUSTANG-0002",
invoiceNumber: "RE-MUSTANG-0002",
invoiceIssuedAt: new Date().toISOString(),
customerFirstName: "Erika",
customerLastName: "Musterfrau",
deliveryMethod: "address",
street: "Kundenweg 2",
zip: "80331",
city: "München",
country: "Deutschland",
paymentMethodTitle: "Kreditkarte",
kleinunternehmer: true,
items: [{ productName: "ToDo-Karten Set", quantity: 2, unitPrice: 12.9, taxRatePercent: 0, bundleContents: null }],
subtotal: 25.8,
shippingCost: 4.95,
discountAmount: 0,
discountCode: null,
total: 30.75,
};
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);
const kleinunternehmerInvoice = await renderInvoiceEInvoice(KLEINUNTERNEHMER_ORDER, SELLER);
await writeFile(new URL("kleinunternehmer-invoice.pdf", OUT_DIR), kleinunternehmerInvoice);
console.log(`Wrote 4 e-invoice fixtures to ${OUT_DIR.pathname}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});