WFDASH-WEBAPP/scripts/migrations/2026-07-17-add-stock-purchase-problem-fields.ts
SJCoetz d306b75bdb feat(stock-purchases,driver-app): add stock purchase workflow and driver pickup support
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.
2026-07-18 22:43:50 +02:00

114 lines
3.4 KiB
TypeScript

// scripts/migrations/2026-07-17-add-stock-purchase-problem-fields.ts
//
// Adds the operator-side problem-tracking fields to
// `stock_purchase_items`, mirroring the same fields on
// `order_items`:
//
// - problem_flag (bool) — driver reported an issue at the
// supplier (short-ship, wrong item,
// damaged, etc.)
// - problem_type (text) — structured category of the issue
// (mirrors `order_items.problem_type`)
// - problem_notes (text) — free-text detail
//
// These three fields are added in one migration so the v1
// driver-app PATCH can stamp them in a single PB write. The
// fields are all optional — most lines never have a problem.
//
// Idempotent: re-running is a no-op when the fields 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_purchase_items";
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 existing = (collection.fields ?? []) as Array<{ name: string }>;
const have = (n: string) => existing.some((f) => f.name === n);
const additions: Array<Record<string, unknown>> = [];
if (!have("problem_flag")) {
additions.push({
name: "problem_flag",
type: "bool",
required: false,
presentable: false,
});
}
if (!have("problem_type")) {
additions.push({
name: "problem_type",
type: "text",
required: false,
presentable: false,
// No PB-side max — service layer clamps. Matches the
// shape of `order_items.problem_type` (free-text in
// practice, but operator types into a small set of
// known values).
max: 0,
});
}
if (!have("problem_notes")) {
additions.push({
name: "problem_notes",
type: "text",
required: false,
presentable: false,
max: 0,
});
}
if (additions.length === 0) {
console.log(
`[stock-purchase-problem-fields] all three fields already present on ${COLLECTION_NAME}`,
);
return;
}
await pb.collections.update(collection.id, {
fields: [...existing, ...additions],
});
console.log(
`[stock-purchase-problem-fields] added ${additions.length} field(s) to ${COLLECTION_NAME}: ${additions.map((a) => a.name).join(", ")}`,
);
}
main().catch((err) => {
console.error("[stock-purchase-problem-fields] failed:", err);
process.exit(1);
});