WFDASH-WEBAPP/scripts/migrations/2026-09-09-fix-quote-attachment-fields-maxselect.ts

80 lines
2.6 KiB
TypeScript
Raw Permalink Normal View History

feat(quotes): split attachments into internal vs outbound with operator upload UI Splits the quote attachment model along the seam that actually matters: 'operator-only references' (drawings the operator consulted, supplier emails they filed away) vs 'customer-facing files' (supplier quotes, drawings, NDAs that need to ride along on the email). Schema (PB): new `internal_attachments` + `outbound_attachments` fields on the `quotes` collection, maxSelect 10 each. Existing `attachments` field kept for back-compat but is no longer the source of truth for outbound emails. API: - GET/POST/DELETE /api/v1/quotes/[id]/internal-attachments[/{filename}] - GET/POST/DELETE /api/v1/quotes/[id]/outbound-attachments[/{filename}] - POST /api/v1/quotes/[id]/attachments/upload kept for legacy `attachments` MCP: `quotes.send_email` description now correctly states `selectedAttachmentIds` are PB file tokens from `outbound_attachments` (supplier quotes, drawings, NDAs), not IDs of the legacy `attachments` collection. Frontend: `ProcessOutboundAttachmentsCard` component lets the operator upload/remove customer-facing files during the process page; approval + process pages now read the new fields. Outbound Send workflow (deployed as `PIEAkPYYYZLGl6Pj`): - Old workflow (id `WF_Email_Send_SMTP`) and stale repo file (`WFDASH — Quote Outbound Send.json`) replaced. - New revision intersects `quotes.outbound_attachments` with `email_outbound_attachment_ids`, downloads each from `/api/files/quotes/<id>/<token>`, and emits `options.attachments: [{name, content, type}]` consumed by `Send Email (SMTP)` (PO Send pattern). - Failure path flipped from `send_failed` to `mail_failed` so `retryQuoteEmail` can find failed sends (it only accepts the latter). Previously failed sends were stranded. Email Monitor (v5) workflow fixes: - Reply-branch auth: any authenticated request now passes through; pre-fix bug rejected branches that the IMAP trigger hadn't seen yet. - Reply detection: bulletproof subject regex (/Q-?\d{4}-?\d{2}-?\d{3}/) accepts both legacy `Q-2026-09-246` and current `Q202609246` formats; canonicalised digits-only for PB `~` substring match. Delivery notes: new pricing endpoint at /api/v1/delivery-notes/items/[id]/pricing + service-layer methods. Snapshots kept for forensics: PRE-FIX-REPLY-{BRANCH-AUTH,DETECTION}.json and push-payload.baseline.json are the previously-deployed workflow shapes, retained so we can diff/rollback if regression hits. Verified: - PUT workflow PIEAkPYYYZLGl6Pj -> 200, active, 25 nodes - quotes-service.test.ts: 117 pass (4 pre-existing failures unchanged) - mcp/route.test.ts: 118 pass (2 pre-existing failures unchanged) - node --check on new Code node JS passes Docs: docs/quotes-outbound-attachments-deploy.md covers the deploy procedure; docs/api-mcp-layer.md description sync.
2026-09-09 11:43:56 +00:00
/* eslint-disable */
/**
* Patch: set maxSelect=10 on the new `outbound_attachments` and
* `internal_attachments` file fields on `quotes`. The initial
* migration nested the options under `options: {...}` which PB stored
* as maxSelect=0 (= "unlimited" for files). File fields need the
* options flat at the top level of the field object, like the
* existing `quote_attachments.file` and friends.
*
* Idempotent — runs to no-op once maxSelect is 10.
*
* Run with:
* npx tsx scripts/migrations/2026-09-09-fix-quote-attachment-fields-maxselect.ts
*/
import PocketBase from "pocketbase";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
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 {
/* not present */
}
}
loadEnvFile(".env");
loadEnvFile(".env.local");
const FIELDS = ["outbound_attachments", "internal_attachments"] as const;
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(
"Set NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD.",
);
}
const pb = new PocketBase(url);
await pb.collection("_superusers").authWithPassword(email, password);
console.log(`[quote-attachment-fields-maxselect] Authenticated against ${url}`);
const quotes = await pb.collections.getOne("quotes");
const fields = (quotes.fields as Array<Record<string, unknown> & { name: string }>).map(
(f) =>
FIELDS.includes((f.name as (typeof FIELDS)[number]))
? { ...f, maxSelect: 10, mimeTypes: [] }
: f,
);
// PB validates that the update payload includes collection rules + the same
// shape it returned. Send the unchanged rules/options/indexes through.
await pb.collections.update(quotes.id, {
fields,
indexes: quotes.indexes,
listRule: quotes.listRule,
viewRule: quotes.viewRule,
createRule: quotes.createRule,
updateRule: quotes.updateRule,
deleteRule: quotes.deleteRule,
});
console.log(
`[quote-attachment-fields-maxselect] Patched ${FIELDS.join(", ")} to maxSelect=10`,
);
}
main().catch((err) => {
console.error("[quote-attachment-fields-maxselect] failed:", err);
process.exit(1);
});