From e0d1356da51c4dcb9e1466b52bfa6998232a36db Mon Sep 17 00:00:00 2001 From: Dave Date: Sun, 30 Aug 2026 12:57:16 +0200 Subject: [PATCH] feat(orders): add delivery-note repeat-order match fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package.json | 1 + ...29-add-order-delivery-note-match-fields.ts | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 scripts/migrations/2026-08-29-add-order-delivery-note-match-fields.ts diff --git a/package.json b/package.json index 1053ee4..a66ca9a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/migrations/2026-08-29-add-order-delivery-note-match-fields.ts b/scripts/migrations/2026-08-29-add-order-delivery-note-match-fields.ts new file mode 100644 index 0000000..bb67d3b --- /dev/null +++ b/scripts/migrations/2026-08-29-add-order-delivery-note-match-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 & { 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); +});