import "./smoke/_env"; import { writeFile, mkdir } from "node:fs/promises"; import path from "node:path"; import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin"; import { renderDeliveryNotePdf } from "../src/lib/pdf/render"; import { resolveDeliveryNoteLineQty } from "../src/lib/pdf/delivery-note-document"; import { fetchLetterhead, letterheadForCompany, loadLetterheadLogoDataUri, } from "../src/lib/letterhead"; type Item = { id: string; description?: string | null; delivery_note_number?: string | null; order_item_details?: unknown; order_qty?: number | null; qty_collected?: number | null; order_item_attachements?: unknown; expand?: { order?: { order_number?: string | null; expand?: { company?: { company?: string | null }; customer?: { customer?: string | null }; }; }; }; }; type Attachment = { token: string; filename: string }; type Result = { deliveryNoteNumber: string; items: number; regenerated: number; errors: string[]; }; function readDateField(value: unknown, key: string): string { if (!value || typeof value !== "object" || Array.isArray(value)) return ""; const candidate = (value as Record)[key]; return typeof candidate === "string" && candidate.length >= 10 ? candidate.slice(0, 10) : ""; } function readAttachments(value: unknown): string[] { if (typeof value === "string" && value.trim()) return [value.trim()]; if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0); return []; } function filenameFromToken(token: string): string { const match = token.match(/^[^_]+_[^_]+_(.+)$/); return match?.[1] ?? token; } function isDeliveryNotePdf(filename: string, deliveryNoteNumber: string): boolean { return filename.replace(/_[a-z0-9]{10,}(?=\.pdf$)/i, "").toLowerCase() === `${deliveryNoteNumber.toLowerCase()}.pdf`; } function parseArgs() { const args = process.argv.slice(2); const dryRun = args.includes("--dry-run"); const limitIndex = args.indexOf("--limit"); const limit = limitIndex >= 0 ? Number(args[limitIndex + 1]) : Infinity; const explicit = args.flatMap((arg, index) => (arg === "--dn" ? [args[index + 1]] : [])); return { dryRun, limit: Number.isFinite(limit) && limit > 0 ? limit : Infinity, explicit }; } async function main() { const { dryRun, limit, explicit } = parseArgs(); const pb = await getAdminPocketBase(); const items = await pb.collection("order_items").getFullList({ filter: 'delivery_note_number != ""', sort: "delivery_note_number,created", expand: "order.company,order.customer", }); const grouped = new Map(); for (const item of items) { const number = item.delivery_note_number?.trim(); if (!number || (explicit.length > 0 && !explicit.includes(number))) continue; const group = grouped.get(number) ?? []; group.push(item); grouped.set(number, group); } const groups = [...grouped.entries()].slice(0, limit); if (groups.length === 0) throw new Error("No existing delivery notes matched."); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const reportDir = path.resolve(process.cwd(), "reports"); await mkdir(reportDir, { recursive: true }); const results: Result[] = []; for (const [deliveryNoteNumber, noteItems] of groups) { const result: Result = { deliveryNoteNumber, items: noteItems.length, regenerated: 0, errors: [] }; try { const first = noteItems[0]; const order = first.expand?.order; const companyName = order?.expand?.company?.company ?? ""; const letterheadCode = letterheadForCompany(companyName) ?? "CNL"; const [letterhead, logoDataUri] = await Promise.all([ fetchLetterhead(letterheadCode), loadLetterheadLogoDataUri(letterheadCode), ]); const pdfBuffer = await renderDeliveryNotePdf({ header: { deliveryNoteNumber, dateIso: readDateField(first.order_item_details, "deliveryNoteDate") || new Date().toISOString().slice(0, 10), orderNumber: order?.order_number ?? "", companyName, customerName: order?.expand?.customer?.customer ?? "", }, lines: noteItems.map((item) => ({ id: item.id, description: (item.description ?? "").trim() || "(no description)", qty: resolveDeliveryNoteLineQty({ orderQty: item.order_qty, qtyCollected: item.qty_collected }), })), letterhead, logoDataUri, }); if (!dryRun) { for (const item of noteItems) { const existing: Attachment[] = readAttachments(item.order_item_attachements).map((token) => ({ token, filename: filenameFromToken(token) })); const preserved = existing.filter((attachment) => !isDeliveryNotePdf(attachment.filename, deliveryNoteNumber)); const form = new FormData(); for (const attachment of preserved) form.append("order_item_attachements", attachment.token); form.append("order_item_attachements", new Blob([new Uint8Array(pdfBuffer)], { type: "application/pdf" }), `${deliveryNoteNumber}.pdf`); await pb.collection("order_items").update(item.id, form); result.regenerated += 1; } } } catch (error) { result.errors.push(error instanceof Error ? error.message : String(error)); } results.push(result); process.stdout.write(`${dryRun ? "Would regenerate" : "Regenerated"} ${deliveryNoteNumber}: ${result.items} item(s)${result.errors.length ? ` — ${result.errors.join("; ")}` : ""}\n`); } await writeFile(path.join(reportDir, `delivery-note-regen-${timestamp}.json`), JSON.stringify({ dryRun, results }, null, 2)); if (results.some((result) => result.errors.length > 0)) process.exitCode = 1; } void main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; });