// scripts/migrations/2026-07-16-add-stock-purchase-send-statuses.ts // // Extends the `stock_purchases.status` select enum to include the // two intermediate states the new outbound-email flow needs: // - "sending" — operator hit "Send to supplier", n8n is processing // - "mail_failed" — SMTP attempt failed, retryable // // These mirror the `purchase_orders` status enum so the same n8n // workflow can drive both flows (the parameterized version reads // `status` from the body / record regardless of collection name). // // Idempotent: re-running is a no-op when the values are already // present. /* eslint-disable */ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import PocketBase from "pocketbase"; 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 { // .env.local doesn't exist — fall through. } } loadEnvFile(".env.local"); const COLLECTION_NAME = "stock_purchases"; const STATUS_FIELD = "status"; const EXISTING_VALUES = ["draft", "sent", "received", "cancelled"]; const NEW_VALUES = ["sending", "mail_failed"]; 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( "NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD must be set.", ); } const pb = new PocketBase(url); await pb.collection("_superusers").authWithPassword(email, password); const collection = await pb.collections.getOne(COLLECTION_NAME); const fields = (collection.fields ?? []) as Array<{ name: string; values?: string[] }>; const statusField = fields.find((f) => f.name === STATUS_FIELD); if (!statusField || statusField.type !== "select") { throw new Error( `Expected ${COLLECTION_NAME}.${STATUS_FIELD} to be a select field. Got ${ statusField?.type || "missing" }.`, ); } const currentValues = statusField.values ?? []; const merged = Array.from(new Set([...currentValues, ...NEW_VALUES])); if (currentValues.includes(NEW_VALUES[0]) && currentValues.includes(NEW_VALUES[1])) { console.log( `[stock-purchase-send-statuses] both "sending" and "mail_failed" already present on ${COLLECTION_NAME}.${STATUS_FIELD}`, ); return; } // Replace the field with the same config + extended values. PB // requires us to re-submit the field type / config block when // updating values. const updatedFields = fields.map((f) => f.name === STATUS_FIELD ? { ...f, values: merged, // `maxSelect` isn't relevant for a single-value status but // we set it explicitly to 1 to mirror the customer-PO // status field. PB preserves the existing value if // omitted, but the type check is stricter on the way in. maxSelect: 1, } : f ); await pb.collections.update(collection.id, { fields: updatedFields }); console.log( `[stock-purchase-send-statuses] extended ${COLLECTION_NAME}.${STATUS_FIELD} values: ${merged.join(", ")}`, ); } main().catch((err) => { console.error("[stock-purchase-send-statuses] failed:", err); process.exit(1); });