// .mts, not .ts: this script uses top-level await, and the package has no // "type": "module", so tsx would otherwise transform it as CJS and fail. import { pbAdmin } from "../src/lib/pb"; import { classifyUnit, poMatchCounts, verifyPurchaseOrderIds } from "../src/lib/dn-pipeline/compare"; type Row = Record; const args = process.argv.slice(2); const batchId = args.find((arg) => !arg.startsWith("-")); const all = args.includes("--all"); if (!batchId && !all) { console.error("Usage: npx tsx scripts/compare-dn-pipelines.ts | --all"); process.exit(2); } const pb = await pbAdmin(); const batches = (all ? await pb.collection("dn_batches").getFullList({ sort: "created" }) : [await pb.collection("dn_batches").getOne(batchId!)]).map((row) => row as Row); const reports: Row[] = []; for (const batch of batches) { const [pages, notes] = await Promise.all([ pb.collection("dn_batch_pages").getFullList({ filter: `batch = ${JSON.stringify(batch.id)}`, fields: "id,read_status" }), pb.collection("dn_batch_notes").getFullList({ filter: `batch = ${JSON.stringify(batch.id)}`, sort: "note_index", fields: "id,stage,status,pages,purchase_order,items,checks,doc_number,delivery_note_number" }), ]); const poIds = [...new Set((notes as Row[]).map((note) => typeof note.purchase_order === "string" ? note.purchase_order : "").filter(Boolean))]; const verifiedPoIds = await verifyPurchaseOrderIds(poIds, async (po) => { try { const row = await pb.collection("purchase_orders").getOne(po, { fields: "id" }) as Row; return { id: String(row.id) }; } catch (error) { throw new Error(`purchase order verification failed for ${po}: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } }); const poItems = (await Promise.all([...verifiedPoIds].map((po) => pb.collection("purchase_order_items").getFullList({ filter: `purchase_order = ${JSON.stringify(po)}`, fields: "id,purchase_order,unit,qty,rate,description" })))).flat() as Row[]; const itemById = new Map(poItems.map((item) => [item.id, item])); let matchedItems = 0; let itemCount = 0; let unitCompatible = 0; let unitIncompatible = 0; let unitUnknown = 0; let quantity = 0; let computedValue = 0; for (const note of notes as Row[]) { for (const line of Array.isArray(note.items) ? note.items : []) { if (!line || typeof line !== "object") continue; itemCount++; const linked = typeof line.purchase_order_item === "string" ? line.purchase_order_item : ""; const poItem = itemById.get(linked); const qty = typeof line.qty === "number" ? line.qty : Number(line.qty ?? 0); quantity += Number.isFinite(qty) ? qty : 0; if (poItem) { matchedItems++; const compatibility = classifyUnit(line.unit, poItem.unit); if (compatibility === "compatible") unitCompatible++; else if (compatibility === "incompatible") unitIncompatible++; else unitUnknown++; const rate = Number(poItem.rate); if (Number.isFinite(rate) && Number.isFinite(qty)) computedValue += qty * rate; } } } const coveredPages = new Set((notes as Row[]).flatMap((note) => Array.isArray(note.pages) ? note.pages.filter((page: unknown) => Number.isInteger(page)) : [])); reports.push({ batchId: batch.id, site: batch.site ?? null, pageCoverage: { total: pages.length, assigned: coveredPages.size, open: Math.max(0, pages.length - coveredPages.size), read: (pages as Row[]).filter((page) => page.read_status === "read").length }, candidateCount: notes.length, stages: Object.fromEntries([...new Set((notes as Row[]).map((note) => String(note.stage ?? note.status ?? "unknown")))].map((stage) => [stage, (notes as Row[]).filter((note) => String(note.stage ?? note.status ?? "unknown") === stage).length])), poMatch: poMatchCounts(notes as Row[], verifiedPoIds), itemMatch: { matched: matchedItems, total: itemCount }, units: { compatible: unitCompatible, incompatible: unitIncompatible, unknown: unitUnknown, matchedItems }, quantity, computedValue: Math.round(computedValue * 100) / 100, writes: false, }); } console.log(JSON.stringify({ generatedAt: new Date().toISOString(), reports }, null, 2));