# PO additional attachments **Goal:** Let the operator attach extra files (supplier quote, drawing, NDA, etc.) to a Purchase Order. The PO email to the supplier includes them alongside the PO PDF. The existing `documents` field (versioned PDFs) and the new `additional_attachments` field stay cleanly separate. ## Scope ### In Scope - Add an `additional_attachments` file field to the `purchase_orders` collection. Same shape as `documents` (file, multi, no PB cap, any mime type). - Migration: add the field via `scripts/migrations/2026-07-09-po-additional-attachments.ts`. Idempotent (PB's add-field migration is a no-op on re-run). - New server route: `POST /api/v1/purchase-orders/[id]/attachments/upload` — multipart, admin-auth, returns the updated `additional_attachments` array. - New server route: `DELETE /api/v1/purchase-orders/[id]/attachments/[filename]` — removes a single token from `additional_attachments`. - Order detail UI: in the **Send PO** modal, an "Additional attachments" panel above the message textarea with: - List of currently-attached files (filename, size, remove button) - Multi-file picker to upload more - Disabled while the modal is loading the line count - Modal: list reads from `po.additional_attachments`; refresh after upload/delete. - New MCP tool: `purchase_orders.add_attachment` (POST a single file via base64, like `attachments.upload_for_quote`). - Updated MCP tool: `purchase_orders.list` returns the attachment count per PO so the agent can see at a glance. - Updated `purchase-orders-service.ts` send-email path: pass the `additional_attachments` array to the n8n webhook so the supplier gets them. - n8n is out of scope for this repo. The webhook payload now includes `additional_attachments: [ { filename, token, content_type, size_bytes }, ... ]` — the workflow needs to read these and attach them. We surface this in the spec so the operator can wire it. ### Out of Scope - Anything that touches `documents` / `documents_history` (the versioned-PDF flow). - Changing the existing `buildDefaultPoEmailBody` template. The body still says "(N lines)" — the attachments travel as files, not in the body text. - Auto-attaching the same supplier quote to every send. Each send picks from whatever is currently attached. - Versioning the additional attachments. They live with the PO; if the operator wants to track "which quote did we send with v3", that's a future feature. ## Current State `purchase_orders.documents` is a multi-file field used for versioned PO PDFs. `documents_history` is a JSON array that tracks each version's `file` (human-readable name), `token` (PB-stored slug), `generatedAt`, `generatedBy`, `reason`, `status`. The two are kept in sync by the POST handler of `/api/v1/purchase-orders/[id]/pdf`. The Send PO modal currently pre-populates the email body with the line count (we just fixed the count source). It has no concept of additional attachments. ## Data Model ### New field on `purchase_orders` ``` additional_attachments type: file required: false maxSelect: 0 (unlimited) maxSize: 0 (no PB cap; the upload route enforces 25 MB per file to match the rest of the app) mimeTypes: null thumbs: null protected: false ``` Rationale: identical shape to `documents` so the existing `files.getURL` and `/api/v1/files/purchase_orders//` proxy whitelist work without changes. The proxy already accepts `purchase_orders`. ### Why a separate field and not the rename-into-`documents` shortcut If we put additional files in `documents` and renamed them with a prefix, we'd break the `documents ↔ documents_history` invariant that the PDF download flow depends on. Operators regenerating POs risk dropping the attachment or having n8n pick the wrong file as the PDF. A separate field is a clean split. ## Behavior ### Operator workflow 1. On the order detail page, open the **Send PO** modal for a PO that needs supporting context (supplier quote, spec sheet, etc.). 2. In the **Additional attachments** panel, click "Attach files" → multi-file picker. 3. Files upload via `POST /api/v1/purchase-orders/{poId}/attachments/upload`. The panel refreshes to show the new list. 4. Existing files can be removed via the trash icon → `DELETE /api/v1/purchase-orders/{poId}/attachments/[filename]`. 5. When the operator hits **Send**, the n8n webhook receives the new payload (see below) and the supplier gets the PO PDF plus all currently-attached files. ### Upload route (POST) - Multipart form: `file` (required, File). - Admin PB (bypasses `createRule`). - Inserts into `additional_attachments` while preserving the existing array. PB SDK's FormData update replaces the field, so we read first, append the new token, and write back. - Returns `{ ok: true, attachments: [{ filename, token, content_type, size_bytes }] }`. - 400 if no file, 500 on PB failure. ### Delete route (DELETE) - Path: `DELETE /api/v1/purchase-orders/{poId}/attachments/[filename]` - The `[filename]` is the PB-stored token (matches the `documents_history[].token` pattern and the proxy URL path). - Admin PB. Read the current `additional_attachments`, filter out the matching token, write back. - Returns `{ ok: true, attachments: [...] }`. - 404 if the token isn't found in the array. ### Send-email payload change The `/api/v1/purchase-orders/[id]/send-email` route currently sends: ``` { po_number, supplier_email, cc, bcc, subject, message, pdf_token, pdf_filename, total_amount } ``` We add: ``` additional_attachments: [ { filename, token, content_type, size_bytes }, ... ] ``` Empty array if nothing attached. n8n reads the field and attaches each token to the outbound SMTP message via the PB file proxy (or its own admin PB client — operator's choice). ## Testing ### Upload route (`route.test.ts`) 1. POST without a file → 400. 2. POST with a single file → 201, response includes the new token, PB update is called with the appended array. 3. POST preserves existing attachments (read-modify-write pattern). 4. Reject files over 25 MB at the route level (matches the quote-attachments cap). 5. POST is admin-auth-gated (no admin → 500 with the existing `getAdminPocketBase` failure shape). ### Delete route (`route.test.ts`) 1. DELETE on an existing token → 200, token removed from the array. 2. DELETE on a missing token → 404. 3. DELETE preserves the rest of the array. ### UI Order detail page: existing modal still works; new attachments panel renders with empty state, "Attach files" button, and the upload list. The send-time test from the previous fix is unchanged. ### MCP 1. `tools/list` includes `purchase_orders.add_attachment`. 2. `purchase_orders.add_attachment` accepts `{ po_id, filename, base64, content_type? }` and returns the same shape as the REST route. 3. `purchase_orders.list` (new) returns an `attachment_count` field per PO. ## Migration `scripts/migrations/2026-07-09-po-additional-attachments.ts`: - Read `purchase_orders` schema from PB. - If `additional_attachments` field doesn't exist, add it via PB's field-add API. - Log `[po-additional-attachments] created field on purchase_orders` or `[po-additional-attachments] already present`. Idempotent on re-run. Add `npm run pb:migrate:po-additional-attachments` to `package.json` (same `npx tsx` convention as every other migration). ## Acceptance Criteria - Operator can upload and remove additional attachments from the Send PO modal. - The Send PO webhook payload includes the additional attachments so n8n can attach them. - Existing versioned-PDF flow is untouched. - All targeted tests pass; full suite non-regressive. - Migration is idempotent. ## Risk / Rollback - **Schema risk:** adding a file field with no constraints is safe — PB accepts empty arrays on existing rows. No data migration needed. - **Rollback:** remove the field via the PB admin UI. Drop the routes, the UI panel, and the MCP tool. The n8n workflow ignores the new payload field if absent. ## Operator-facing note (separate doc, post-implementation) A short note for whoever maintains the n8n workflow: > The Send PO webhook now includes `additional_attachments: [{ filename, token, content_type, size_bytes }, ...]`. Read each entry, fetch the file from the PB file proxy (`/api/files/purchase_orders//` with admin auth), and attach it to the outbound SMTP message. Empty array means no extras.