/** * One-off repair for the short pickups that were part-invoiced before * Collection Confirm learned to split them (commit 2e55d21). * * Each line was invoiced for what was collected, then bounced back to * `ready_invoice` with the rest still owed and never re-ordered. This does * what the new split does, after the fact: * - the original is cut to the invoiced qty and closed as `invoiced`; * - the rest becomes a new `pending` line on the same order (New tab), * with the `shortCollection` marker and its original PO for Back order. * * Guarded per line: still `ready_invoice`, `qty_invoiced > 0`, * `qty_invoiced == qty_collected < quantity`. Dry run unless `--apply`. * * npx tsx scripts/2026-10-22-repair-short-splits.ts [--apply] */ import "./_env"; import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin"; import { runWithAuditActor } from "../src/lib/server/audit-context"; import { appendNote } from "../src/lib/note-feed"; import { buildShortCollectionSplit, newRecordId } from "../src/lib/short-collection"; const IDS = [ "3sipxowwqkfw9x3", // 6632 U BOLT M24(400) HDG — 549 of 770 "4juzxtt2yj3tsyl", // MSP18104 Insertion rubber — 5 of 8 "9gojrwwszua8sgy", // ZA10PO26012735 Welding rod 2.5mm — 3 of 42 ]; const APPLY = process.argv.includes("--apply"); const actor = { id: "claude-short-split-repair", name: "Claude (short-collection repair)" }; async function main() { const pb = await getAdminPocketBase(); const at = new Date().toISOString(); for (const id of IDS) { const row = (await pb.collection("order_items").getOne(id, { expand: "purchase_order" })) as Record & { expand?: { purchase_order?: { po_number?: string } }; }; const quantity = Number(row.quantity); const collected = Number(row.qty_collected); const invoiced = Number(row.qty_invoiced); const label = `${id} ${String(row.description)} (${collected}/${quantity}, invoiced ${invoiced})`; if (row.item_status !== "ready_invoice" || !(invoiced > 0) || invoiced !== collected || collected >= quantity) { console.log(`SKIP ${label}: status ${String(row.item_status)} — no longer the stuck shape`); continue; } const split = buildShortCollectionSplit(row as Parameters[0], { remainderId: newRecordId(), poNumber: row.expand?.purchase_order?.po_number ?? "", at, }); if (!split) { console.log(`SKIP ${label}: nothing to split`); continue; } const originalPatch = { ...split.originalPatch, item_status: "invoiced", notes: appendNote(row.notes as string | null, { ts: at, actor, category: "quantity", body: `Short collection repaired: ${collected} of ${quantity} collected and invoiced on ${String(row.customer_invoice_number ?? "")}. The ${split.remainderQty} still owed moved to a new line in New on this order.`, }), }; console.log(`${APPLY ? "APPLY" : "DRY "} ${label} → original qty ${collected} invoiced; new line ${split.remainder.id} qty ${split.remainderQty} pending (PO ${row.expand?.purchase_order?.po_number ?? "?"})`); if (!APPLY) continue; const batch = pb.createBatch(); batch.collection("order_items").create(split.remainder); batch.collection("order_items").update(id, originalPatch); await batch.send(); } } runWithAuditActor({ ...actor, source: "server", ignoreRequestUser: true }, main).catch((err) => { console.error(err); process.exit(1); });