WFDASH-WEBAPP/scripts/migrations/2026-06-30-add-pricelist-notes.ts
SJCoetz 81c9f64158 feat(pricelist): notes field for AI instructions + file_name on upload
Operators can now type per-upload instructions for the AI extraction
step in the Pricelist upload form. The notes go into a new
`raw_price_list.notes` field that the n8n workflow reads and
injects into the LLM user message as per-upload context.

Common uses:
  - "Brand is JEI" → AI stamps every item with brand="JEI"
  - "Apply 30% markup" → Code node regexes this out for selling_price
  - "All prices in ZAR excl. VAT" → AI uses ZAR
  - "Page 4 has discount codes" → AI skips those rows

Also auto-populates `raw_price_list.file_name` (the original
browser-side filename) on upload — PB slugifies the upload to a
token stored inside supplier_file, so without this field the
operator saw the slugged version. Existing rows backfilled by the
admin script.

The `log_summary` field is now reserved for the AI's write-back
summary (set by the n8n workflow after extraction). It was
previously operator-editable on the form, which was wrong —
overwriting it clobbered the AI's output.

New migration: scripts/migrations/2026-06-30-add-pricelist-notes.ts
(idempotent, only adds `notes` if missing — `file_name` was
added manually via the PB admin UI before this commit).

doc/pricelist-extraction.md updated with the n8n LLM user message
shape, the markup regex, and the notes field's role in the pipeline.
2026-06-30 11:09:27 +02:00

60 lines
2 KiB
TypeScript

/**
* Migration — add the `notes` field to `raw_price_list`.
*
* The `file_name` field already exists in the live PB (added by the
* operator via the admin UI). This script is idempotent: it checks
* for the field first and only adds it if missing.
*
* The `notes` field carries free-text operator instructions for the AI
* extraction step — e.g. "Brand is JEI. Apply 30% markup. All prices
* in ZAR excl. VAT. POA for any missing values." The n8n workflow's
* LLM user message is built from this + the supplier name.
*
* Run with:
* npm run pb:migrate:pricelist-notes
*/
import PocketBase from "pocketbase";
const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090";
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
throw new Error("PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD are required.");
}
async function main() {
const pb = new PocketBase(PB_URL);
await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
const cols = await pb.collections.getFullList();
const raw = cols.find((c) => c.name === "raw_price_list");
if (!raw) {
throw new Error(
"raw_price_list collection not found. Run `npm run pb:migrate:raw-price-list` first.",
);
}
const hasNotes = raw.fields.some((f) => f.name === "notes");
if (hasNotes) {
console.log("raw_price_list.notes already exists, skipping.");
return;
}
// Append the notes field. PB requires us to send the full field
// list (not a partial patch), so we copy all existing fields and
// tack on the new one.
const updatedFields = [
...raw.fields,
{ name: "notes", type: "text", required: false, options: { max: 0 } },
];
await pb.collections.update(raw.id, { fields: updatedFields });
console.log("Added raw_price_list.notes");
console.log("Pricelist notes migration complete.");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});