PocketBase returns 0 for unset number fields, which broke the original null-coalescing check in effectiveValidityDays, leading to PDFs showing "Valid until: today" for all new quotes created via email ingest. Changes include: - Update effectiveValidityDays to treat 0, negatives, null/undefined as falling back to 30-day default - Add test cases for 0 and negative validity_days inputs - Explicitly set validity_days to QUOTE_VALID_DAYS when creating new quotes - Add one-time idempotent migration script to backfill legacy invalid validity values - Refactor item intel popover patch logic to use PocketBase SDK for authenticated requests - Add n8n workflow for AI-powered item insight matching
153 lines
4.8 KiB
TypeScript
153 lines
4.8 KiB
TypeScript
/* eslint-disable */
|
|
/**
|
|
* One-off data repair: stamp `validity_days = 30` on every quote
|
|
* where the field is missing, null, or zero.
|
|
*
|
|
* ## Background
|
|
*
|
|
* PocketBase returns `0` (not `null`) for an un-set number field. The
|
|
* PDF formatter originally used `quote.validity_days ?? 30`, which
|
|
* does *not* treat 0 as nullish. Every quote created via the email
|
|
* ingest path (`createQuoteFromEmail`) before this fix shipped
|
|
* ended up with `validity_days = 0`, so the generated PDF said
|
|
* "Valid until: today".
|
|
*
|
|
* This script does a one-shot patch to bring every existing quote
|
|
* to the package default. Quotes where an operator had explicitly
|
|
* set a value (any positive number) are left untouched.
|
|
*
|
|
* ## Usage
|
|
*
|
|
* Dry run (default — safe to run anytime):
|
|
*
|
|
* npx tsx scripts/migrations/2026-08-09-backfill-quote-validity-days.ts
|
|
*
|
|
* Apply the repair:
|
|
*
|
|
* npx tsx scripts/migrations/2026-08-09-backfill-quote-validity-days.ts --apply
|
|
*
|
|
* ## Idempotency
|
|
*
|
|
* The filter `validity_days = null || validity_days <= 0` only matches
|
|
* quotes that need fixing. Re-running after a successful apply
|
|
* updates zero rows and exits cleanly.
|
|
*/
|
|
import PocketBase from "pocketbase";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
function loadEnvFile(name: string) {
|
|
try {
|
|
const raw = readFileSync(resolve(process.cwd(), name), "utf8");
|
|
for (const line of raw.split("\n")) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
const m = trimmed.match(/^([A-Z0-9_]+)\s*=\s*(.+)$/i);
|
|
if (m && !process.env[m[1]]) {
|
|
process.env[m[1]] = m[2].replace(/^["']|["']$/g, "").trim();
|
|
}
|
|
}
|
|
} catch {
|
|
// missing file — fine
|
|
}
|
|
}
|
|
loadEnvFile(".env");
|
|
loadEnvFile(".env.local");
|
|
|
|
const QUOTE_VALID_DAYS = 30;
|
|
const APPLY = process.argv.includes("--apply");
|
|
const PAGE_SIZE = 500;
|
|
|
|
async function main() {
|
|
const url = process.env.NEXT_PUBLIC_POCKETBASE_URL;
|
|
const email = process.env.PB_ADMIN_EMAIL;
|
|
const password = process.env.PB_ADMIN_PASSWORD;
|
|
if (!url || !email || !password) {
|
|
throw new Error(
|
|
"Missing NEXT_PUBLIC_POCKETBASE_URL / PB_ADMIN_EMAIL / PB_ADMIN_PASSWORD in env",
|
|
);
|
|
}
|
|
|
|
const pb = new PocketBase(url);
|
|
await pb.admins.authWithPassword(email, password);
|
|
|
|
// Match every quote that's missing / unset / zero / negative on
|
|
// validity_days. The shape mirrors `effectiveValidityDays`'s
|
|
// nullish handling so we patch exactly the rows the PDF code
|
|
// would currently fall back on.
|
|
const filter = 'validity_days = null || validity_days <= 0';
|
|
const all = (await pb.collection("quotes").getFullList({
|
|
filter,
|
|
fields: "id,quote_number,validity_days,quote_date",
|
|
sort: "quote_number",
|
|
})) as Array<{
|
|
id: string;
|
|
quote_number: string;
|
|
validity_days: number | null;
|
|
quote_date: string;
|
|
}>;
|
|
|
|
if (all.length === 0) {
|
|
console.log("No quotes with missing/0 validity_days — nothing to repair.");
|
|
return;
|
|
}
|
|
|
|
console.log(`Found ${all.length} quote(s) with missing or 0 validity_days.`);
|
|
console.log("");
|
|
console.log("First 20 rows that would be patched:");
|
|
for (const q of all.slice(0, 20)) {
|
|
console.log(
|
|
` ${q.quote_number.padEnd(16)} was=${String(q.validity_days).padStart(4)} → ${QUOTE_VALID_DAYS} (date=${q.quote_date || "—"})`,
|
|
);
|
|
}
|
|
if (all.length > 20) {
|
|
console.log(` ... and ${all.length - 20} more`);
|
|
}
|
|
console.log("");
|
|
|
|
if (!APPLY) {
|
|
console.log("DRY RUN — pass --apply to perform the repair.");
|
|
console.log("");
|
|
console.log("Re-run with: npx tsx scripts/migrations/2026-08-09-backfill-quote-validity-days.ts --apply");
|
|
return;
|
|
}
|
|
|
|
// PB doesn't expose a batch-update collection endpoint via the SDK
|
|
// for arbitrary fields. Update each row one-by-one. With 200 rows
|
|
// worst case this takes ~30 s.
|
|
console.log("Patching...");
|
|
let patched = 0;
|
|
let failed = 0;
|
|
for (const q of all) {
|
|
try {
|
|
await pb.collection("quotes").update(q.id, { validity_days: QUOTE_VALID_DAYS });
|
|
patched++;
|
|
} catch (err) {
|
|
failed++;
|
|
console.error(` ✗ ${q.quote_number} (${q.id}) failed:`, err);
|
|
}
|
|
}
|
|
console.log(` ✓ Patched ${patched} quote(s).`);
|
|
if (failed > 0) {
|
|
console.error(` ✗ ${failed} quote(s) failed.`);
|
|
}
|
|
|
|
// Re-query to confirm zero rows remain in the "needs-fixing" set.
|
|
const remaining = await pb.collection("quotes").getFullList({
|
|
filter,
|
|
fields: "id",
|
|
});
|
|
console.log("");
|
|
console.log(`Repair complete. Quotes still in the "needs fixing" set: ${remaining.length}`);
|
|
if (remaining.length > 0) {
|
|
console.error(
|
|
"WARNING: some quotes still have validity_days = 0/null — investigate before re-running.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|