backup: GL Missing Dept+Class → Freshdesk Ticket [2026-10-14]

This commit is contained in:
moonside committed 2026-10-14 04:00:49 +00:00
1 parent 7d79b215aa
commit a596f2daed
1 file changed
+65 -57
@@ -2,11 +2,11 @@
"id": "g6XejMv9mvZwYOoS",
"name": "GL Missing Dept+Class → Freshdesk Ticket",
"active": true,
"activeVersionId": "c4d54c06-313e-4447-b40d-9436c142bf1e",
"activeVersionId": "243af477-fe03-4ef5-85e8-c6b8555b4829",
"createdAt": "2026-06-10T20:12:11.230Z",
"updatedAt": "2026-08-11T17:09:07.418Z",
"updatedAt": "2026-10-13T16:25:04.283Z",
"isArchived": false,
"versionId": "c4d54c06-313e-4447-b40d-9436c142bf1e",
"versionId": "243af477-fe03-4ef5-85e8-c6b8555b4829",
"triggerCount": 1,
"nodes": [
{
@@ -15,13 +15,13 @@
"query": "SELECT\n gl.company_id,\n gl.\"Date\"::date AS date,\n gl.\"Num\" AS num,\n gl.\"Txn_Id\" AS txn_id,\n gl.\"Account\" AS account,\n gl.\"Account_Id\" AS account_id,\n coa.\"Classification\" AS classification,\n coa.\"AccountType\" AS account_type,\n gl.\"name\" AS name,\n gl.\"Transaction_Type\" AS transaction_type,\n gl.\"CustomerName\" AS customer_name,\n gl.\"Department\" AS department,\n gl.\"Class\" AS class,\n gl.amount\nFROM view_general_ledger gl\nINNER JOIN chartofaccounts coa\n ON coa.\"Account Key\" = gl.company_id || ' ' || gl.\"Account_Id\"\nWHERE\n coa.\"Classification\" = 'Expense'\n AND coa.\"AccountType\" <> 'Other Expense'\n AND gl.\"Num\" IS NOT NULL\n AND TRIM(gl.\"Num\") <> ''\n AND (gl.\"Department\" IS NULL OR TRIM(gl.\"Department\") = '')\n AND (gl.\"Class\" IS NULL OR TRIM(gl.\"Class\") = '')\n AND gl.\"Date\" >= DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month')\n AND gl.\"Date\" < DATE_TRUNC('month', CURRENT_DATE + INTERVAL '1 month')\n AND NOT EXISTS (\n SELECT 1 FROM moneybot_gl_missing_dept_class_log log\n WHERE log.company_id = gl.company_id\n AND log.date = gl.\"Date\"::date\n AND log.num = gl.\"Num\"\n AND log.account = gl.\"Account\"\n )\nORDER BY gl.company_id, gl.\"Date\" DESC\nLIMIT 500;",
"options": {}
},
"id": "42e9579d-d9a3-47e4-9e6f-16ff7cbc15ba",
"id": "d99db822-331b-4722-b94d-076fd82c5241",
"name": "🗄️ PG – Fetch Untagged Rows",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
208,
-1232
224,
-1424
],
"credentials": {
"postgres": {
@@ -54,26 +54,26 @@
},
"options": {}
},
"id": "69f58fcd-046a-4017-ac95-e801d81ed05f",
"id": "a850d62d-4e0e-4944-9b87-71dacc627ebd",
"name": "🔍 Any New Rows?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
400,
-1232
416,
-1424
]
},
{
"parameters": {
"jsCode": "const rows = $input.all().map(i => i.json);\nconst grouped = {};\n\nfor (const row of rows) {\n const cid = row.company_id || 'UNKNOWN';\n if (!grouped[cid]) grouped[cid] = [];\n grouped[cid].push(row);\n}\n\nconst companyLabels = {\n MUS: 'Masttro US (MUS)',\n MMX: 'Masttro Mexico (MMX)',\n MSAG: 'Masttro Switzerland (MSAG)',\n MHAG: 'Masttro Switzerland Holding (MHAG)',\n MGC: 'Masttro Global (MGC)'\n};\n\nconst companyCurrency = {\n MUS: 'USD',\n MMX: 'USD',\n MGC: 'USD',\n MSAG: 'CHF',\n MHAG: 'CHF'\n};\n\nconst cleanDate = (d) => d ? String(d).split('T')[0] : '';\n\nfunction qboLink(txnType, txnId) {\n if (!txnId) return '';\n const typeMap = {\n 'Bill': 'bill',\n 'Check': 'check',\n 'Credit Card Charge': 'creditcardcharge',\n 'Credit Card Credit': 'creditcardcredit',\n 'Journal Entry': 'journal',\n 'Expense': 'expense',\n 'Purchase Order': 'purchaseorder',\n 'Vendor Credit': 'vendorcredit'\n };\n const path = typeMap[txnType];\n if (!path) return '';\n return `https://app.qbo.intuit.com/app/${path}?txnId=${encodeURIComponent(txnId)}`;\n}\n\nconst now = new Date();\nconst prevMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);\nconst currMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);\nconst fmt = d => d.toISOString().split('T')[0];\nconst dateRangeLabel = `${fmt(prevMonthStart)} – ${fmt(currMonthEnd)}`;\n\nconst output = [];\n\nfor (const [company_id, txns] of Object.entries(grouped)) {\n const label = companyLabels[company_id] || company_id;\n const currency = companyCurrency[company_id] || 'USD';\n const count = txns.length;\n const totalAmt = txns.reduce((s, r) => s + parseFloat(r.amount || 0), 0);\n\n const tableRows = txns.map(r => {\n const amt = parseFloat(r.amount || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\n const link = qboLink(r.transaction_type, r.txn_id);\n const editCell = link\n ? `<a href=\"${link}\" target=\"_blank\">Edit in QBO</a>`\n : '—';\n const dept = r.department && r.department.trim() ? r.department : '<span style=\"color:#cc0000\">Missing</span>';\n const cls = r.class && r.class.trim() ? r.class : '<span style=\"color:#cc0000\">Missing</span>';\n return `<tr>\n <td>${cleanDate(r.date)}</td>\n <td>${r.transaction_type || ''}</td>\n <td>${r.name || ''}</td>\n <td>${r.account || ''}</td>\n <td>${r.customer_name || ''}</td>\n <td>${dept}</td>\n <td>${cls}</td>\n <td style=\"text-align:right\">${amt} ${currency}</td>\n <td>${editCell}</td>\n </tr>`;\n }).join('\\n');\n\n const description = `<p><strong>${label}</strong> has <strong>${count} expense transaction(s)</strong> missing both Department and Class coding.</p>\n<p>Date range scanned: <strong>${dateRangeLabel}</strong></p>\n<p>Only transactions where <strong>both</strong> Department and Class are blank are included.</p>\n\n<table border=\"1\" cellpadding=\"4\" cellspacing=\"0\" style=\"border-collapse:collapse;font-size:12px\">\n <thead style=\"background:#f0f0f0\">\n <tr>\n <th>Date</th><th>Type</th><th>Name</th><th>Account</th><th>Customer</th><th>Department</th><th>Class</th><th style=\"text-align:right\">Amount (${currency})</th><th>Action</th>\n </tr>\n </thead>\n <tbody>\n ${tableRows}\n </tbody>\n</table>\n\n<p><em>Total uncoded expense amount: ${totalAmt.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency}</em></p>\n<p>Please click <strong>Edit in QBO</strong> on each row and update the Department and Class fields.</p>`;\n\n output.push({\n json: {\n company_id,\n subject: `[${company_id}] GL Coding Alert – ${count} Expense Transaction(s) Missing Dept & Class (${dateRangeLabel})`,\n description,\n rows: txns\n }\n });\n}\n\nreturn output;"
},
"id": "417c74cb-9e63-4020-9d1d-a1f88fcbde79",
"id": "1361c47c-52f6-46d8-94e7-f851e8676663",
"name": "⚙️ Group by Company & Build Ticket",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
608,
-1248
624,
-1440
]
},
{
@@ -96,13 +96,13 @@
"jsonBody": "={{ JSON.stringify({ subject: $json.subject, description: $json.description, email: 'mnolan@masttro.com', priority: 2, status: 2, group_id: 158000821060 }) }}",
"options": {}
},
"id": "674f9eda-2dba-472c-a9fb-313a4333d9ab",
"id": "4bbb7311-a3a7-488b-855c-1abcbabec911",
"name": "🎫 Freshdesk – Create Ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
848,
-1248
864,
-1440
],
"credentials": {
"httpBasicAuth": {
@@ -115,13 +115,13 @@
"parameters": {
"jsCode": "const ticketId = String($('🎫 Freshdesk – Create Ticket').item.json.id || '');\nconst rows = $('⚙️ Group by Company & Build Ticket').item.json.rows;\n\nreturn rows.map(r => ({\n json: {\n company_id: r.company_id,\n date: String(r.date).split('T')[0],\n num: r.num || '',\n account: r.account || '',\n amount: parseFloat(r.amount || 0),\n ticket_id: ticketId\n }\n}));"
},
"id": "34d8b607-52fd-48a1-9ea9-c4725fdb58ad",
"id": "f263b80c-fce4-42d0-ab4a-1f15cd4f9f6b",
"name": "⚙️ Prep Log Rows",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1072,
-1248
1088,
-1440
]
},
{
@@ -130,13 +130,13 @@
"query": "INSERT INTO moneybot_gl_missing_dept_class_log (company_id, date, num, account, amount, ticket_id)\nVALUES (\n '{{ $json.company_id }}',\n '{{ $json.date }}',\n '{{ $json.num }}',\n '{{ $json.account }}',\n {{ $json.amount }},\n '{{ $json.ticket_id }}'\n)\nON CONFLICT (company_id, date, num, account) DO NOTHING;",
"options": {}
},
"id": "9e6ea309-4b22-4614-b270-19fa2d654ecd",
"id": "407319ec-4b69-4026-8f6e-8627aafe36c8",
"name": "🗄️ PG – Log to Tracking Table",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1296,
-1248
1312,
-1440
],
"credentials": {
"postgres": {
@@ -150,19 +150,19 @@
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 30
"field": "hours",
"triggerAtMinute": 17
}
]
}
},
"id": "7cf9aeac-0320-48df-b56e-0b6d4342a22d",
"name": "15m",
"id": "b06dfc08-18f5-4d04-97e8-bb2dcbee13db",
"name": "1h",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
-1232
16,
-1424
]
}
],
@@ -222,7 +222,7 @@
]
]
},
"15m": {
"1h": {
"main": [
[
{
@@ -259,6 +259,14 @@
"recurrenceRuleSignatures": [
null
]
},
"node:1h": {
"recurrenceRules": [
null
],
"recurrenceRuleSignatures": [
null
]
}
},
"pinData": {},
@@ -274,7 +282,7 @@
}
],
"activeVersion": {
"versionId": "c4d54c06-313e-4447-b40d-9436c142bf1e",
"versionId": "243af477-fe03-4ef5-85e8-c6b8555b4829",
"workflowId": "g6XejMv9mvZwYOoS",
"nodes": [
{
@@ -283,13 +291,13 @@
"query": "SELECT\n gl.company_id,\n gl.\"Date\"::date AS date,\n gl.\"Num\" AS num,\n gl.\"Txn_Id\" AS txn_id,\n gl.\"Account\" AS account,\n gl.\"Account_Id\" AS account_id,\n coa.\"Classification\" AS classification,\n coa.\"AccountType\" AS account_type,\n gl.\"name\" AS name,\n gl.\"Transaction_Type\" AS transaction_type,\n gl.\"CustomerName\" AS customer_name,\n gl.\"Department\" AS department,\n gl.\"Class\" AS class,\n gl.amount\nFROM view_general_ledger gl\nINNER JOIN chartofaccounts coa\n ON coa.\"Account Key\" = gl.company_id || ' ' || gl.\"Account_Id\"\nWHERE\n coa.\"Classification\" = 'Expense'\n AND coa.\"AccountType\" <> 'Other Expense'\n AND gl.\"Num\" IS NOT NULL\n AND TRIM(gl.\"Num\") <> ''\n AND (gl.\"Department\" IS NULL OR TRIM(gl.\"Department\") = '')\n AND (gl.\"Class\" IS NULL OR TRIM(gl.\"Class\") = '')\n AND gl.\"Date\" >= DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month')\n AND gl.\"Date\" < DATE_TRUNC('month', CURRENT_DATE + INTERVAL '1 month')\n AND NOT EXISTS (\n SELECT 1 FROM moneybot_gl_missing_dept_class_log log\n WHERE log.company_id = gl.company_id\n AND log.date = gl.\"Date\"::date\n AND log.num = gl.\"Num\"\n AND log.account = gl.\"Account\"\n )\nORDER BY gl.company_id, gl.\"Date\" DESC\nLIMIT 500;",
"options": {}
},
"id": "42e9579d-d9a3-47e4-9e6f-16ff7cbc15ba",
"id": "d99db822-331b-4722-b94d-076fd82c5241",
"name": "🗄️ PG – Fetch Untagged Rows",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
208,
-1232
224,
-1424
],
"credentials": {
"postgres": {
@@ -322,26 +330,26 @@
},
"options": {}
},
"id": "69f58fcd-046a-4017-ac95-e801d81ed05f",
"id": "a850d62d-4e0e-4944-9b87-71dacc627ebd",
"name": "🔍 Any New Rows?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
400,
-1232
416,
-1424
]
},
{
"parameters": {
"jsCode": "const rows = $input.all().map(i => i.json);\nconst grouped = {};\n\nfor (const row of rows) {\n const cid = row.company_id || 'UNKNOWN';\n if (!grouped[cid]) grouped[cid] = [];\n grouped[cid].push(row);\n}\n\nconst companyLabels = {\n MUS: 'Masttro US (MUS)',\n MMX: 'Masttro Mexico (MMX)',\n MSAG: 'Masttro Switzerland (MSAG)',\n MHAG: 'Masttro Switzerland Holding (MHAG)',\n MGC: 'Masttro Global (MGC)'\n};\n\nconst companyCurrency = {\n MUS: 'USD',\n MMX: 'USD',\n MGC: 'USD',\n MSAG: 'CHF',\n MHAG: 'CHF'\n};\n\nconst cleanDate = (d) => d ? String(d).split('T')[0] : '';\n\nfunction qboLink(txnType, txnId) {\n if (!txnId) return '';\n const typeMap = {\n 'Bill': 'bill',\n 'Check': 'check',\n 'Credit Card Charge': 'creditcardcharge',\n 'Credit Card Credit': 'creditcardcredit',\n 'Journal Entry': 'journal',\n 'Expense': 'expense',\n 'Purchase Order': 'purchaseorder',\n 'Vendor Credit': 'vendorcredit'\n };\n const path = typeMap[txnType];\n if (!path) return '';\n return `https://app.qbo.intuit.com/app/${path}?txnId=${encodeURIComponent(txnId)}`;\n}\n\nconst now = new Date();\nconst prevMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);\nconst currMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);\nconst fmt = d => d.toISOString().split('T')[0];\nconst dateRangeLabel = `${fmt(prevMonthStart)} – ${fmt(currMonthEnd)}`;\n\nconst output = [];\n\nfor (const [company_id, txns] of Object.entries(grouped)) {\n const label = companyLabels[company_id] || company_id;\n const currency = companyCurrency[company_id] || 'USD';\n const count = txns.length;\n const totalAmt = txns.reduce((s, r) => s + parseFloat(r.amount || 0), 0);\n\n const tableRows = txns.map(r => {\n const amt = parseFloat(r.amount || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\n const link = qboLink(r.transaction_type, r.txn_id);\n const editCell = link\n ? `<a href=\"${link}\" target=\"_blank\">Edit in QBO</a>`\n : '—';\n const dept = r.department && r.department.trim() ? r.department : '<span style=\"color:#cc0000\">Missing</span>';\n const cls = r.class && r.class.trim() ? r.class : '<span style=\"color:#cc0000\">Missing</span>';\n return `<tr>\n <td>${cleanDate(r.date)}</td>\n <td>${r.transaction_type || ''}</td>\n <td>${r.name || ''}</td>\n <td>${r.account || ''}</td>\n <td>${r.customer_name || ''}</td>\n <td>${dept}</td>\n <td>${cls}</td>\n <td style=\"text-align:right\">${amt} ${currency}</td>\n <td>${editCell}</td>\n </tr>`;\n }).join('\\n');\n\n const description = `<p><strong>${label}</strong> has <strong>${count} expense transaction(s)</strong> missing both Department and Class coding.</p>\n<p>Date range scanned: <strong>${dateRangeLabel}</strong></p>\n<p>Only transactions where <strong>both</strong> Department and Class are blank are included.</p>\n\n<table border=\"1\" cellpadding=\"4\" cellspacing=\"0\" style=\"border-collapse:collapse;font-size:12px\">\n <thead style=\"background:#f0f0f0\">\n <tr>\n <th>Date</th><th>Type</th><th>Name</th><th>Account</th><th>Customer</th><th>Department</th><th>Class</th><th style=\"text-align:right\">Amount (${currency})</th><th>Action</th>\n </tr>\n </thead>\n <tbody>\n ${tableRows}\n </tbody>\n</table>\n\n<p><em>Total uncoded expense amount: ${totalAmt.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency}</em></p>\n<p>Please click <strong>Edit in QBO</strong> on each row and update the Department and Class fields.</p>`;\n\n output.push({\n json: {\n company_id,\n subject: `[${company_id}] GL Coding Alert – ${count} Expense Transaction(s) Missing Dept & Class (${dateRangeLabel})`,\n description,\n rows: txns\n }\n });\n}\n\nreturn output;"
},
"id": "417c74cb-9e63-4020-9d1d-a1f88fcbde79",
"id": "1361c47c-52f6-46d8-94e7-f851e8676663",
"name": "⚙️ Group by Company & Build Ticket",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
608,
-1248
624,
-1440
]
},
{
@@ -364,13 +372,13 @@
"jsonBody": "={{ JSON.stringify({ subject: $json.subject, description: $json.description, email: 'mnolan@masttro.com', priority: 2, status: 2, group_id: 158000821060 }) }}",
"options": {}
},
"id": "674f9eda-2dba-472c-a9fb-313a4333d9ab",
"id": "4bbb7311-a3a7-488b-855c-1abcbabec911",
"name": "🎫 Freshdesk – Create Ticket",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
848,
-1248
864,
-1440
],
"credentials": {
"httpBasicAuth": {
@@ -383,13 +391,13 @@
"parameters": {
"jsCode": "const ticketId = String($('🎫 Freshdesk – Create Ticket').item.json.id || '');\nconst rows = $('⚙️ Group by Company & Build Ticket').item.json.rows;\n\nreturn rows.map(r => ({\n json: {\n company_id: r.company_id,\n date: String(r.date).split('T')[0],\n num: r.num || '',\n account: r.account || '',\n amount: parseFloat(r.amount || 0),\n ticket_id: ticketId\n }\n}));"
},
"id": "34d8b607-52fd-48a1-9ea9-c4725fdb58ad",
"id": "f263b80c-fce4-42d0-ab4a-1f15cd4f9f6b",
"name": "⚙️ Prep Log Rows",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1072,
-1248
1088,
-1440
]
},
{
@@ -398,13 +406,13 @@
"query": "INSERT INTO moneybot_gl_missing_dept_class_log (company_id, date, num, account, amount, ticket_id)\nVALUES (\n '{{ $json.company_id }}',\n '{{ $json.date }}',\n '{{ $json.num }}',\n '{{ $json.account }}',\n {{ $json.amount }},\n '{{ $json.ticket_id }}'\n)\nON CONFLICT (company_id, date, num, account) DO NOTHING;",
"options": {}
},
"id": "9e6ea309-4b22-4614-b270-19fa2d654ecd",
"id": "407319ec-4b69-4026-8f6e-8627aafe36c8",
"name": "🗄️ PG – Log to Tracking Table",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1296,
-1248
1312,
-1440
],
"credentials": {
"postgres": {
@@ -418,19 +426,19 @@
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 30
"field": "hours",
"triggerAtMinute": 17
}
]
}
},
"id": "7cf9aeac-0320-48df-b56e-0b6d4342a22d",
"name": "15m",
"id": "b06dfc08-18f5-4d04-97e8-bb2dcbee13db",
"name": "1h",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
-1232
16,
-1424
]
}
],
@@ -490,7 +498,7 @@
]
]
},
"15m": {
"1h": {
"main": [
[
{
@@ -504,10 +512,10 @@
},
"nodeGroups": [],
"authors": "Michael Nolan",
"name": "added txnid fix",
"name": "Version 243af477",
"description": "",
"autosaved": false,
"createdAt": "2026-08-11T17:09:07.420Z",
"updatedAt": "2026-08-11T17:09:12.541Z"
"createdAt": "2026-10-13T16:25:04.286Z",
"updatedAt": "2026-10-13T16:25:05.384Z"
}
}