29ea9fafd8
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.
110 lines
4.4 KiB
TypeScript
110 lines
4.4 KiB
TypeScript
// 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);
|
||
});
|