Five new fields on the `orders` collection: - potential_delivery_note (relation → delivery_notes, single) — the DN whose items exactly matched this order's items - potential_delivery_note_number (text) — denormalized copy of the matched DN's delivery_note_number, written alongside the relation so the orders list can render the number without a second `expand` (PB issues a separate IN query per expanded relation — orders-service.ts:253 documents why this is avoided here) - delivery_note_match_confidence (number) — always 1 when set, kept for shape parity with quote_match_confidence - delivery_note_match_method (text) — 'exact_items' or 'no_match', text not select for the same forward-compatibility reason quote_match_method is text - delivery_note_matching_attempted (bool) — audit flag Idempotent — each step detects its own 'already done' state. Verified by re-running after the first apply. Plan-staleness fix: PB stores relation options FLAT at the top level of the field object (collectionId / cascadeDelete / minSelect / maxSelect), not nested under an `options` key. The plan's snippet had the nested form and PB rejected it with 'fields.N.collectionId Cannot be blank.' Verified the correct shape against the live orders.potential_quote field before fixing. Co-Authored-By: Claude <noreply@anthropic.com>
143 lines
4.6 KiB
TypeScript
143 lines
4.6 KiB
TypeScript
/* eslint-disable */
|
|
/**
|
|
* Migration: add delivery-note repeat-order detection fields to `orders`.
|
|
*
|
|
* Adds five fields:
|
|
*
|
|
* 1. `potential_delivery_note` (relation → delivery_notes, single)
|
|
* The DN whose items exactly matched this order's items.
|
|
*
|
|
* 2. `potential_delivery_note_number` (text)
|
|
* Denormalized copy of the matched DN's `delivery_note_number`.
|
|
* Written alongside (1) so the orders list can render the number
|
|
* without a second `expand` — see the comment at
|
|
* orders-service.ts:253 for why that expand cost is avoided here
|
|
* (PB issues a separate IN query per expanded relation).
|
|
* `delivery_note_number` is immutable once assigned, so copying
|
|
* it carries no staleness risk.
|
|
*
|
|
* 3. `delivery_note_match_confidence` (number)
|
|
* Always 1 when set — the match is exact-or-nothing, not scored.
|
|
* Kept for shape parity with quote_match_confidence and so a
|
|
* future fuzzy extension doesn't need a second migration.
|
|
*
|
|
* 4. `delivery_note_match_method` (text)
|
|
* "exact_items" or "no_match". Text, not select, for the same
|
|
* forward-compatibility reason quote_match_method is text.
|
|
*
|
|
* 5. `delivery_note_matching_attempted` (bool)
|
|
* Audit flag mirroring quote_matching_attempted.
|
|
*
|
|
* Idempotent — safe to re-run. Each step detects its own "already
|
|
* done" state.
|
|
*
|
|
* Run with:
|
|
* npx tsx scripts/migrations/2026-08-29-add-order-delivery-note-match-fields.ts
|
|
*/
|
|
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 {
|
|
// file not present — fine, rely on shell env
|
|
}
|
|
}
|
|
loadEnvFile(".env");
|
|
loadEnvFile(".env.local");
|
|
|
|
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(
|
|
"Set NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD (e.g. sourced from .env.local).",
|
|
);
|
|
}
|
|
|
|
const pb = new PocketBase(url);
|
|
await pb.collection("_superusers").authWithPassword(email, password);
|
|
console.log(`[order-delivery-note-match] Authenticated against ${url}`);
|
|
|
|
const deliveryNotes = await pb.collections.getOne("delivery_notes");
|
|
|
|
async function addFieldIfMissing(
|
|
fieldDef: Record<string, unknown> & { name: string },
|
|
) {
|
|
const orders = await pb.collections.getOne("orders");
|
|
if (orders.fields.some((f: { name: string }) => f.name === fieldDef.name)) {
|
|
console.log(`[order-delivery-note-match] orders.${fieldDef.name} already present — skipping.`);
|
|
return;
|
|
}
|
|
await pb.collections.update(orders.id, {
|
|
fields: [...orders.fields, fieldDef],
|
|
});
|
|
console.log(`[order-delivery-note-match] Added orders.${fieldDef.name}.`);
|
|
}
|
|
|
|
await addFieldIfMissing({
|
|
name: "potential_delivery_note",
|
|
type: "relation",
|
|
required: false,
|
|
presentable: false,
|
|
// PB stores relation options FLAT at the top level of the field
|
|
// object, not nested under `options`. Verified against the live
|
|
// schema by inspecting `orders.potential_quote`. The plan's
|
|
// snippet showed the nested form; PB rejects it with
|
|
// "fields.N.collectionId Cannot be blank."
|
|
collectionId: deliveryNotes.id,
|
|
cascadeDelete: false,
|
|
minSelect: 0,
|
|
maxSelect: 1,
|
|
});
|
|
|
|
await addFieldIfMissing({
|
|
name: "potential_delivery_note_number",
|
|
type: "text",
|
|
required: false,
|
|
presentable: false,
|
|
options: { max: 32 },
|
|
});
|
|
|
|
await addFieldIfMissing({
|
|
name: "delivery_note_match_confidence",
|
|
type: "number",
|
|
required: false,
|
|
presentable: false,
|
|
options: { min: 0, max: 1 },
|
|
});
|
|
|
|
await addFieldIfMissing({
|
|
name: "delivery_note_match_method",
|
|
type: "text",
|
|
required: false,
|
|
presentable: false,
|
|
options: { max: 32 },
|
|
});
|
|
|
|
await addFieldIfMissing({
|
|
name: "delivery_note_matching_attempted",
|
|
type: "bool",
|
|
required: false,
|
|
presentable: false,
|
|
options: {},
|
|
});
|
|
|
|
console.log("[order-delivery-note-match] Migration complete.");
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("[order-delivery-note-match] Migration failed:", err);
|
|
process.exit(1);
|
|
});
|