Phase 4: Mustang EN16931/PDF-A-3 validation in CI
Validate e-invoices / mustang (push) Failing after 1m30s
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:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user