/** * Maps scripts/nss-split-pins-din94-pilot.csv into PocketBase * `stock_items` records for review. * * Output: scripts/nss-split-pins-din94-pb-import.json * * Run: npx tsx scripts/nss-split-pins-din94-pb-transform.ts */ import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const SOURCE_CSV = join( import.meta.dirname, "nss-split-pins-din94-pilot.csv", ); const OUTPUT_JSON = join( import.meta.dirname, "nss-split-pins-din94-pb-import.json", ); const SOURCE_URL = "https://nssfasteners.co.za/pins/split-pins/split-pin-din94.html"; const SOURCE_LABEL = "NSS Fasteners price list - split pin DIN94"; 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}`, r.grade ? `Grade: ${r.grade}` : null, `Diameter: ${r.diameter}`, `Length: ${r.length}`, `Finish: ${r.finish}`, r.pack ? `Pack: ${r.pack}` : null, `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: "Each", 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-split-pins-din94-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 (e.g., '5x50 SPLIT PIN PLN'). unit=Each (split pins are sold by piece). DIN94 split pins come in Mild Steel Grade 4.8 (Zinc Plated) and Stainless Steel A2/A4 (Plain). Pack rule captures 'Pack of N' from the page (25/50/100/200). Source URL is the unfiltered category landing page; each row carries its own product URL.", }, records, }, null, 2, ), ); console.log(`Wrote ${records.length} records to ${OUTPUT_JSON}`);