{
  "name": "Financial Close & Reconciliation",
  "nodes": [
    {
      "id": "schedule_daily_close",
      "name": "Daily Close Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * *"
            }
          ]
        }
      }
    },
    {
      "id": "http_bank_transactions",
      "name": "Get Bank Transactions",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        220,
        160
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "GET",
        "url": "https://REPLACE_WITH_YOUR_BANK_FEED_API/transactions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "since",
              "value": "={{ $today.minus({ days: 1 }).toISODate() }}"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "quickbooks_get_invoices",
      "name": "Get Open Invoices",
      "type": "n8n-nodes-base.quickbooks",
      "typeVersion": 1,
      "position": [
        220,
        440
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "invoice",
        "operation": "getAll",
        "returnAll": true,
        "options": {}
      }
    },
    {
      "id": "code_match_transactions",
      "name": "Match Transactions to Invoices",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ],
      "parameters": {
        "language": "javaScript",
        "mode": "runOnceForAllItems",
        "jsCode": "const txns = $('Get Bank Transactions').all().map(i => i.json);\nconst invoices = $('Get Open Invoices').all().map(i => i.json);\nconst dayMs = 24 * 60 * 60 * 1000;\nconst out = [];\nfor (const t of txns) {\n  const amount = Math.abs(Number(t.amount ?? 0));\n  const txnDate = new Date(t.date ?? t.postedAt ?? Date.now());\n  let best = null;\n  let bestDelta = Infinity;\n  for (const inv of invoices) {\n    const invAmount = Math.abs(Number(inv.TotalAmt ?? inv.amount ?? 0));\n    const invDate = new Date(inv.TxnDate ?? inv.date ?? Date.now());\n    const amountDelta = Math.abs(invAmount - amount);\n    const daysApart = Math.abs(txnDate - invDate) / dayMs;\n    const refMatch = String(t.reference ?? '').includes(String(inv.DocNumber ?? ''));\n    if (amountDelta > invAmount * 0.02 && !refMatch) continue;\n    if (daysApart > 10) continue;\n    if (amountDelta < bestDelta) {\n      bestDelta = amountDelta;\n      best = inv;\n    }\n  }\n  out.push({\n    json: {\n      ...t,\n      amount,\n      matched: Boolean(best),\n      matchedInvoiceId: best?.Id ?? best?.id ?? null,\n      matchedInvoiceNumber: best?.DocNumber ?? null,\n      discrepancyAmount: best ? Number(bestDelta.toFixed(2)) : amount,\n    },\n  });\n}\nreturn out;"
      }
    },
    {
      "id": "if_matched",
      "name": "Matched?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        660,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "match-check-1",
              "leftValue": "={{ $json.matched }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ]
        }
      }
    },
    {
      "id": "sheets_log_reconciled",
      "name": "Log Reconciled Line",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        880,
        160
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "sheet",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_YOUR_SHEET_ID",
          "cachedResultName": "Finance Close Tracker"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Reconciliation Log",
          "cachedResultName": "Reconciliation Log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "date": "={{ $today.toISODate() }}",
            "amount": "={{ $json.amount }}",
            "invoice_number": "={{ $json.matchedInvoiceNumber }}",
            "status": "reconciled"
          }
        },
        "options": {}
      }
    },
    {
      "id": "if_discrepancy_threshold",
      "name": "Discrepancy Above Threshold?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        880,
        440
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "threshold-check-1",
              "leftValue": "={{ $json.discrepancyAmount }}",
              "rightValue": 500,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        }
      }
    },
    {
      "id": "slack_alert_bookkeeper",
      "name": "Alert Bookkeeper — Discrepancy",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.4,
      "position": [
        1100,
        380
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#finance-close",
          "cachedResultName": "finance-close"
        },
        "text": "={{ \":warning: *Unmatched transaction* — ₱\" + $json.amount + \" on \" + $today.toISODate() + \" has no clean invoice match (nearest gap: ₱\" + $json.discrepancyAmount + \"). Needs a human look before close.\" }}"
      }
    },
    {
      "id": "sheets_log_minor",
      "name": "Log Minor Discrepancy",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1100,
        540
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "sheet",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_YOUR_SHEET_ID",
          "cachedResultName": "Finance Close Tracker"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Reconciliation Log",
          "cachedResultName": "Reconciliation Log"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "date": "={{ $today.toISODate() }}",
            "amount": "={{ $json.amount }}",
            "invoice_number": "",
            "status": "review_later"
          }
        },
        "options": {}
      }
    },
    {
      "id": "code_build_summary",
      "name": "Build Close Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        300
      ],
      "parameters": {
        "language": "javaScript",
        "mode": "runOnceForAllItems",
        "jsCode": "const rows = $('Match Transactions to Invoices').all().map(i => i.json);\nconst reconciled = rows.filter(r => r.matched);\nconst flagged = rows.filter(r => !r.matched);\nconst reconciledTotal = reconciled.reduce((s, r) => s + r.amount, 0);\nconst flaggedTotal = flagged.reduce((s, r) => s + r.amount, 0);\nreturn [{\n  json: {\n    date: new Date().toISOString().slice(0, 10),\n    reconciledCount: reconciled.length,\n    reconciledTotal: reconciledTotal.toFixed(2),\n    flaggedCount: flagged.length,\n    flaggedTotal: flaggedTotal.toFixed(2),\n  },\n}];"
      }
    },
    {
      "id": "gmail_send_summary",
      "name": "Send Reconciliation Summary",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        1540,
        300
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "bookkeeper@REPLACE_WITH_YOUR_DOMAIN.com",
        "subject": "={{ \"Close summary — \" + $json.date }}",
        "emailType": "text",
        "message": "={{ \"Reconciled: \" + $json.reconciledCount + \" transactions (₱\" + $json.reconciledTotal + \")\\nFlagged: \" + $json.flaggedCount + \" transactions (₱\" + $json.flaggedTotal + \") — needs review before books close.\" }}",
        "options": {}
      }
    }
  ],
  "connections": {
    "Daily Close Trigger": {
      "main": [
        [
          {
            "node": "Get Bank Transactions",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Open Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Bank Transactions": {
      "main": [
        [
          {
            "node": "Match Transactions to Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Open Invoices": {
      "main": [
        [
          {
            "node": "Match Transactions to Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match Transactions to Invoices": {
      "main": [
        [
          {
            "node": "Matched?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Matched?": {
      "main": [
        [
          {
            "node": "Log Reconciled Line",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Discrepancy Above Threshold?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Discrepancy Above Threshold?": {
      "main": [
        [
          {
            "node": "Alert Bookkeeper — Discrepancy",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Minor Discrepancy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Reconciled Line": {
      "main": [
        [
          {
            "node": "Build Close Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Bookkeeper — Discrepancy": {
      "main": [
        [
          {
            "node": "Build Close Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Minor Discrepancy": {
      "main": [
        [
          {
            "node": "Build Close Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Close Summary": {
      "main": [
        [
          {
            "node": "Send Reconciliation Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}