WFDASH-WEBAPP/n8n_workfows/WFDASH — Quote Outbound Send.json
SJCoetz 588e9762c2 feat: add quote templates, improved email, new statuses
Add complete quote template functionality, enhanced outbound email sending, new quote statuses, and supporting infrastructure:
- Add new quote statuses (template, sending, mail_failed) with tailored UI styling for status pills
- Implement full template workflows: save real quotes as templates, create new quotes from existing templates
- Add support for custom quote dates and extended validity periods on generated PDF documents
- Build out improved email pipeline with persisted compose fields and retry functionality for failed sends
- Add attachment direction tracking to categorize inbound, outbound, and generated quote attachments
- Fix tooltip component to properly handle Radix `asChild` prop to avoid React DOM attribute errors
- Add comprehensive test coverage, schema migrations, and update sample test and production data
2026-06-24 22:57:01 +02:00

344 lines
No EOL
11 KiB
JSON

{
"name": "WFDASH — Quote Outbound Send",
"description": "Sends approved quote emails via SMTP. Triggered when Next.js flips a quote to status='sending'. Records success (→ emailed) or failure (→ mail_failed) back to PocketBase with the SMTP response.",
"tags": [
{
"name": "wfdash",
"id": "wf-tag-wfdash"
}
],
"nodes": [
{
"parameters": {
"events": [
"update"
],
"collection": "quotes",
"filter": "status='sending'",
"options": {
"expand": "customer"
}
},
"id": "trigger-pocketbase-quote-sending",
"name": "PB: Quote status → sending",
"type": "n8n-nodes-pocketbase.pocketbaseTrigger",
"typeVersion": 1,
"position": [
0,
0
],
"webhookId": "wf-quote-outbound-send-trigger",
"credentials": {
"pocketbaseApi": {
"id": "PB_CRED_ID",
"name": "PocketBase account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose",
"version": 3
},
"conditions": [
{
"leftValue": "={{ $json.record.status }}",
"rightValue": "sending",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "cond-status-sending"
},
{
"leftValue": "={{ $json.record.email_message_id }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEquals"
},
"id": "cond-not-already-sent"
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-status-sending",
"name": "If status == sending",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
220,
0
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.WFDASH_API_BASE_URL }}/api/mcp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "X-Agent-Api-Key",
"value": "={{ $env.AGENT_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"jsonrpc\": \"2.0\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"quotes.get_send_payload\",\n \"arguments\": { \"quote_id\": \"{{ $json.record.id }}\" }\n },\n \"id\": \"1\"\n}",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
},
"timeout": 15000
}
},
"id": "mcp-fetch-send-payload",
"name": "MCP: Get send payload",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
440,
-120
]
},
{
"parameters": {
"jsCode": "// Compose the MIME message from the MCP payload. n8n's Send Email\n// node handles attachments + headers natively, so we only need to\n// shape the data it consumes.\nconst payload = $json.result?.data || $json.result || $json;\nconst quote = payload.quote || {};\nconst customer = quote.expand?.customer || quote.customer || {};\n\n// Build a deterministic RFC 5322 Message-ID per send. The format\n// embeds the quote id + epoch ms so retries get a different id\n// (RFC 5322 requires uniqueness).\nconst now = new Date();\nconst epoch = now.getTime();\nconst nonce = Math.random().toString(36).slice(2, 10);\nconst messageId = `<quote-${quote.id}-${epoch}-${nonce}@wfdash.local>`;\n\n// Pull the operator's persisted composition (saved at modal submit\n// time). Defaults come from MCP payload.\nconst subject = payload.subject || `Quote ${quote.quote_number}`;\nconst text = payload.message || `Please find attached quote ${quote.quote_number}.`;\n\n// Convert the JSON-encoded attachment IDs into a list of file URLs.\n// n8n's HTTP Request nodes download each file into $binary.\nconst attIds = Array.isArray(payload.selected_attachment_ids)\n ? payload.selected_attachment_ids\n : [];\nconst pdfId = payload.pdf_attachment_id;\nconst allIds = pdfId ? [pdfId, ...attIds.filter((x) => x !== pdfId)] : attIds;\n\nreturn {\n json: {\n quote_id: quote.id,\n quote_number: quote.quote_number,\n to: payload.to || customer.email,\n cc: payload.cc || \"\",\n bcc: payload.bcc || \"\",\n subject,\n text,\n from_name: payload.from_name || quote.company_letterhead || \"WFDASH\",\n attachment_ids: allIds,\n message_id: messageId,\n compose_ts: now.toISOString()\n }\n};\n"
},
"id": "code-format-email",
"name": "Format email params",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
-120
]
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/files/quote_attachments/{{ $json.attachment_id }}/{{ $json.filename }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ $env.PB_ADMIN_TOKEN }}"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "file"
}
},
"timeout": 30000
}
},
"id": "http-fetch-attachment",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
880,
-120
]
},
{
"parameters": {
"fromEmail": "=quotes@wfsales.co.za",
"toEmail": "={{ $json.to }}",
"subject": "={{ $json.subject }}",
"emailFormat": "text",
"text": "={{ $json.text }}",
"options": {
"appendAttribution": false,
"ccEmail": "={{ $json.cc }}",
"bccEmail": "={{ $json.bcc }}",
"replyTo": "quotes@wfsales.co.za",
"attachments": "={{ $binary }}"
}
},
"id": "send-email-smtp",
"name": "Send Email (SMTP)",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
1100,
-120
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"onError": "continueRegularOutput",
"credentials": {
"smtp": {
"id": "SMTP_CRED_ID",
"name": "WFDASH Outbound SMTP"
}
}
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/collections/pbc_2527524235/records/{{ $node[\"Format email params\"].json.quote_id }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Authorization",
"value": "={{ $env.PB_ADMIN_TOKEN }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"status\": \"emailed\",\n \"email_delivered\": true,\n \"email_delivered_at\": \"{{ $now.toISO() }}\",\n \"email_outbound_message_id\": \"{{ $node[\"Format email params\"].json.message_id }}\",\n \"email_error\": \"\",\n \"sending_started_at\": \"\"\n}",
"options": {
"timeout": 15000
}
},
"id": "patch-quote-emailed",
"name": "PATCH: quote → emailed",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1320,
-200
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/collections/pbc_2527524235/records/{{ $node[\"Format email params\"].json.quote_id }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Authorization",
"value": "={{ $env.PB_ADMIN_TOKEN }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"status\": \"mail_failed\",\n \"email_failed_at\": \"{{ $now.toISO() }}\",\n \"email_error\": \"[smtp] {{ ($json.error?.message || $json.message || 'Unknown SMTP failure').toString().slice(0, 4900) }}\",\n \"email_outbound_message_id\": \"{{ $node[\"Format email params\"].json.message_id }}\",\n \"sending_started_at\": \"\"\n}",
"options": {
"timeout": 15000
}
},
"id": "patch-quote-mail-failed",
"name": "PATCH: quote → mail_failed",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1320,
-40
]
}
],
"connections": {
"PB: Quote status → sending": {
"main": [
[
{
"node": "If status == sending",
"type": "main",
"index": 0
}
]
]
},
"If status == sending": {
"main": [
[
{
"node": "MCP: Get send payload",
"type": "main",
"index": 0
}
]
]
},
"MCP: Get send payload": {
"main": [
[
{
"node": "Format email params",
"type": "main",
"index": 0
}
]
]
},
"Format email params": {
"main": [
[
{
"node": "Send Email (SMTP)",
"type": "main",
"index": 0
}
]
]
},
"Send Email (SMTP)": {
"main": [
[
{
"node": "PATCH: quote → emailed",
"type": "main",
"index": 0
}
]
],
"error": [
[
{
"node": "PATCH: quote → mail_failed",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"saveDataSuccessExecution": "all",
"saveDataErrorExecution": "all",
"saveManualExecutions": true,
"saveExecutionProgress": true,
"timezone": "Africa/Johannesburg"
},
"staticData": null,
"pinData": {},
"versionId": "1",
"triggerCount": 1,
"meta": {
"instanceId": "wfdash-quote-outbound-send",
"templateCredsSetupCompleted": false,
"description": "SMTP creds + PocketBase creds + WFDASH API base URL + AGENT_API_KEY env vars must be configured in n8n before activation. Add a new SMTP credential named 'WFDASH Outbound SMTP' and a PocketBase credential named 'PocketBase account' before publishing."
}
}