/** * One-off ingest: write the 142 NSS spring pin ISO8752 items * (Spring Steel CS70) from `scripts/nss-spring-pins-iso8752-pb-import.json` * into the PocketBase `stock_items` collection. * * Same safety rails as the other ingest scripts (idempotent, * applyDefaultMarkupToPatch, per-row try/catch). * * Run: * npx tsx scripts/nss-spring-pins-iso8752-ingest.ts --dry-run --limit 5 * npx tsx scripts/nss-spring-pins-iso8752-ingest.ts --limit 5 * npx tsx scripts/nss-spring-pins-iso8752-ingest.ts */ import "./_env"; import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin"; import { applyDefaultMarkupToPatch } from "../src/lib/server/stock-default-markup-service"; import { readFileSync } from "node:fs"; import { join } from "node:path"; const IMPORT_JSON = join( import.meta.dirname, "nss-spring-pins-iso8752-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 = { name: string; sku: string; description: string; location: string; notes: string; cost_history: Array<{ price: number; currency: string; source: string; extracted_at: string; source_url: string; qty_basis: number; extraction_method: string; notes: string; }>; cost_price: number; unit: string; current_qty: number; min_qty: number; target_qty: number; is_active: boolean; supplier: string; category: string; }; function buildPatch(r: ImportRecord): Record { const seedEntry = { cost_price: r.cost_price, quantity: 0, purchased_at: r.cost_history[0]?.extracted_at ?? new Date().toISOString(), purchase_id: "nss-spring-pins-iso8752-2026-09-16", supplier_id: r.supplier, source: "nssfasteners.co.za", source_url: r.cost_history[0]?.source_url ?? "", currency: r.cost_history[0]?.currency ?? "ZAR", qty_basis: r.cost_history[0]?.qty_basis ?? 1, extraction_method: r.cost_history[0]?.extraction_method ?? "browser_automation", notes: r.cost_history[0]?.notes ?? r.location, }; return applyDefaultMarkupToPatch( { sku: r.sku, name: r.name, description: r.description, unit: r.unit, location: r.location, current_qty: r.current_qty, min_qty: r.min_qty, target_qty: r.target_qty, supplier: r.supplier, category: r.category, is_active: r.is_active, notes: r.notes, cost_price: r.cost_price, cost_history: [seedEntry], }, undefined, ); } async function loadExistingSkus(): Promise> { const pb = await getAdminPocketBase(); const rows = await pb.collection("stock_items").getFullList<{ sku: string }>({ fields: "sku", batch: 500, }); return new Set(rows.map((r) => (r.sku || "").trim()).filter(Boolean)); } async function main(): Promise { const t0 = Date.now(); const importDoc = JSON.parse(readFileSync(IMPORT_JSON, "utf8")) as { records: ImportRecord[]; }; const allRecords = importDoc.records; const records = Number.isFinite(limit) ? allRecords.slice(0, limit) : allRecords; console.log(`Loaded ${allRecords.length} records from ${IMPORT_JSON}`); if (records.length < allRecords.length) { console.log(` --limit applied: running on first ${records.length}`); } console.log(`Mode: ${dryRun ? "DRY RUN (no writes)" : "WRITE"}`); const existingSkus = await loadExistingSkus(); console.log(`Existing SKUs in PB: ${existingSkus.size}`); const toIngest = records.filter((r) => !existingSkus.has(r.sku)); const skipped = records.filter((r) => existingSkus.has(r.sku)); console.log(`Records to ingest: ${toIngest.length}`); if (skipped.length > 0) { console.log(`Records skipped (already in PB): ${skipped.length}`); } if (dryRun) { console.log("\nDry run — preview of what would be written:\n"); let i = 0; for (const r of toIngest.slice(0, 5)) { const patch = buildPatch(r); console.log( ` [${++i}] ${patch.sku} ${patch.name} cost=R ${patch.cost_price} sell=R ${patch.selling_price} markup=${patch.markup}% cat=${patch.category}`, ); } if (toIngest.length > 5) { console.log(` ... and ${toIngest.length - 5} more`); } console.log(`\n duration: ${((Date.now() - t0) / 1000).toFixed(1)}s`); return; } const pb = await getAdminPocketBase(); let created = 0; let failed = 0; const failures: Array<{ sku: string; error: string }> = []; for (const r of toIngest) { try { const patch = buildPatch(r); await pb.collection("stock_items").create(patch); created++; if (created % 50 === 0 || created === toIngest.length) { console.log( ` ... ${created}/${toIngest.length} written (last: ${patch.sku})`, ); } } catch (err) { failed++; const message = err instanceof Error ? err.message : String(err); failures.push({ sku: r.sku, error: message }); } } console.log("\n=== Ingest summary ==="); console.log(` Created: ${created}`); console.log(` Failed: ${failed}`); console.log(` Skipped: ${skipped.length} (already in PB)`); 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); });