From e19bb7dbf75688ebb49cd980980446fd858cbaac Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Thu, 9 Jul 2026 22:55:16 +0200 Subject: [PATCH] Add PO additional attachments spec --- ...-07-09-po-additional-attachments-design.md | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-09-po-additional-attachments-design.md diff --git a/docs/superpowers/specs/2026-07-09-po-additional-attachments-design.md b/docs/superpowers/specs/2026-07-09-po-additional-attachments-design.md new file mode 100644 index 0000000..5840ec2 --- /dev/null +++ b/docs/superpowers/specs/2026-07-09-po-additional-attachments-design.md @@ -0,0 +1,160 @@ +# 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. \ No newline at end of file