// scripts/migrations/2026-08-07-stock-purchase-items-catalogue-fields.ts // // One-shot schema migration: add three catalogue fields to // `stock_purchase_items`: // // - sku (text, optional) — the WFDASH catalogue SKU // that this PO line corresponds to. // May differ from the supplier's // sku when the supplier relabels. // - unit (text, optional) — supplier-side unit string // (e.g. "L", "m", "kg"). Used by // the agent when the supplier // invoice disagrees with the PO. // - supplier_sku (text, optional) — the SKU printed on the // supplier's catalogue / invoice. // Lets Hermes reconcile the // supplier invoice against the PO // line without a manual lookup. // // Why this exists: the operator's external agent (Hermes) // reconciles supplier invoices against stock POs. Today, when // the supplier renames a SKU on the invoice, Hermes has no field // to record the supplier's sku on the PO line. The three fields // are the minimal additive set for that workflow. // // The MCP `stock_purchase_items.update` tool's whitelist references // these three fields. The whitelist also references `cost_price`, // `description`, `notes`, and `quantity` — all of which already // exist on the collection, so no migration is needed for them. // // Idempotent: re-running is a no-op when every field is already // present. Partial states are surfaced clearly so the operator // can re-run to add the missing field(s) one at a time. import { getAdminPocketBase } from "@/lib/server/pocketbase-admin"; const COLLECTION_NAME = "stock_purchase_items"; const NEW_FIELDS = [ { name: "sku", type: "text" }, { name: "unit", type: "text" }, { name: "supplier_sku", type: "text" }, ] as const; async function main() { const pb = await getAdminPocketBase(); const collection = await pb.collections.getOne(COLLECTION_NAME); const existing = (collection.fields ?? []) as Array<{ name: string }>; const existingNames = new Set(existing.map((f) => f.name)); const missing = NEW_FIELDS.filter((f) => !existingNames.has(f.name)); if (missing.length === 0) { console.log( `[stock-purchase-items-catalogue-fields] all fields already present on ${COLLECTION_NAME}: ${NEW_FIELDS.map((f) => f.name).join(", ")}`, ); return; } // Build the new field definitions. PB text fields need // `maxSize` — the collection-wide convention here is 500 chars // (matches the existing `description` and `notes` fields on the // same collection). const newFieldDefs = missing.map((f) => ({ name: f.name, type: "text", required: false, presentable: false, protected: false, maxSize: 500, pattern: "", })); await pb.collections.update(collection.id, { fields: [...existing, ...newFieldDefs], }); console.log( `[stock-purchase-items-catalogue-fields] added ${missing.length} field(s) to ${COLLECTION_NAME}: ${missing.map((f) => f.name).join(", ")}`, ); if (NEW_FIELDS.some((f) => !missing.find((m) => m.name === f.name))) { const already = NEW_FIELDS.filter( (f) => !missing.find((m) => m.name === f.name), ).map((f) => f.name); console.log( `[stock-purchase-items-catalogue-fields] skipped (already present): ${already.join(", ")}`, ); } } main().catch((err) => { console.error( "[stock-purchase-items-catalogue-fields] failed:", err, ); process.exit(1); });