84 lines
3 KiB
TypeScript
84 lines
3 KiB
TypeScript
|
|
/**
|
|||
|
|
* One-off clean-up when quote matching became AI-only (2026-10-24).
|
|||
|
|
*
|
|||
|
|
* The retired rule-based matcher left soft suggestions (`potential_quote`)
|
|||
|
|
* on unlinked orders — many wrong: one quote was suggested to 16 unrelated
|
|||
|
|
* orders because same company + customer scored 0.5 with no matching
|
|||
|
|
* lines. This clears every suggestion that did NOT come from the AI order
|
|||
|
|
* assistant (`quote_match_method` not starting with `ai_`), so the order
|
|||
|
|
* page shows "Match with AI" instead of a stale guess.
|
|||
|
|
*
|
|||
|
|
* Hard links (`orders.quote`) are never touched. AI suggestions stay.
|
|||
|
|
* Dry run unless `--apply`.
|
|||
|
|
*
|
|||
|
|
* npx tsx scripts/2026-10-24-clear-rule-quote-suggestions.ts [--apply]
|
|||
|
|
*/
|
|||
|
|
import "./_env";
|
|||
|
|
import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin";
|
|||
|
|
import { runWithAuditActor } from "../src/lib/server/audit-context";
|
|||
|
|
|
|||
|
|
const APPLY = process.argv.includes("--apply");
|
|||
|
|
const actor = { id: "claude-quote-suggestion-clear", name: "Claude (rule-based suggestion clean-up)" };
|
|||
|
|
|
|||
|
|
const FILTER = 'potential_quote != "" && quote = "" && quote_match_method !~ "ai_"';
|
|||
|
|
|
|||
|
|
interface Row {
|
|||
|
|
id: string;
|
|||
|
|
order_number?: string;
|
|||
|
|
potential_quote?: string;
|
|||
|
|
potential_quote_confidence?: number;
|
|||
|
|
quote_match_method?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
const pb = await getAdminPocketBase();
|
|||
|
|
pb.autoCancellation(false);
|
|||
|
|
|
|||
|
|
const rows = await pb.collection("orders").getFullList<Row>({
|
|||
|
|
filter: FILTER,
|
|||
|
|
fields: "id,order_number,potential_quote,potential_quote_confidence,quote_match_method",
|
|||
|
|
sort: "-created",
|
|||
|
|
batch: 500,
|
|||
|
|
});
|
|||
|
|
console.log(`${APPLY ? "APPLY" : "DRY "} ${rows.length} rule-based suggestion(s) on unlinked orders`);
|
|||
|
|
|
|||
|
|
const perQuote = new Map<string, number>();
|
|||
|
|
const perMethod = new Map<string, number>();
|
|||
|
|
for (const r of rows) {
|
|||
|
|
perQuote.set(r.potential_quote || "", (perQuote.get(r.potential_quote || "") ?? 0) + 1);
|
|||
|
|
perMethod.set(r.quote_match_method || "(none)", (perMethod.get(r.quote_match_method || "(none)") ?? 0) + 1);
|
|||
|
|
}
|
|||
|
|
console.log("\n── by method ──");
|
|||
|
|
for (const [m, n] of [...perMethod].sort((a, b) => b[1] - a[1])) console.log(` ${n} × ${m}`);
|
|||
|
|
console.log("\n── most-suggested quotes ──");
|
|||
|
|
for (const [q, n] of [...perQuote].sort((a, b) => b[1] - a[1]).slice(0, 10)) console.log(` ${n} × ${q}`);
|
|||
|
|
|
|||
|
|
if (!APPLY) {
|
|||
|
|
console.log("\nDry run — nothing was written. Re-run with --apply to clear them.");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let cleared = 0;
|
|||
|
|
let failed = 0;
|
|||
|
|
for (const r of rows) {
|
|||
|
|
try {
|
|||
|
|
await pb.collection("orders").update(r.id, {
|
|||
|
|
potential_quote: "",
|
|||
|
|
potential_quote_confidence: 0,
|
|||
|
|
quote_match_method: "",
|
|||
|
|
quote_matching_attempted: false,
|
|||
|
|
});
|
|||
|
|
cleared++;
|
|||
|
|
} catch (err) {
|
|||
|
|
failed++;
|
|||
|
|
console.error(`ERROR ${r.order_number || r.id}: ${err instanceof Error ? err.message : err}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
console.log(`\ncleared ${cleared}, failed ${failed}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
runWithAuditActor({ ...actor, source: "server", ignoreRequestUser: true }, main).catch((err) => {
|
|||
|
|
console.error(err);
|
|||
|
|
process.exit(1);
|
|||
|
|
});
|