Add PO additional attachments spec

This commit is contained in:
SJCoetz 2026-07-09 22:55:16 +02:00
parent dfe52a62d9
commit e19bb7dbf7

View file

@ -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/<id>/<token>` 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/<poId>/<token>` with admin auth), and attach it to the outbound SMTP message. Empty array means no extras.