many updates :-)

This commit is contained in:
Gerhard Scheikl
2026-05-08 10:40:19 +02:00
parent 5b2aa5d62b
commit 770c6fd16a
16 changed files with 876 additions and 151 deletions
+80 -2
View File
@@ -17,6 +17,8 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { execFileSync } from "node:child_process";
import { tmpdir } from "node:os";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -51,6 +53,22 @@ function assertNear(name: string, actual: number, expected: number, eps = 0.01)
assert(name, Math.abs(actual - expected) <= eps, `expected ~${expected}, got ${actual}`);
}
/**
* Extracts text from a PDF buffer using the system `pdftotext` (poppler).
* The smoke script runs in a dev environment where poppler is available;
* if it ever isn't, the assertion failures will surface a clear ENOENT.
*/
async function pdfToText(pdf: Buffer): Promise<string> {
const inPath = resolve(tmpdir(), `linumiq-smoke-${Date.now()}-${Math.random().toString(36).slice(2)}.pdf`);
writeFileSync(inPath, pdf);
try {
const out = execFileSync("pdftotext", ["-layout", "-enc", "UTF-8", inPath, "-"], { encoding: "utf8" });
return out;
} finally {
try { require("node:fs").unlinkSync(inPath); } catch { /* best-effort cleanup */ }
}
}
// ------------------------------------------------------------------
// Synthetic settings (mirrors the reference invoice)
// ------------------------------------------------------------------
@@ -81,7 +99,8 @@ const settings = {
invoiceSeed: 1000,
defaultLanguage: "de",
paymentTermDays: 14,
footerNote: "",
footerNote: "Vielen Dank für Ihren Auftrag.",
footerNoteEn: "Thank you for your business.",
kleinunternehmer: false,
logoUrl: "",
smtpHost: "",
@@ -139,6 +158,7 @@ function buildAtB2BOrder(): RawOrderForInvoice {
sku: "BT-TRK-001",
quantity: qty,
originalUnitPriceSet: { shopMoney: { amount: unitNet.toFixed(2), currencyCode: "EUR" } },
imageUrl: "file://product-image", // placeholder; the smoke script inlines a real data: URL on the composed line below.
taxLines: [
{
title: "USt 20%",
@@ -285,7 +305,13 @@ async function main() {
// Attach logo (loaded from disk so the smoke output mirrors a real merchant setup).
const logoPath = resolve(__dirname, "..", "data", "linumiq-logo.png");
const logoBytes = readFileSync(logoPath);
vm.issuer.logoDataUrl = `data:image/png;base64,${logoBytes.toString("base64")}`;
const logoDataUrl = `data:image/png;base64,${logoBytes.toString("base64")}`;
vm.issuer.logoDataUrl = logoDataUrl;
// Stand in for a real product image (the orchestrator fetches it from
// Shopify CDN; here we re-use the logo bytes so the table cell exercises
// the icon rendering path).
assertEq("composer propagates imageUrl", vm.lines[0].imageUrl, "file://product-image");
vm.lines[0].imageDataUrl = logoDataUrl;
// Attach GiroCode (rendered manually here since the orchestrator does it).
vm.giroCodePngDataUrl = await buildGiroCodeDataUrl({
beneficiaryName: settings.companyName,
@@ -337,6 +363,58 @@ async function main() {
assert("storno PDF > 5 KB", stornoBuf.length > 5_000, `actual ${stornoBuf.length}`);
console.log(` → wrote ${stornoOut} (${stornoBuf.length} bytes)`);
// ----------------------------------------------------------------
// Footer note translation
// ----------------------------------------------------------------
console.log("• Footer note (per-language)");
const deVm = composeInvoice({ order, settings: settings as never, invoiceNumber: "RE-1010" });
assertEq("DE footerNote propagated", deVm.issuer.footerNote, "Vielen Dank für Ihren Auftrag.");
assertEq("EN footerNote propagated", deVm.issuer.footerNoteEn, "Thank you for your business.");
const enVm = composeInvoice({ order, settings: settings as never, invoiceNumber: "RE-1011", forceLanguage: "en" });
assertEq("forceLanguage=en", enVm.language, "en");
// Render both and verify the rendered text differs by language.
const dePdf = await renderInvoicePdf(deVm);
const enPdf = await renderInvoicePdf(enVm);
const deText = await pdfToText(dePdf);
const enText = await pdfToText(enPdf);
assert("DE PDF contains German footer", deText.includes("Vielen Dank für Ihren Auftrag."), `text snippet: ${deText.slice(0, 200)}`);
assert("DE PDF does NOT contain English footer", !deText.includes("Thank you for your business."));
assert("EN PDF contains English footer", enText.includes("Thank you for your business."), `text snippet: ${enText.slice(0, 200)}`);
assert("EN PDF does NOT contain German footer", !enText.includes("Vielen Dank für Ihren Auftrag."));
// Closing line: generic thank-you, no owner-name signature directly under it.
// (The legal "Inhaber: <name>" block in the page footer is unrelated and stays.)
assert("DE PDF closing reads 'Danke für deinen Einkauf'", deText.includes("Danke für deinen Einkauf"));
assert("DE PDF no longer contains 'Mit freundlichen Grüßen'", !deText.includes("Mit freundlichen Grüßen"));
assert(
"DE PDF: owner name is NOT a signature under the closing",
!/Danke für deinen Einkauf[\s\S]{0,40}Gerhard Berger/.test(deText),
);
assert("EN PDF closing reads 'Thank you for your purchase.'", enText.includes("Thank you for your purchase."));
assert("EN PDF no longer contains 'Kind regards'", !enText.includes("Kind regards"));
assert(
"EN PDF: owner name is NOT a signature under the closing",
!/Thank you for your purchase\.[\s\S]{0,40}Gerhard Berger/.test(enText),
);
// Fallback: when footerNoteEn is empty, English uses the German note.
console.log("• Footer note fallback (en → de when EN empty)");
const settingsNoEn = { ...(settings as object), footerNoteEn: "" } as never;
const fbVm = composeInvoice({ order, settings: settingsNoEn, invoiceNumber: "RE-1012", forceLanguage: "en" });
const fbPdf = await renderInvoicePdf(fbVm);
const fbText = await pdfToText(fbPdf);
assert("EN PDF falls back to DE footer when EN empty", fbText.includes("Vielen Dank für Ihren Auftrag."));
// Empty German footer + empty English footer renders nothing (no crash).
console.log("• Footer note empty (no crash, nothing rendered)");
const settingsBlank = { ...(settings as object), footerNote: "", footerNoteEn: "" } as never;
const blankVm = composeInvoice({ order, settings: settingsBlank, invoiceNumber: "RE-1013" });
const blankPdf = await renderInvoicePdf(blankVm);
const blankText = await pdfToText(blankPdf);
assert("blank-footer DE PDF produced", blankPdf.length > 4_000);
assert("blank-footer DE PDF has no German footer text", !blankText.includes("Vielen Dank für Ihren Auftrag."));
if (failed > 0) {
console.error(`\n${failed} assertion(s) FAILED`);
process.exit(1);