WFDASH-WEBAPP/scripts/smoke/sku-matcher.ts
SJCoetz 9cdcb3b096 feat: implement sku-based catalogue pricing matching workflow
This commit adds the full end-to-end SKU pricing match flow for quote items:
- Add new QuoteItem and PriceListRow fields for SKU and pricing metadata
- Add sku_normalised to price_list for robust format-agnostic SKU matching
- Refactor price list search to use modular search builder and filter helpers
- Add database migrations, backfill scripts, and new package commands
- Update catalogue search UI and fix quote list accidental click behavior
- Add comprehensive unit, smoke, and end-to-end test suites
- Update documentation for the new price search and extraction logic
2026-06-30 21:20:53 +02:00

88 lines
3.6 KiB
TypeScript

/* eslint-disable */
/**
* Smoke test — exercise the full tiered SKU + description matcher
* against live PB.
*
* Run with:
* env PB_ADMIN_EMAIL=… PB_ADMIN_PASSWORD=… NEXT_PUBLIC_POCKETBASE_URL=…
* npx tsx scripts/smoke/sku-matcher.ts
*/
import {
findPriceListBySku,
findFirstPriceListBySku,
classifyMatch,
explainMatch,
matchTierLabel,
normaliseSku,
escapeForPbString,
} from "../../src/lib/server/price-list-match";
import PocketBase from "pocketbase";
async function main() {
const URL = process.env.NEXT_PUBLIC_POCKETBASE_URL;
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
if (!URL || !ADMIN_EMAIL || !ADMIN_PASSWORD) {
throw new Error("Set PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, NEXT_PUBLIC_POCKETBASE_URL.");
}
const pb = new PocketBase(URL);
await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
async function tryMatch(label: string, sku: string, opts: { tier3?: boolean } = {}) {
const matches = await findPriceListBySku(
sku,
{ enableTier3: opts.tier3 ?? true },
pb as unknown as PocketBase,
);
if (matches.length === 0) {
console.log(` ${label.padEnd(50)} (input: "${sku}") → no match`);
return;
}
const top = matches[0];
const tier = classifyMatch(sku, top);
const why = explainMatch(sku, top);
console.log(` ${label.padEnd(50)} (input: "${sku}")`);
console.log(` tier ${tier} ${matchTierLabel(tier)} — "${why}"`);
console.log(` matched: sku="${top.sku}" desc="${top.description}" cost=${top.cost_price} sell=${top.selling_price} (${top.supplier})`);
if (matches.length > 1) {
console.log(` ${matches.length - 1} more candidates (top-tier only: full list would be Tier-sorted by picker UI)`);
}
}
console.log("=== Tiered matcher smoke test ===");
console.log("\n--- Tier 1: exact equality ---");
await tryMatch("exact, as stored", "JEITD-06");
await tryMatch("case insensitive", "jeitd-06");
await tryMatch("without dashes", "JEITD06", { tier3: false });
console.log("\n--- Tier 2: SKU substring fallback ---");
await tryMatch("partial prefix", "JEITD", { tier3: false });
await tryMatch("brand prefix in SKU", "TS-DRL", { tier3: false });
console.log("\n--- Tier 3: description / brand fuzzy (Tier 3 enabled) ---");
await tryMatch("brand-only search", "JEISA");
await tryMatch("category-ish fuzzy", "TURBO");
await tryMatch("country-ish fuzzy", "WESTSCOTT");
console.log("\n--- Tier-3 disabled (bulk auto-fill rejects fuzzy) ---");
await tryMatch("no fuzzy — empty", "bosch grinder", { tier3: false });
await tryMatch("no fuzzy — empty", "m20 bolt", { tier3: false });
console.log("\n--- Tier 3 ENABLED (picker surfaces them) ---");
await tryMatch("with fuzzy — candidates", "bosch grinder");
await tryMatch("with fuzzy — candidates", "m20 bolt");
console.log("\n--- Sanity: pure helpers ---");
console.log(` normaliseSku("jeitd-06") = "${normaliseSku("jeitd-06")}"`);
console.log(` escapeForPbString('Acme "Pro" line') = "${escapeForPbString('Acme "Pro" line')}"`);
console.log(` classifyMatch("JEITD-06", { sku: "JEITD-06", sku_normalised: "JEITD06" }) = ${classifyMatch("JEITD-06", { sku: "JEITD-06", sku_normalised: "JEITD06" })}`);
console.log(` classifyMatch("JEITD", { sku: "JEITD-D11", sku_normalised: "JEITDD11" }) = ${classifyMatch("JEITD", { sku: "JEITD-D11", sku_normalised: "JEITDD11" })}`);
await tryMatch("empty string skipped", "");
await tryMatch("garbage no match (tier3 ON)", "ZZZGARBAGE-XYZ");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});