Add full sprint pricing workflow and supporting infrastructure: - New sidebar navigation entry for the sprint dashboard - PocketBase category collection schema and migration script - Server API routes for sprint item claims, attachments, counts, and file proxying - Dev identity utilities and UI switcher for multi-user testing - Reusable FilterTabs component replacing ItemStatusTabs - Updated TypeScript types for sprint-related entities Also include: - Fix PocketBase file URL method casing (getUrl → getURL) across codebase - Add development utility, smoke test, and helper scripts - Update package.json with new sprint migration command
78 lines
No EOL
3.1 KiB
TypeScript
78 lines
No EOL
3.1 KiB
TypeScript
/* eslint-disable */
|
|
/**
|
|
* Smoke test — exercise the new /match endpoint end-to-end against
|
|
* the live PB + n8n workflow.
|
|
*
|
|
* Run with:
|
|
* env PB_USER_EMAIL=… PB_USER_PASSWORD=… NEXT_PUBLIC_POCKETBASE_URL=…
|
|
* npx tsx scripts/smoke/match-endpoint.ts
|
|
*
|
|
* The script:
|
|
* 1. Authenticates as a PB `users` (not superuser) account so the
|
|
* route's `assertBrowserSession` passes.
|
|
* 2. POSTs { sku, description } bodies to /api/v1/quotes/<id>/items/<itemId>/match
|
|
* using a known item id from your live PB.
|
|
* 3. Prints the candidates (sku, tier, why, cost).
|
|
*
|
|
* Replace QUOTE_ID and ITEM_ID with real ids from your PB before
|
|
* running. The endpoint exists at /api/v1/quotes/[id]/items/[itemId]/match
|
|
* — it requires the Next.js dev server to be running (or a built
|
|
* server).
|
|
*/
|
|
import PocketBase from "pocketbase";
|
|
|
|
async function main() {
|
|
const URL = process.env.NEXT_PUBLIC_POCKETBASE_URL;
|
|
const USER_EMAIL = process.env.PB_USER_EMAIL;
|
|
const USER_PASSWORD = process.env.PB_USER_PASSWORD;
|
|
const QUOTE_ID = process.env.QUOTE_ID;
|
|
const ITEM_ID = process.env.ITEM_ID;
|
|
const DASHBOARD_URL = process.env.DASHBOARD_URL || "http://127.0.0.1:3000";
|
|
if (!URL || !USER_EMAIL || !USER_PASSWORD || !QUOTE_ID || !ITEM_ID) {
|
|
throw new Error(
|
|
"Set NEXT_PUBLIC_POCKETBASE_URL, PB_USER_EMAIL, PB_USER_PASSWORD, QUOTE_ID, ITEM_ID."
|
|
);
|
|
}
|
|
|
|
// Auth as a regular PB user.
|
|
const pb = new PocketBase(URL);
|
|
await pb.collection("users").authWithPassword(USER_EMAIL, USER_PASSWORD);
|
|
const token = pb.authStore.token;
|
|
if (!token) throw new Error("PB auth succeeded but no token returned.");
|
|
console.log(`Authed as ${USER_EMAIL} (token length ${token.length})`);
|
|
|
|
async function tryMatch(label: string, body: Record<string, string>) {
|
|
const url = `${DASHBOARD_URL.replace(/\/$/, "")}/api/v1/quotes/${QUOTE_ID}/items/${ITEM_ID}/match`;
|
|
console.log(`\n ${label}`);
|
|
console.log(` body: ${JSON.stringify(body)}`);
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", Authorization: token },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
console.log(` HTTP ${res.status}: ${data.error || "(no error message)"}`);
|
|
return;
|
|
}
|
|
const candidates = Array.isArray(data.candidates) ? data.candidates : [];
|
|
console.log(` ${candidates.length} candidate(s)`);
|
|
for (const c of candidates) {
|
|
console.log(` tier ${c.tier} ${c.sku} R${c.cost_price} "${c.why}"`);
|
|
}
|
|
}
|
|
|
|
console.log("=== /match endpoint smoke test ===");
|
|
await tryMatch("Tier 1 — exact SKU", { sku: "JEITD-06" });
|
|
await tryMatch("Tier 2 — SKU substring", { sku: "JEITD" });
|
|
await tryMatch("Tier 3 — fuzzy description", { description: "TURBO" });
|
|
await tryMatch("Tier 3 — fuzzy 'twist'", { description: "twist" });
|
|
await tryMatch("SKU + description combo", { sku: "JEITD", description: "turbo" });
|
|
await tryMatch("Both empty (400 expected)", { sku: "", description: "" });
|
|
await tryMatch("Garbage (no match expected)", { sku: "ZZZGARBAGE", description: "" });
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
}); |