/** * Maps scripts/nss-fasteners-pilot.csv into PocketBase * `stock_items` records for review. * * Output: scripts/nss-fasteners-pb-import.json * * No data is written to PocketBase yet — this is for the user to * inspect the field mapping before any ingest. * * Run: bun scripts/nss-fasteners-pb-transform.ts * or: npx tsx scripts/nss-fasteners-pb-transform.ts */ import { readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; // === Configurable per source run === // Override at the command line with NSS_PRODUCT=washers NSS_GRADE=... // to point this transform at a different CSV / category without // editing the file. Defaults below are for the hex nut DIN934 run. const PRODUCT = process.env.NSS_PRODUCT ?? 'hex-nut-din934'; const SOURCE_CSV = join( import.meta.dirname, process.env.NSS_CSV ?? 'nss-fasteners-single-pieces.csv', ); const OUTPUT_JSON = join( import.meta.dirname, process.env.NSS_OUTPUT ?? 'nss-fasteners-pb-import.json', ); const SOURCE_URL = process.env.NSS_URL ?? 'https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773'; const SOURCE_LABEL = process.env.NSS_LABEL ?? 'NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)'; const EXTRACTED_AT = process.env.NSS_EXTRACTED_AT ?? '2026-09-16'; // === supplied by operator on 2026-09-16 === const SUPPLIER_NSS_ID = '6xuu7a0pmmubtqd'; const CATEGORY_FASTENERS_ID = '4emvaanzogj0586'; const MARKUP_PERCENT = 80; // selling_price = cost_price * (1 + MARKUP_PERCENT / 100) const SOURCE_URL = 'https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html?gradematerial=771,773'; const SOURCE_LABEL = 'NSS Fasteners price list - hex nut DIN934 (Grade 8 + A2)'; const EXTRACTED_AT = '2026-09-16'; // YYYY-MM-DD type CsvRow = { SKU: string; PosDescription: string; Description: string; Material: string; Grade: string; Diameter: string; Thread: string; Finish: string; Pack: string; UnitPrice: string; }; const parsePrice = (s: string): number | null => { // "R 59.66" -> 59.66 const m = s.match(/([\d.]+)/); if (!m) return null; const n = parseFloat(m[1]); return Number.isFinite(n) ? n : null; }; const toPbRecord = (r: CsvRow) => { const costPrice = parsePrice(r.UnitPrice); const notes = [ `Material: ${r.Material}`, `Grade: ${r.Grade}`, `Diameter: ${r.Diameter}`, `Thread: ${r.Thread}`, `Finish: ${r.Finish}`, `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', }, ]; return { // === required (SKU-style code — what the POS shows) === // Both `name` and `description` carry the SKU-style code so the // POS sees the same string regardless of which field it pulls. // The friendly product type ("Black Cut Washer DIN126", "Hex // Nut DIN934", etc.) lives in `notes` for human-readable audit. name: r.PosDescription, description: r.PosDescription, // === identifying === sku: r.SKU, // === sourcing / tracking (user's suggestion) === location: SOURCE_LABEL, notes, cost_history: costHistory, // === pricing === cost_price: costPrice, selling_price: costPrice != null ? round2(costPrice * (1 + MARKUP_PERCENT / 100)) : 0, markup: MARKUP_PERCENT, // === inventory defaults (no source data) === unit: r.Pack || 'Each', current_qty: 0, min_qty: 0, target_qty: 0, is_active: true, // === relations (looked up by the operator) === supplier: SUPPLIER_NSS_ID, category: CATEGORY_FASTENERS_ID, }; }; const round2 = (n: number) => Math.round(n * 100) / 100; 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 => { // naive CSV split - no embedded commas in current dataset, so safe. 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); const SOURCE_URL_LINE = SOURCE_URL; writeFileSync( OUTPUT_JSON, JSON.stringify( { meta: { product: PRODUCT, source_csv: SOURCE_CSV.replace(import.meta.dirname + '/', ''), source_url: SOURCE_URL_LINE, extracted_at: EXTRACTED_AT, row_count: records.length, pb_collection: 'stock_items', notes: 'selling_price computed as cost_price * (1 + ' + MARKUP_PERCENT + '%). Re-run with updated cost_price to refresh prices. name=description both carry the SKU-style PosCode (POS-friendly).', }, records, }, null, 2, ), ); console.log(`Wrote ${records.length} records to ${OUTPUT_JSON} (product: ${PRODUCT})`);