From 7fceb47005658d1ec683be8028dffef24fa53888 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 22 Oct 2026 04:05:55 +0200 Subject: [PATCH] chore(scripts): one-off repair of 3 part-invoiced short pickups Applied 2026-10-22 against live PB: 6632, MSP18104, ZA10PO26012735. Co-Authored-By: Claude Opus 5.5 --- scripts/2026-10-22-repair-short-splits.ts | 77 +++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 scripts/2026-10-22-repair-short-splits.ts diff --git a/scripts/2026-10-22-repair-short-splits.ts b/scripts/2026-10-22-repair-short-splits.ts new file mode 100644 index 0000000..d4d8ce9 --- /dev/null +++ b/scripts/2026-10-22-repair-short-splits.ts @@ -0,0 +1,77 @@ +/** + * 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); +});