WFDASH-WEBAPP/n8n_workfows/WFDASH — Purchase Order Send.json

114 lines
7 KiB
JSON
Raw Normal View History

{
"name": "WFDASH — Purchase Order Send",
"description": "Sends supplier PO emails with the latest PO PDF attached. Fired by Next.js route /api/v1/purchase-orders/[id]/send-email after it builds the full render-ready payload (recipient + subject + body + PDF attachment URL + AGENT_API_KEY + dashboard base URL). n8n never reads PocketBase or env vars directly. Records success/failure back to the dashboard via the outcome callback route.",
"tags": [
{ "name": "wfdash", "id": "wf-tag-wfdash" }
],
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "wf-po-send",
"responseMode": "lastNode",
"options": {}
},
"id": "wf-po-send-webhook",
"name": "PO Send Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [0, 0],
"webhookId": "wf-po-send"
},
{
"parameters": {
"jsCode": "// Resolve any attachment URLs into { name, content (base64), type }\n// entries the n8n emailSend node accepts. Returns the original input\n// shape unchanged when there are no attachments.\n//\n// n8n Code node: no $env.* access in this environment\n// (N8N_BLOCK_ENV_ACCESS_IN_NODE=true in docker-compose). The dashboard\n// route pushes everything we need (incl. the agentApiKey Bearer\n// credential) on the webhook body. The file proxy whitelist now\n// includes purchase_orders so the GET succeeds without further auth\n// headers from the cookie-less n8n-worker.\nconst first = ($input.first() || {}).json || {};\nconst body = first.body || first;\nconst attachments = body.attachments || [];\nconst agentApiKey = body.agentApiKey || '';\n\nif (!Array.isArray(attachments) || attachments.length === 0) {\n return [{ json: { ...first, body: { ...body, attachments: [] }, attachmentsResolved: [] } }];\n}\n\nconst resolved = [];\nfor (const att of attachments) {\n try {\n const res = await this.helpers.httpRequest({\n method: 'GET',\n url: att.url,\n headers: agentApiKey\n ? { 'Authorization': 'Bearer ' + agentApiKey }\n : {},\n encoding: 'base64',\n returnBinary: false,\n timeout: 15000,\n });\n resolved.push({\n name: att.filename || 'attachment.pdf',\n content: typeof res === 'string' ? res : (res && res.data) || '',\n type: att.contentType || 'application/pdf',\n });\n } catch (err) {\n return [{ json: {\n ...first,\n body: {\n ...body,\n attachments: [],\n _attachmentFetchError: 'PDF fetch failed: ' + (err.message || String(err)),\n },\n attachmentsResolved: [],\n } }];\n }\n}\n\nreturn [{ json: {\n ...first,\n body: { ...body, attachments: [] },\n attachmentsResolved: resolved,\n} }];"
},
"id": "wf-po-send-fetch-pdf",
"name": "Fetch PDF attachment bytes",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [220, 0]
},
{
"parameters": {
"fromEmail": "={{ $json.body.fromEmail || 'orders@wfdash.local' }}",
"toEmail": "={{ $json.body.recipient }}",
"ccEmail": "={{ $json.body.cc }}",
"bccEmail": "={{ $json.body.bcc }}",
"subject": "={{ $json.body.subject }}",
"text": "={{ $json.body.body }}",
"html": "={{ $json.body.body }}",
"options": {
"attachments": "={{ $json.attachmentsResolved && $json.attachmentsResolved.length ? $json.attachmentsResolved : [] }}"
}
},
"id": "wf-po-send-smtp",
"name": "Send PO email (SMTP)",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [440, 0]
},
{
"parameters": {
"jsCode": "// Resolve the SMTP result from the Send PO email (SMTP) node, then\n// POST the outcome back to the dashboard's\n// /api/v1/purchase-orders/[id]/send-email/outcome route. The dashboard\n// owns the canonical write (status flip + order_items.item_status flip)\n// using its own admin PB auth. n8n never carries the admin token.\n//\n// The Bearer key and dashboard base URL are pushed on the webhook body\n// (not $env.*) because the docker-compose has\n// N8N_BLOCK_ENV_ACCESS_IN_NODE=true — Code nodes cannot read env vars\n// at runtime.\nconst first = ($input.first() || {}).json || {};\nconst body = first.body || first;\nconst agentApiKey = body.agentApiKey || '';\nconst dashboardBase = (body.dashboardBaseUrl || '').replace(/\\/$/, '');\nconst poId = body.poId;\n\nif (!agentApiKey) {\n return [{ json: {\n poId,\n ok: false,\n error: 'agentApiKey missing from webhook body — route did not push it',\n } }];\n}\nif (!dashboardBase) {\n return [{ json: {\n poId,\n ok: false,\n error: 'dashboardBaseUrl missing from webhook body — route did not push it',\n } }];\n}\n\nconst sendResult = ($('Send PO email (SMTP)').first().json) || {};\nconst ok = !sendResult.error && sendResult.ok !== false;\nconst messageId = sendResult.messageId || sendResult.message_id || '';\nconst errorMessage = ok ? '' : (sendResult.error || sendResult.response || 'Unknown SMTP error').toString().slice(0, 990);\n\ntry {\n const out = await this.helpers.httpRequest({\n method: 'POST',\n url: dashboardBase + '/api/v1/purchase-orders/' + poId + '/send-email/outcome',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + agentApiKey,\n },\n json: true,\n body: {\n outcome: ok ? 'success' : 'failure',\n messageId,\n errorMessage,\n },\n });\n return [{ json: { poId, ok, messageId, error: errorMessage, outcome: out } }];\n} catch (err) {\n return [{ json: {\n poId,\n ok,\n messageId,\n error: 'Outcome callback failed: ' + (err.message || String(err)),\n } }];\n}"
},
"id": "wf-po-send-record-outcome",
"name": "Record outcome to PB",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [660, 0]
}
],
"connections": {
"PO Send Webhook": {
"main": [
[
{
"node": "Fetch PDF attachment bytes",
"type": "main",
"index": 0
}
]
]
},
"Fetch PDF attachment bytes": {
"main": [
[
{
"node": "Send PO email (SMTP)",
"type": "main",
"index": 0
}
]
]
},
"Send PO email (SMTP)": {
"main": [
[
{
"node": "Record outcome to PB",
"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-po-send",
"templateCredsSetupCompleted": false,
"description": "After import: open the workflow, attach an SMTP credential to the 'Send PO email (SMTP)' node (any name; the SDK does not pin a credential id). Then activate."
}
}