/* eslint-disable */ /** * Test data seeder — creates 5 dummy grinders in `price_list` and * a fresh test quote with 3 items exercising different matcher * scenarios (exact SKU, fuzzy description, no match). * * Markers (so cleanup is a one-liner later): * - All 5 price_list rows have brand = "TEST_GRINDER" * - The test quote's description starts with "[TEST-DATA]" * * Cleanup commands (printed at the end): * - Delete the 5 grinders: * pb.collection("price_list").delete(ids…) * - Delete the test quote (cascades to quote_items via PB rule, * or do both explicitly if cascade isn't configured): * pb.collection("quotes").delete(quoteId) * * Run with: * env PB_ADMIN_EMAIL=… PB_ADMIN_PASSWORD=… NEXT_PUBLIC_POCKETBASE_URL=… * npx tsx scripts/dev/seed-test-grinders.ts */ import PocketBase from "pocketbase"; const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL; const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL; const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD; if (!PB_URL || !ADMIN_EMAIL || !ADMIN_PASSWORD) { throw new Error( "Set PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, NEXT_PUBLIC_POCKETBASE_URL." ); } const pb = new PocketBase(PB_URL); async function main() { await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD).catch((e) => { console.error("Admin auth failed:", e); process.exit(1); }); /** Build search_text the same way the migration backfill does: * sku + description + brand + category + subcategory + unit + * prices + markup + spec kv pairs + synonyms joined. Lowercased, * single-space separated. Tier 3 fuzzy matching uses this field. */ function buildSearchText(row: { sku: string; description: string; brand: string; category: string; unit: string; cost_price: number; selling_price: number; markup: number; specifications?: Record; }): string { const bits: string[] = []; bits.push(row.sku.toLowerCase()); bits.push(row.description.toLowerCase()); bits.push(row.brand.toLowerCase()); bits.push(row.category.toLowerCase()); bits.push(row.unit.toLowerCase()); bits.push(String(row.cost_price)); bits.push(String(Math.round(row.cost_price))); bits.push(String(row.selling_price)); bits.push(String(Math.round(row.selling_price))); bits.push(String(row.markup)); const specs = row.specifications || {}; for (const [k, v] of Object.entries(specs)) { bits.push(k.toLowerCase()); if (Array.isArray(v)) { for (const item of v) bits.push(String(item).toLowerCase()); } else if (v != null) { bits.push(String(v).toLowerCase()); } } return bits.join(" ").replace(/\s+/g, " ").trim(); } /** Strip non-alphanumeric, uppercase — the canonical sku_normalised form * the matcher uses for Tier 1/Tier 2 equality + substring checks. */ function normaliseSku(sku: string): string { return sku.toUpperCase().replace(/[^A-Z0-9]/g, ""); } // JEISA-WESTSCOTT — the dominant supplier in the catalogue. Using it // keeps the test data consistent with what the picker shows on existing rows. const SUPPLIER_JEISA = "q6hpwbxsicyoiuu"; // 5 dummy grinders. Designed to exercise three match tiers: // - Exact SKU match: MAK-AG-125-9558 // - Tier 2 substring: MAK (matches MAK-*) // - Tier 3 fuzzy: "angle grinder 125mm" hits multiple rows by brand/category // - No-match sanity: a circular saw item won't match const GRINDERS = [ { sku: "MAK-AG-125-9558", description: "MAKITA 9558NB ANGLE GRINDER 125MM 840W", brand: "TEST_GRINDER", category: "angle_grinder", subcategory: "small", unit: "each", cost_price: 1450.00, selling_price: 1850.00, markup: 27.6, specifications: { diameter_mm: 125, power_w: 840, synonyms: [ "makita grinder", "9558nb", "angle grinder 125mm", "small angle grinder", "MAKITA 9558NB ANGLE GRINDER 125MM 840W", ], }, }, { sku: "BOS-AG-125-GWS850", description: "BOSCH GWS 850C ANGLE GRINDER 125MM 850W", brand: "TEST_GRINDER", category: "angle_grinder", subcategory: "small", unit: "each", cost_price: 1395.00, selling_price: 1795.00, markup: 28.7, specifications: { diameter_mm: 125, power_w: 850, synonyms: [ "bosch grinder", "gws 850c", "angle grinder 125mm", "small angle grinder", "BOSCH GWS 850C ANGLE GRINDER 125MM 850W", ], }, }, { sku: "DEW-AG-125-DWE4257", description: "DEWALT DWE4257 ANGLE GRINDER 125MM 1500W", brand: "TEST_GRINDER", category: "angle_grinder", subcategory: "medium", unit: "each", cost_price: 2895.00, selling_price: 3690.00, markup: 27.5, specifications: { diameter_mm: 125, power_w: 1500, synonyms: [ "dewalt grinder", "dwe4257", "angle grinder 125mm", "medium angle grinder", "DEWALT DWE4257 ANGLE GRINDER 125MM 1500W", ], }, }, { sku: "MAK-AG-125-9558HD", description: "MAKITA 9558PB ANGLE GRINDER 125MM HEAVY DUTY", brand: "TEST_GRINDER", category: "angle_grinder", subcategory: "small", unit: "each", cost_price: 1750.00, selling_price: 2250.00, markup: 28.6, specifications: { diameter_mm: 125, power_w: 840, synonyms: [ "makita grinder", "9558pb", "angle grinder 125mm", "small angle grinder", "MAKITA 9558PB ANGLE GRINDER 125MM HEAVY DUTY", ], }, }, { sku: "GEN-DISC-125-CUT", description: "GENERIC 125MM CUTTING DISC FOR ANGLE GRINDER", brand: "TEST_GRINDER", category: "angle_grinder", subcategory: "consumable", unit: "each", cost_price: 18.50, selling_price: 28.00, markup: 51.4, specifications: { diameter_mm: 125, type: "cutting_disc", synonyms: [ "cutting disc", "125mm disc", "angle grinder disc", "GENERIC 125MM CUTTING DISC FOR ANGLE GRINDER", ], }, }, ]; console.log("=== Inserting 5 test grinders ==="); const grinderIds: string[] = []; for (const g of GRINDERS) { const payload = { sku: g.sku, sku_normalised: normaliseSku(g.sku), description: g.description, brand: g.brand, category: g.category, subcategory: g.subcategory, unit: g.unit, cost_price: g.cost_price, selling_price: g.selling_price, markup: g.markup, supplier: SUPPLIER_JEISA, specifications: g.specifications, search_text: buildSearchText({ sku: g.sku, description: g.description, brand: g.brand, category: g.category, unit: g.unit, cost_price: g.cost_price, selling_price: g.selling_price, markup: g.markup, specifications: g.specifications, }), }; const created = await pb.collection("price_list").create(payload); grinderIds.push(created.id); console.log(` ✓ ${g.sku.padEnd(22)} id=${created.id} R${g.cost_price}`); } // === Quote + items === // Use David Els as the customer (real id probed earlier). const CUSTOMER_ID = "jjjkadudyctuke5"; const QUOTE_NUMBER = "Q" + Date.now(); console.log(`\n=== Creating test quote ${QUOTE_NUMBER} ===`); const quote = await pb.collection("quotes").create({ quote_number: QUOTE_NUMBER, customer: CUSTOMER_ID, status: "claimed", company_letterhead: "CNL", description: "[TEST-DATA] 3 grinders for matcher testing — delete after use. " + "Item 1 = exact SKU match (Tier 1), Item 2 = fuzzy description " + "match (Tier 3, opens picker), Item 3 = no-match sanity check.", }); console.log(` ✓ quote id=${quote.id}`); // 3 quote items designed to exercise the matcher const ITEMS = [ { description: "Makita angle grinder 125mm 840W", sku: "MAK-AG-125-9558", note: "Exact SKU match → expect Tier 1 auto-fill + auto-save", }, { description: "angle grinder 125mm", sku: "", note: "Fuzzy Tier 3 — expect picker with 4 candidates (Makita/Bosch/DeWalt/Generic-disc)", }, { description: "circular saw blade 184mm", sku: "", note: "No-match sanity — expect toast 'no catalogue row matched'", }, ]; console.log(`\n=== Creating 3 quote_items ===`); const itemIds: string[] = []; for (const it of ITEMS) { const created = await pb.collection("quote_items").create({ quote: quote.id, quote_number: QUOTE_NUMBER, description: it.description, sku: it.sku, quantity: 1, cost_price: 0, selling_price: 0, markup: 0, pricing_source: "manual", is_processed: false, user_edited: false, }); itemIds.push(created.id); console.log(` ✓ ${it.sku || "(no sku)"} — "${it.description}"`); console.log(` id=${created.id}`); console.log(` ${it.note}`); } console.log("\n=== DONE ==="); console.log(`Quote URL: http://localhost:4000/quotes/${quote.id}/process`); console.log(`Quote #: ${QUOTE_NUMBER}`); console.log(`Grinder ids: ${grinderIds.join(", ")}`); console.log(`Item ids: ${itemIds.join(", ")}`); console.log(`\n=== CLEANUP ===`); console.log(`Delete the 5 grinders (filter by brand=TEST_GRINDER):`); console.log(` pb.collection("price_list").getFullList({ filter: 'brand="TEST_GRINDER"' })`); console.log(` → for each, pb.collection("price_list").delete(id)`); console.log(`Delete the test quote (cascades to items):`); console.log(` pb.collection("quotes").delete("${quote.id}")`); } main().catch((e) => { console.error(e); process.exit(1); });