/** * Maps scripts/nss-threaded-rods-pilot.csv into PocketBase * `stock_items` records for review. * * Output: scripts/nss-threaded-rods-pb-import.json */ import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const SOURCE_CSV = join( import.meta.dirname, "nss-threaded-rods-pilot.csv", ); const OUTPUT_JSON = join( import.meta.dirname, "nss-threaded-rods-pb-import.json", ); const SOURCE_URL = "https://nssfasteners.co.za/threaded-rods/threaded-rod-din975-6.html?materialclass=1071,1072&length=1152"; const SOURCE_LABEL = "NSS Fasteners price list - threaded rod DIN975/6, 1m (Hi-Tensile 8.8 + Mild Steel 4.8)"; const EXTRACTED_AT = "2026-09-16"; const SUPPLIER_NSS_ID = "6xuu7a0pmmubtqd"; const CATEGORY_FASTENERS_ID = "4emvaanzogj0586"; const MARKUP_PERCENT = 80; type CsvRow = { sku: string; posDescription: string; description: string; material: string; grade: string; diameter: string; length: string; finish: string; pack: string; unitPrice: string; url: string; }; const parsePrice = (s: string): number | null => { const m = s.match(/([\d.]+)/); if (!m) return null; const n = parseFloat(m[1]); return Number.isFinite(n) ? n : null; }; const round2 = (n: number) => Math.round(n * 100) / 100; const toPbRecord = (r: CsvRow) => { const costPrice = parsePrice(r.unitPrice); const notes = [ `Material: ${r.material}`, `Grade: ${r.grade}`, `Diameter: ${r.diameter}`, `Length: ${r.length}`, `Finish: ${r.finish}`, r.pack ? `Pack rule: ${r.pack}` : "", `Source: ${SOURCE_LABEL}`, ] .filter(Boolean) .join(" | "); const costHistory = [ { price: costPrice, currency: "ZAR", source: "nssfasteners.co.za", extracted_at: EXTRACTED_AT, source_url: SOURCE_URL, qty_basis: 1, extraction_method: "browser_automation", notes: SOURCE_LABEL, }, ]; return { name: r.posDescription, description: r.posDescription, sku: r.sku, location: SOURCE_LABEL, notes, cost_history: costHistory, cost_price: costPrice, selling_price: costPrice != null ? round2(costPrice * (1 + MARKUP_PERCENT / 100)) : 0, markup: MARKUP_PERCENT, unit: r.length || "Each", // 1m is the default length for this product family current_qty: 0, min_qty: 0, target_qty: 0, is_active: true, supplier: SUPPLIER_NSS_ID, category: CATEGORY_FASTENERS_ID, }; }; const csv = readFileSync(SOURCE_CSV, "utf8").trim(); const [headerLine, ...dataLines] = csv.split("\n"); const headers = headerLine.split(",") as (keyof CsvRow)[]; const rows: CsvRow[] = dataLines.map((line) => { const cols = line.split(","); const obj = {} as CsvRow; headers.forEach((h, i) => { (obj as Record)[h] = cols[i] ?? ""; }); return obj; }); const records = rows.map(toPbRecord); writeFileSync( OUTPUT_JSON, JSON.stringify( { meta: { source_csv: "scripts/nss-threaded-rods-pilot.csv", source_url: SOURCE_URL, extracted_at: EXTRACTED_AT, row_count: records.length, pb_collection: "stock_items", notes: "selling_price = cost_price * (1 + 80%). name=description both carry the PosCode. unit set to length (1m) since these rods are sold by length, not by piece.", }, records, }, null, 2, ), ); console.log(`Wrote ${records.length} records to ${OUTPUT_JSON}`);