132 lines
5 KiB
TypeScript
132 lines
5 KiB
TypeScript
|
|
/* eslint-disable */
|
||
|
|
/**
|
||
|
|
* Smoke test — exercise the re-price flow end-to-end.
|
||
|
|
*
|
||
|
|
* 1. Pick an existing quote with at least one quote_item.
|
||
|
|
* 2. Snapshot the current state of N items.
|
||
|
|
* 3. Set SKU on each item to a real catalogue row.
|
||
|
|
* 4. Run the same matcher the endpoint uses (findFirstPriceListBySku),
|
||
|
|
* emulate the patch the endpoint would write.
|
||
|
|
* 5. Restore the original values (so we don't leave the live DB
|
||
|
|
* in a half-baked state for the test).
|
||
|
|
*
|
||
|
|
* Run with:
|
||
|
|
* env PB_ADMIN_EMAIL=… PB_ADMIN_PASSWORD=… NEXT_PUBLIC_POCKETBASE_URL=…
|
||
|
|
* npx tsx scripts/smoke/re-price.ts
|
||
|
|
*/
|
||
|
|
import {
|
||
|
|
findFirstPriceListBySku,
|
||
|
|
type PriceListMatch,
|
||
|
|
} from "../../src/lib/server/price-list-match";
|
||
|
|
import PocketBase from "pocketbase";
|
||
|
|
|
||
|
|
type QuoteItemSnapshot = {
|
||
|
|
id: string;
|
||
|
|
cost_price?: number;
|
||
|
|
selling_price?: number;
|
||
|
|
description?: string;
|
||
|
|
supplier?: string;
|
||
|
|
sku?: string;
|
||
|
|
pricing_source?: string;
|
||
|
|
matched_price_list_id?: string;
|
||
|
|
pricing_notes?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
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);
|
||
|
|
|
||
|
|
// 1. Find a quote with at least 2 items we can play with.
|
||
|
|
const candidates = await pb.collection("quote_items").getList(1, 30, {
|
||
|
|
sort: "+created",
|
||
|
|
});
|
||
|
|
if (candidates.items.length < 2) {
|
||
|
|
throw new Error("Need ≥2 quote items to run this smoke test.");
|
||
|
|
}
|
||
|
|
const items = candidates.items.slice(0, 3);
|
||
|
|
const quoteId = items[0].quote;
|
||
|
|
|
||
|
|
// 2. Snapshot the originals so we can restore at the end.
|
||
|
|
const snapshots: QuoteItemSnapshot[] = items.map((it) => ({
|
||
|
|
id: it.id,
|
||
|
|
cost_price: it.cost_price ?? 0,
|
||
|
|
selling_price: it.selling_price ?? 0,
|
||
|
|
description: it.description ?? "",
|
||
|
|
supplier: it.supplier ?? "",
|
||
|
|
sku: it.sku ?? "",
|
||
|
|
pricing_source: it.pricing_source ?? "",
|
||
|
|
matched_price_list_id: it.matched_price_list_id ?? "",
|
||
|
|
pricing_notes: it.pricing_notes ?? "",
|
||
|
|
}));
|
||
|
|
console.log(`[re-price smoke] quote=${quoteId}, picked items: ${items.map((it) => it.id).join(", ")}`);
|
||
|
|
|
||
|
|
// 3. SKUs to inject. Two real catalogue SKUs + one nonsense.
|
||
|
|
const testCases: Array<{ id: string; sku: string; label: string }> = [
|
||
|
|
{ id: items[0].id, sku: "JEITD-06", label: "exact (twist drill 6mm)" },
|
||
|
|
{ id: items[1].id, sku: "jeitd-08", label: "case-insensitive (twist drill 8mm)" },
|
||
|
|
{ id: items[2].id, sku: "NOPE-NOPE-99", label: "garbage (expect no match)" },
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const tc of testCases) {
|
||
|
|
await pb.collection("quote_items").update(tc.id, { sku: tc.sku });
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. Run the matcher + emulate the patch the endpoint writes.
|
||
|
|
console.log("\n[re-price smoke] Running matcher over each item");
|
||
|
|
for (const tc of testCases) {
|
||
|
|
const match = await findFirstPriceListBySku(tc.sku, {}, pb as unknown as PocketBase);
|
||
|
|
if (!match) {
|
||
|
|
console.log(` ${tc.label.padEnd(38)} → no match (expected for "${tc.sku}")`);
|
||
|
|
await pb.collection("quote_items").update(tc.id, {
|
||
|
|
pricing_notes: `No match for SKU "${tc.sku}"`,
|
||
|
|
});
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const patch = {
|
||
|
|
cost_price: match.cost_price,
|
||
|
|
selling_price: match.selling_price,
|
||
|
|
description: (match.description ?? "").trim() || (snapshots.find((s) => s.id === tc.id)?.description ?? ""),
|
||
|
|
pricing_source: "catalogue",
|
||
|
|
matched_price_list_id: match.id,
|
||
|
|
pricing_notes: `Auto-matched from ${match.supplier} on ${new Date().toISOString().slice(0, 10)}`,
|
||
|
|
};
|
||
|
|
console.log(` ${tc.label.padEnd(38)} → matched ${match.sku} (${match.supplier})`);
|
||
|
|
console.log(` patch: cost=${patch.cost_price}, sell=${patch.selling_price}, pnotes="${patch.pricing_notes}"`);
|
||
|
|
await pb.collection("quote_items").update(tc.id, patch);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. Verify by re-reading.
|
||
|
|
console.log("\n[re-price smoke] Verifying patched state");
|
||
|
|
for (const tc of testCases) {
|
||
|
|
const it = await pb.collection("quote_items").getOne(tc.id);
|
||
|
|
console.log(` ${tc.label.padEnd(38)} sku="${it.sku}" cost=${it.cost_price} sell=${it.selling_price} source="${it.pricing_source}" mid="${it.matched_price_list_id}"`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. Restore — don't leave the live DB half-set.
|
||
|
|
console.log("\n[re-price smoke] Restoring snapshots");
|
||
|
|
for (const snap of snapshots) {
|
||
|
|
await pb.collection("quote_items").update(snap.id, {
|
||
|
|
cost_price: snap.cost_price,
|
||
|
|
selling_price: snap.selling_price,
|
||
|
|
description: snap.description,
|
||
|
|
supplier: snap.supplier || null,
|
||
|
|
sku: snap.sku,
|
||
|
|
pricing_source: snap.pricing_source,
|
||
|
|
matched_price_list_id: snap.matched_price_list_id,
|
||
|
|
pricing_notes: snap.pricing_notes,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
console.log("[re-price smoke] Done. DB restored.");
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((e) => {
|
||
|
|
console.error(e);
|
||
|
|
process.exit(1);
|
||
|
|
});
|