207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
|
|
/**
|
||
|
|
* 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: <SKU code> | …" (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 == <SKU code>` 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: <X> | " 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: <something> | " 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<void> {
|
||
|
|
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<PbRow>({
|
||
|
|
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 == <skuCode>` 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);
|
||
|
|
});
|