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.
4.5 KiB
Quote Outbound Send — operator-uploaded attachments fix
Deployed 2026-09-09 alongside the quotes.outbound_attachments /
quotes.internal_attachments storage split (the migration that
moved operator-uploaded files off the legacy quote_attachments
collection's direction enum).
What the deployed workflow does now
The active n8n workflow is WFDASH — Quote Outbound Send (push-payload + outbound-attachments) (id PIEAkPYYYZLGl6Pj,
previously named …(push-payload)).
The chain still ends in Send Email (SMTP) with the auto-generated
PDF attached — same as before — plus a new Build attachments list Code node that runs between HTTP Request (PDF download) and
the SMTP node. It:
- Reads
quotes.outbound_attachments(the PB file field on the parent quote row) andemail_outbound_attachment_ids(the JSON array of operator-selected tokens persisted bysendQuoteEmail). - Intersects the two so only files the operator explicitly marked for the customer get attached. Defends against drift between the modal snapshot and the send.
- Downloads each token from
https://<PB>/api/files/quotes/<quoteId>/<token>viathis.helpers.httpRequest. - Returns a single item with
attachments: [{name, content (base64), type}, …]on the JSON — same shape the deployedWFDASH — Purchase Order Sendworkflow consumes. - Per-download failures are skipped (not raised) so a single bad
token doesn't abort the whole send. Operator sees the final count
via
attachmentCount.
The SMTP node now consumes the array via
options.attachments={{ $json.attachments && $json.attachments.length ? $json.attachments : [] }} — matching the PO Send pattern. The
previous options.fileAttachments=data (PDF only) is removed.
Bonus fix: failure status send_failed → mail_failed
The deployed Update quote - Send Failed node previously flipped a
failed send to status send_failed. The dashboard's retryQuoteEmail
only accepts mail_failed, so failed sends were stranded — operator
couldn't retry from the UI. The failure path now flips to
mail_failed. Stale send_failed rows (if any pre-existed) can be
re-categorised with a one-shot PB update if the operator wants the
tab badge to count them.
Why the repo file was deleted
n8n_workfows/WFDASH — Quote Outbound Send.json (the pre-existing
repo file) was stale — it referenced a non-existent MCP tool
(quotes.get_send_payload), the old row-ID model
(selected_attachment_ids + pdf_attachment_id), $env.PB_ADMIN_TOKEN
(which the docker-compose blocks via
N8N_BLOCK_ENV_ACCESS_IN_NODE=true), and field names that had
been renamed (email_message_id, email_delivered, …). It did
not match the deployed workflow and would have misled anyone
debugging the system. Deleted.
A clean snapshot of the previously-deployed workflow is preserved at
n8n_workfows/WFDASH — Quote Outbound Send (push-payload).baseline.json
for diff/rollback reference. The post-fix deployed workflow is in
n8n_workfows/WFDASH — Quote Outbound Send (with-outbound-attachments).json.
MCP quotes.send_email description
Updated to spell out the new field semantics: "selectedAttachmentIds
is a list of PB file tokens from quotes.outbound_attachments
(operator-uploaded customer-facing files — supplier quotes, drawings,
NDAs). The PDF is always attached separately from
quote_attachments kind="pdf". Also added a per-property description on the input schema and updated the docs/api-mcp-layer.md`
table to match.
Verification
| Layer | Result |
|---|---|
| Deployed workflow via n8n REST | PUT returned HTTP 200, active=true, node count 24 → 25, new connections verified |
quotes-service.test.ts |
117 pass, 4 pre-existing failures (stale-PDF auto-regen — unrelated, baseline fails too) |
mcp/route.test.ts |
118 pass, 2 pre-existing failures (unrelated, baseline fails too) |
| Code node JS syntax | node --check passes |
Files touched
- Deleted:
n8n_workfows/WFDASH — Quote Outbound Send.json - Added:
n8n_workfows/WFDASH — Quote Outbound Send (push-payload).baseline.jsonn8n_workfows/WFDASH — Quote Outbound Send (with-outbound-attachments).jsondocs/quotes-outbound-attachments-deploy.md
- Modified:
src/app/api/mcp/route.ts(tool description +selectedAttachmentIdsschema property)src/app/api/v1/quotes/[id]/send-email/route.ts(docstring only)docs/api-mcp-layer.md(table row)
- Deployed: workflow
PIEAkPYYYZLGl6Pjin the live n8n instance