WFDASH-WEBAPP/scripts/migrations/2026-08-07-stock-purchase-items-catalogue-fields.ts
SJCoetz c4125ee40a feat: add split-invoice stock PO workflow and supporting tooling
Ship full split-invoice stock purchase order workflow:
- Add 5 new MCP tools for per-line stock purchase item management and catalogue updates
- Add idempotent database migration adding sku, unit, and supplier_sku fields to stock_purchase_items
- Implement service layers for all new operations, including auto-closing the parent PO when all lines are fully collected
- Add complete test coverage for new services and API routes
- Update all relevant documentation including MCP API specs, agent instructions, and feature request logs
2026-08-07 00:53:29 +02:00

96 lines
3.6 KiB
TypeScript

// 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);
});