// 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); });