Add complete stock purchase order workflow including: - Status tracking with sending/mail_failed states and UI labels - Server-side PDF rendering and document storage - File proxy API support for stock PO attachments - Send outcome webhook callback for email notifications Add driver app support: - Unified stop list API for both customer deliveries and stock pickups - Status update endpoint for collected quantities and problem reporting - Collection map page support for stock pickup locations Include matching PocketBase migrations, type definitions, and stock inventory fulfillment logic.
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
// 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);
|
|
});
|