/** * Revert the name/description swap I did in the previous fix: * * The 99 NSS-imported rows currently have * name = "Hex Nut DIN934" (was SKU code originally) * description = "Hex Nut DIN934" (unchanged throughout) * notes = "PosCode: | …" (PosCode prepended) * * What they should be is the ORIGINAL ingest shape: * name = SKU code, e.g. "M48 NUT 8 PLN" (the PosCode) * description = SKU code, e.g. "M48 NUT 8 PLN" (matches name now) * notes = original notes without the PosCode prefix * * Net result: the POS reads `description` and shows the SKU code * (e.g. "M48 NUT 8 PLN") because the SKU code is now the * canonical label — and the original `name` is back where it was. * * The SKU code is sourced from `scripts/nss-fasteners-pb-import.json`, * which is the original ingest payload (the `name` field there is * the SKU code we want back in PB). * * Idempotent: if `name == description == ` and `notes` * no longer has the `PosCode:` prefix, the row is skipped. * * Run: * npx tsx scripts/nss-fasteners-revert-name-description.ts --dry-run * npx tsx scripts/nss-fasteners-revert-name-description.ts --limit 5 * npx tsx scripts/nss-fasteners-revert-name-description.ts */ import "./_env"; // side-effect import: loads .env.local import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin"; import { readFileSync } from "node:fs"; import { join } from "node:path"; const NSS_LOCATION = "NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)"; const IMPORT_JSON = join(import.meta.dirname, "nss-fasteners-pb-import.json"); const args = process.argv.slice(2); const dryRun = args.includes("--dry-run"); const limitArg = args.find((a) => a.startsWith("--limit=")); const limit = limitArg ? Number(limitArg.split("=")[1]) : Infinity; type ImportRecord = { sku: string; name: string; // original SKU code, e.g. "M48 NUT 8 PLN" description: string; notes: string; }; type PbRow = { id: string; sku: string; name: string; description: string; notes: string; }; /** * Strip the "PosCode: | " prefix that the previous fix added. * If the prefix isn't there (e.g. the row was already reverted, or * was never touched), notes is returned unchanged. * * Anchored to the literal "PosCode:" tag — we never trim anything * else, so the original notes content (Material / Grade / etc.) is * preserved exactly. */ function stripPosCodePrefix(notes: string): string { // Match "PosCode: | " at the very start. const m = notes.match(/^PosCode:\s*[^\|]+\|\s*/); if (!m) return notes; return notes.substring(m[0].length); } async function main(): Promise { const t0 = Date.now(); const importDoc = JSON.parse(readFileSync(IMPORT_JSON, "utf8")) as { records: ImportRecord[]; }; const skuCodeBySku = new Map( importDoc.records.map((r) => [r.sku, r.name] as const), ); const pb = await getAdminPocketBase(); const allRows = await pb.collection("stock_items").getFullList({ filter: `location = "${NSS_LOCATION}"`, fields: "id,sku,name,description,notes", batch: 200, sort: "sku", }); console.log(`Found ${allRows.length} NSS-imported rows in PB.`); const rowsToProcess = Number.isFinite(limit) ? allRows.slice(0, limit) : allRows; if (rowsToProcess.length < allRows.length) { console.log(` --limit applied: processing first ${rowsToProcess.length}`); } // Build the to-do list. Idempotency check: skip rows that already // have `name == description == ` and `notes` without the // PosCode prefix. const todo: Array<{ id: string; sku: string; skuCode: string; currName: string; currDesc: string; newName: string; newDesc: string; newNotes: string; }> = []; const already = { name: 0, desc: 0, notes: 0 }; for (const r of rowsToProcess) { const skuCode = skuCodeBySku.get(r.sku) ?? r.name; // safe fallback const desiredName = skuCode; const desiredDesc = skuCode; const desiredNotes = stripPosCodePrefix(r.notes ?? ""); const nameOk = r.name === desiredName; const descOk = r.description === desiredDesc; const notesOk = desiredNotes === (r.notes ?? ""); if (nameOk) already.name++; if (descOk) already.desc++; if (notesOk) already.notes++; if (nameOk && descOk && notesOk) continue; todo.push({ id: r.id, sku: r.sku, skuCode, currName: r.name, currDesc: r.description, newName: desiredName, newDesc: desiredDesc, newNotes: desiredNotes, }); } console.log(`Already up-to-date: name=${already.name}, description=${already.desc}, notes=${already.notes} (of ${rowsToProcess.length}).`); console.log(`To update: ${todo.length}`); if (todo.length > 0) { console.log("\nSample diffs:"); for (const t of todo.slice(0, 5)) { console.log(` ${t.sku}`); console.log(` name: "${t.currName}" → "${t.newName}"`); console.log(` description: "${t.currDesc}" → "${t.newDesc}"`); console.log(` notes: drops "PosCode: ${t.skuCode} | " prefix`); } if (todo.length > 5) { console.log(` … and ${todo.length - 5} more`); } } if (dryRun) { console.log(`\nDRY RUN — no writes. duration: ${((Date.now() - t0) / 1000).toFixed(1)}s`); return; } let updated = 0; let failed = 0; const failures: Array<{ sku: string; error: string }> = []; for (const t of todo) { try { await pb.collection("stock_items").update(t.id, { name: t.newName, description: t.newDesc, notes: t.newNotes, }); updated++; if (updated % 25 === 0 || updated === todo.length) { console.log(` ... ${updated}/${todo.length} updated (last: ${t.sku})`); } } catch (err) { failed++; const message = err instanceof Error ? err.message : String(err); failures.push({ sku: t.sku, error: message }); } } console.log("\n=== Revert summary ==="); console.log(` Updated: ${updated}`); console.log(` Failed: ${failed}`); console.log(` Skipped (already correct): ${rowsToProcess.length - todo.length}`); console.log(` Duration: ${((Date.now() - t0) / 1000).toFixed(1)}s`); if (failures.length > 0) { console.log("\nFailures:"); for (const f of failures) { console.log(` ${f.sku} → ${f.error}`); } process.exitCode = 1; } } main().catch((err) => { console.error("FATAL:", err); process.exit(1); });