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 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-10-22 04:05:55 +02:00
parent 2e55d21937
commit 7fceb47005

View file

@ -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<string, unknown> & {
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<typeof buildShortCollectionSplit>[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);
});