/** * Merge YG-1 drafts. * * PO26073102STK (existing draft, 4 HSS drill-bit lines, 2026-07-31) * PO26080301STK (new draft, 16 tap lines, loaded 2026-08-03 from quote SOQ078608) * * Both are still in `draft` status (neither has been sent to the supplier), * so the merge is purely local — the supplier hasn't seen either PO number. * * ## Direction * * Keep the older PO number (`PO26073102STK`) — it's the operator's * existing accumulated draft per the system's "one draft per supplier" * design (see `stock-quick-draft-service.ts`). The new draft was created * by `2026-08-03-load-yg1-quote-soq078608.ts`; merging its lines back * into the existing draft collapses the two into a single draft. * * ## What this does * * 1. Updates all 16 `stock_purchase_items` rows on `PO26080301STK` * to point at `PO26073102STK` (just flip the `purchase` field). * 2. Deletes the now-empty `PO26080301STK` header. * 3. Updates `PO26073102STK`'s `notes` to record both quote sources. * * The line `id`s, `created` timestamps, `description`, `quantity`, * `cost_price`, `notes` (including the "No stock" flag on TS2352932), * and `stock_item` references are all preserved — this is a pure * parent-pointer swap on the line rows. * * ## Preconditions * * - Both purchase ids exist and are in `status="draft"`. * - Neither has been sent (`email_message_id` is empty, no `documents`). * - The new draft has 16 lines, the old draft has 4. * * Run: `npx tsx scripts/2026-08-03-merge-yg1-drafts.ts` */ import PocketBase from "pocketbase"; const PB_URL = "https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io"; const ADMIN_EMAIL = "david.els1@outlook.com"; const ADMIN_PASSWORD = "1234567890"; const EXISTING_DRAFT_ID = "2a6qo4nxb2ni4tm"; // PO26073102STK — keep this one const NEW_DRAFT_ID = "cj0h9jdsas2s4gk"; // PO26080301STK — drain into the above async function main() { const pb = new PocketBase(PB_URL); await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); console.log("Authenticated as admin"); // ── 1. Pre-flight checks ───────────────────────────────────────── console.log("\n=== Pre-flight ==="); const existing = await pb.collection("stock_purchases").getOne(EXISTING_DRAFT_ID); const newDraft = await pb.collection("stock_purchases").getOne(NEW_DRAFT_ID); console.log(` PO26073102STK status=${existing.status} notes="${existing.notes}"`); console.log(` PO26080301STK status=${newDraft.status} notes="${newDraft.notes}"`); if (existing.status !== "draft" || newDraft.status !== "draft") { throw new Error(`Refusing to merge: both drafts must be in 'draft' status (existing=${existing.status}, new=${newDraft.status})`); } if (existing.email_message_id || newDraft.email_message_id) { throw new Error("Refusing to merge: one or both drafts already have an email_message_id (already sent?)"); } const existingLines = await pb.collection("stock_purchase_items").getList(1, 1, { filter: `purchase = "${EXISTING_DRAFT_ID}"`, }); const newLines = await pb.collection("stock_purchase_items").getList(1, 1, { filter: `purchase = "${NEW_DRAFT_ID}"`, }); console.log(` PO26073102STK lines: ${existingLines.totalItems}`); console.log(` PO26080301STK lines: ${newLines.totalItems}`); // ── 2. Re-parent the new draft's lines ─────────────────────────── console.log("\n=== Re-parenting lines ==="); const newLinesList = await pb .collection("stock_purchase_items") .getFullList({ filter: `purchase = "${NEW_DRAFT_ID}"`, fields: "id,description,quantity" }); for (const ln of newLinesList) { await pb.collection("stock_purchase_items").update(ln.id, { purchase: EXISTING_DRAFT_ID, }); console.log(` ${ln.id} → PO26073102STK (${ln.description.slice(0, 40)} qty=${ln.quantity})`); } // ── 3. Delete the empty new draft ──────────────────────────────── console.log("\n=== Deleting empty draft PO26080301STK ==="); await pb.collection("stock_purchases").delete(NEW_DRAFT_ID); console.log(` deleted ${NEW_DRAFT_ID}`); // ── 4. Update notes on the surviving draft ─────────────────────── console.log("\n=== Updating notes on PO26073102STK ==="); const mergedNotes = "YG-1 stock draft. Sources: existing draft since 2026-07-31 (4 HSS drill-bit lines) + YG-1 quote SOQ078608 dated 2026-08-03 for customer CNL001 (16 line items, merged 2026-08-03)."; await pb.collection("stock_purchases").update(EXISTING_DRAFT_ID, { notes: mergedNotes, }); console.log(` set notes: "${mergedNotes}"`); // ── 5. Verify ──────────────────────────────────────────────────── console.log("\n=== Post-merge verification ==="); const finalDraft = await pb.collection("stock_purchases").getOne(EXISTING_DRAFT_ID, { expand: "supplier", }); const finalLines = await pb.collection("stock_purchase_items").getList(1, 1, { filter: `purchase = "${EXISTING_DRAFT_ID}"`, }); console.log(` Purchase: ${finalDraft.purchase_number} (${finalDraft.expand?.supplier?.suppliername})`); console.log(` Status: ${finalDraft.status}`); console.log(` Lines: ${finalLines.totalItems}`); const totalCost = await pb .collection("stock_purchase_items") .getFullList({ filter: `purchase = "${EXISTING_DRAFT_ID}"` }); const total = totalCost.reduce((s, l) => s + l.quantity * l.cost_price, 0); console.log(` Total cost (post-discount, excl VAT): R ${total.toFixed(2)}`); // Confirm the new-draft id is gone. try { await pb.collection("stock_purchases").getOne(NEW_DRAFT_ID); throw new Error("New draft still exists after delete!"); } catch (err) { if (err instanceof Error && err.message.includes("not found")) { console.log(` PO26080301STK (${NEW_DRAFT_ID}): deleted ✓`); } else { throw err; } } } main().catch((err) => { console.error("FATAL:", err); process.exit(1); });