feat(orders): add delivery-note repeat-order match fields
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>
This commit is contained in:
parent
3cf2837c3f
commit
e0d1356da5
2 changed files with 144 additions and 0 deletions
|
|
@ -18,6 +18,7 @@
|
|||
"pb:migrate:orders-rep": "npx tsx scripts/migrations/2026-06-26-add-orders-rep.ts",
|
||||
"pb:migrate:drop-orders-status": "npx tsx scripts/migrations/2026-06-27-drop-orders-status-and-wipe.ts",
|
||||
"pb:migrate:add-order-quote-match": "npx tsx scripts/migrations/2026-06-27-add-order-quote-match-fields.ts",
|
||||
"pb:migrate:add-order-delivery-note-match": "npx tsx scripts/migrations/2026-08-29-add-order-delivery-note-match-fields.ts",
|
||||
"pb:migrate:add-purchase-order-pdfs": "npx tsx scripts/migrations/2026-06-28-add-purchase-order-pdfs.ts",
|
||||
"pb:migrate:add-companies-coordinates": "npx tsx scripts/migrations/2026-06-28-add-companies-coordinates.ts",
|
||||
"pb:migrate:convert-customer-fields": "npx tsx scripts/migrations/2026-06-28-convert-customer-fields.ts",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
/* 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);
|
||||
});
|
||||
Loading…
Reference in a new issue