WFDASH-WEBAPP/docs/superpowers/specs/2026-07-09-po-additional-attachments-design.md
2026-07-09 22:55:16 +02:00

8.2 KiB

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.