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.
60 lines
2 KiB
TypeScript
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);
|
|
});
|