WFDASH-WEBAPP/scripts/dev/seed-test-grinders.ts

316 lines
9.3 KiB
TypeScript
Raw Permalink Normal View History

/* 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, unknown>;
}): 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);
});