many updates :-)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Regenerates a single existing invoice using the offline session token,
|
||||
* downloads the resulting PDF and pretty-prints the closing/footer text so
|
||||
* we can verify the new wording end-to-end against the live dev store.
|
||||
*
|
||||
* Usage: npx tsx scripts/regenerate-invoice.ts [invoiceNumber]
|
||||
* default invoiceNumber = RE-1001
|
||||
*/
|
||||
import "dotenv/config";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import db from "../app/db.server";
|
||||
import { unauthenticated } from "../app/shopify.server";
|
||||
import { generateInvoice } from "../app/services/invoice/generateInvoice.server";
|
||||
|
||||
async function main() {
|
||||
const wanted = process.argv[2] ?? "RE-1001";
|
||||
|
||||
const invoice = await db.invoice.findFirst({
|
||||
where: { invoiceNumber: wanted, kind: "invoice", cancelledAt: null },
|
||||
orderBy: [{ version: "desc" }, { createdAt: "desc" }],
|
||||
});
|
||||
if (!invoice) throw new Error(`No invoice ${wanted} found in DB`);
|
||||
|
||||
console.log(`Found ${invoice.invoiceNumber} (status=${invoice.status}, sentAt=${invoice.sentAt ?? "—"}) for order ${invoice.orderId}`);
|
||||
|
||||
const { admin } = await unauthenticated.admin(invoice.shopDomain);
|
||||
|
||||
const result = await generateInvoice({
|
||||
shopDomain: invoice.shopDomain,
|
||||
admin,
|
||||
orderId: invoice.orderId,
|
||||
forceRegenerate: true,
|
||||
});
|
||||
|
||||
console.log(`Regenerated → version ${result.version}, pdfUrl=${result.pdfUrl}`);
|
||||
|
||||
const out = resolve(process.cwd(), "data", `regen-${result.invoiceNumber}.pdf`);
|
||||
const res = await fetch(result.pdfUrl);
|
||||
if (!res.ok) throw new Error(`download failed: ${res.status}`);
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
writeFileSync(out, buf);
|
||||
console.log(`Saved ${out} (${buf.length} bytes)`);
|
||||
|
||||
const text = execFileSync("pdftotext", ["-layout", "-enc", "UTF-8", out, "-"], { encoding: "utf8" });
|
||||
|
||||
const okDe = text.includes("Danke für deinen Einkauf");
|
||||
const okEn = text.includes("Thank you for your purchase.");
|
||||
const oldDe = text.includes("Mit freundlichen Grüßen");
|
||||
const oldEn = /\bKind regards\b/.test(text);
|
||||
|
||||
console.log(`\n--- closing text checks ---`);
|
||||
console.log(` contains 'Danke für deinen Einkauf' : ${okDe}`);
|
||||
console.log(` contains 'Thank you for your purchase.': ${okEn}`);
|
||||
console.log(` contains old 'Mit freundlichen Grüßen' : ${oldDe}`);
|
||||
console.log(` contains old 'Kind regards' : ${oldEn}`);
|
||||
|
||||
const langOk = invoice.language === "en" ? okEn : okDe;
|
||||
const cleanOld = invoice.language === "en" ? !oldEn : !oldDe;
|
||||
if (!langOk || !cleanOld) {
|
||||
console.error(`FAIL: closing line did not match expected wording for language=${invoice.language}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`PASS: regenerated ${invoice.invoiceNumber} (lang=${invoice.language}) shows the new closing.`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => db.$disconnect());
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user