{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "3bf367ea-63dc-4547-aec6-5b7fd658608c",
      "name": "\ud83d\udccb Flow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -272,
        16
      ],
      "parameters": {
        "width": 500,
        "height": 374,
        "content": "## \ud83d\udcca WhatsApp Expense Tracker \u2013 AI Receipt Scanner\n\n**How it works:**\n1. User sends a receipt photo (or text command) on WhatsApp via WATI\n2. n8n receives the webhook, detects if it's an image or text\n3. If image \u2192 OpenAI Vision extracts vendor, amount, date, category\n4. Extracted data is appended to Google Sheets\n5. If user sends `report` \u2192 monthly summary is pulled from Sheets and sent back\n\n**Setup needed:** WATI credentials, OpenAI API key, Google Sheets OAuth"
      },
      "typeVersion": 1
    },
    {
      "id": "ab271019-dd04-4b53-b6ab-78b8025ca94a",
      "name": "Sticky \u2013 Receive & Route",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        272,
        400
      ],
      "parameters": {
        "color": 7,
        "width": 548,
        "height": 484,
        "content": "### 1\ufe0f\u20e3 Webhook + Route\n**WATI Trigger** receives incoming WhatsApp messages.\n**Switch** checks: image \u2192 receipt scan | `report` text \u2192 monthly report | anything else \u2192 help message."
      },
      "typeVersion": 1
    },
    {
      "id": "0f4b6bab-89b4-4d75-957b-d767139da255",
      "name": "Sticky \u2013 AI Scan",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 808,
        "height": 420,
        "content": "### 2\ufe0f\u20e3 Download & Scan Receipt\n**Get a media file** downloads the receipt image binary from WATI.\n**Prepare Image Code** converts the binary to a base64 data URL so OpenAI Vision can read it.\n**OpenAI GPT-4o** analyses the image and returns structured JSON."
      },
      "typeVersion": 1
    },
    {
      "id": "a81dbf8b-d435-4327-a6ff-2772794b8693",
      "name": "Sticky \u2013 Log & Confirm",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1760,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 404,
        "height": 416,
        "content": "### 3\ufe0f\u20e3 Log to Sheets + Confirm\n**Google Sheets \u2013 Append** logs the row: timestamp, phone, vendor, amount, currency, date, category, description, month.\n**WATI \u2013 Confirm** sends a session message back to the user with the logged expense summary."
      },
      "typeVersion": 1
    },
    {
      "id": "7cdee34e-1fc5-4f2c-8ca3-c62fa4f5a28f",
      "name": "Sticky \u2013 Report",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        784
      ],
      "parameters": {
        "color": 7,
        "width": 724,
        "height": 283,
        "content": "### 4\ufe0f\u20e3 Monthly Report\n**Google Sheets \u2013 Read** fetches ALL rows, filtered in the Code node by current month.\n**Build Report Code** aggregates totals per category with visual % bars.\n**WATI \u2013 Send Report** replies with the formatted breakdown."
      },
      "typeVersion": 1
    },
    {
      "id": "01fbe3f8-b96b-4ee0-bcf2-1b867556711f",
      "name": "Route Message",
      "type": "n8n-nodes-base.switch",
      "position": [
        528,
        640
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Image Receipt",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "2960fec3-8599-4a2d-a77c-a9204a6a9fad",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.type }}",
                    "rightValue": "image"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Monthly Report",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "ed5d4131-7461-4dbb-8ffa-6c70749a42c3",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.text }}",
                    "rightValue": "={{ $json.text }}"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "88893515-d634-49d0-b457-f8b230a08a7e",
      "name": "Parse & Validate Expense",
      "type": "n8n-nodes-base.code",
      "position": [
        1520,
        528
      ],
      "parameters": {
        "jsCode": "// 1. Get the raw text content from the OpenAI extraction node\n// Note: We access .choices[0].message.content because it's a Chat Completion\nconst rawContent = $input.first().json.choices[0].message.content\nconst parsed = JSON.parse(rawContent);\n\n// 2. Reference the correct Trigger name (Wati Trigger1)\nconst triggerNode = $('Wati Trigger').first();\nconst phone = triggerNode.json.waId || triggerNode.json.from || 'unknown';\nconst senderName = triggerNode.json.senderName || 'User';\n\n// 3. Create the expense object\nconst now = new Date();\nconst expenseRow = {\n  timestamp: now.toISOString(),\n  phone: phone,\n  senderName: senderName,\n  vendor: parsed.vendor || parsed.merchant || 'Unknown Vendor',\n  amount: parsed.amount || parsed.total || 0,\n  currency: parsed.currency || 'INR',\n  date: parsed.date || now.toISOString().split('T')[0],\n  category: parsed.category || 'Other',\n  description: parsed.description || ''\n};\n\nreturn { json: expenseRow };"
      },
      "typeVersion": 2
    },
    {
      "id": "411fe9db-8c1f-4319-bd11-d93161178880",
      "name": "Google Sheets \u2013 Log Expense",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1808,
        528
      ],
      "parameters": {
        "columns": {
          "value": {
            "date ": "={{ $json.date }}",
            "month": "={{ DateTime.fromISO($json.date).monthLong }}",
            "phone": "={{ $json.phone }}",
            "amount": "={{ $json.amount }}",
            "vendor": "={{ $json.vendor }}",
            "category": "={{ $json.category }}",
            "currency": "={{ $json.currency }}",
            "timestamp": "={{ $json.timestamp }}",
            "description": "={{ $json.description }}"
          },
          "schema": [
            {
              "id": "timestamp",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "vendor",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "vendor",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "amount",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "amount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "currency",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "currency",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "date ",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "date ",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "category",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "month",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "month",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IgLWByzBp46IeTnxt_5I4jpmJh31Z0TIh7gBpoQjCSQ/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1IgLWByzBp46IeTnxt_5I4jpmJh31Z0TIh7gBpoQjCSQ",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IgLWByzBp46IeTnxt_5I4jpmJh31Z0TIh7gBpoQjCSQ/edit?usp=drivesdk",
          "cachedResultName": "wati - expense"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "5a3cccd1-35bd-4851-a1bc-b8051261c0ab",
      "name": "Google Sheets \u2013 Read This Month",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1008,
        912
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IgLWByzBp46IeTnxt_5I4jpmJh31Z0TIh7gBpoQjCSQ/edit#gid=0",
          "cachedResultName": "Expenses"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1IgLWByzBp46IeTnxt_5I4jpmJh31Z0TIh7gBpoQjCSQ",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IgLWByzBp46IeTnxt_5I4jpmJh31Z0TIh7gBpoQjCSQ/edit?usp=drivesdk",
          "cachedResultName": "wati - expense"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "8a7e4899-b4f3-4f17-87b1-6d9d59f8a116",
      "name": "Build Monthly Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        912
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 Build Monthly Report \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Reads all rows, filters to current month, aggregates by category.\n\nconst allRows = $input.all();\nconst now = new Date();\nconst currentMonth = now.toISOString().substring(0, 7); // YYYY-MM\nconst callerPhone = $('Wati Trigger').first().json.waId\n\n// Filter only this month & this user's expenses\nconst rows = allRows.filter(r => {\n  const rowMonth = r.json.month || (r.json.timestamp || '').substring(0, 7);\n  const rowPhone = r.json.phone || '';\n  return rowMonth === currentMonth && rowPhone === callerPhone;\n});\n\nconst monthLabel = now.toLocaleString('default', { month: 'long', year: 'numeric' });\n\nif (!rows || rows.length === 0) {\n  return [{ json: { reportText: `\ud83d\udcca No expenses recorded for *${monthLabel}* yet.\\n\\nSnap a receipt photo to start tracking!` } }];\n}\n\n// Aggregate by category\nconst categoryTotals = {};\nlet grandTotal = 0;\nlet currency = 'USD';\n\nfor (const row of rows) {\n  const cat = row.json.category || 'Other';\n  const amt = parseFloat(row.json.amount) || 0;\n  currency = row.json.currency || currency;\n  categoryTotals[cat] = (categoryTotals[cat] || 0) + amt;\n  grandTotal += amt;\n}\n\n// Build formatted WhatsApp message with visual bars\nconst lines = [`\ud83d\udcca *Monthly Expense Report*`, `\ud83d\udcc5 *${monthLabel}*`, ''];\n\nconst sorted = Object.entries(categoryTotals).sort((a, b) => b[1] - a[1]);\nfor (const [cat, total] of sorted) {\n  const pct = grandTotal > 0 ? ((total / grandTotal) * 100) : 0;\n  const filledBars = Math.round(pct / 10);\n  const bar = '\u2588'.repeat(filledBars) + '\u2591'.repeat(10 - filledBars);\n  lines.push(`${bar} *${cat}*`);\n  lines.push(`  ${currency} ${total.toFixed(2)}  (${pct.toFixed(1)}%)`);\n  lines.push('');\n}\n\nlines.push(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);\nlines.push(`\ud83d\udcb0 *Total Spent: ${currency} ${grandTotal.toFixed(2)}*`);\nlines.push(`\ud83e\uddfe *Receipts logged: ${rows.length}*`);\nlines.push('');\nlines.push('Snap a receipt photo to log a new expense!');\n\nreturn [{ json: { reportText: lines.join('\\n'), grandTotal, currency, count: rows.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "47eef0fa-7eb6-495b-8ba4-d78bf81c92ff",
      "name": "Prepare Image for OpenAI1",
      "type": "n8n-nodes-base.code",
      "position": [
        1136,
        528
      ],
      "parameters": {
        "jsCode": "// Get the item and its binary data reference\nconst item = $input.first();\nconst binaryPropertyName = 'data'; // Ensure this matches your binary field name\nconst binaryData = item.binary?.[binaryPropertyName];\n\nif (!binaryData) {\n  throw new Error(`No binary data found in property \"${binaryPropertyName}\".`);\n}\n\n// Retrieve the actual file buffer from n8n's storage\nconst buffer = await this.helpers.getBinaryDataBuffer(0, binaryPropertyName);\n\n// Convert that buffer into a real Base64 string\nconst base64String = buffer.toString('base64');\nconst mimeType = binaryData.mimeType || 'image/jpeg';\n\nreturn {\n  json: {\n    mimeType: mimeType,\n    // This will now contain the ACTUAL image code, not \"filesystem-v2\"\n    dataUrl: `data:${mimeType};base64,${base64String}`\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "de8d0d89-cccb-4599-afff-ddb08b4f261b",
      "name": "OpenAI \u2013 Extract Receipt Data2",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1312,
        528
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"model\": \"gpt-4o\",\n  \"max_tokens\": 600,\n  \"temperature\": 0.1,\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": [\n        {\n          \"type\": \"text\",\n          \"text\": \"You are a receipt scanning assistant. Extract the following details from this receipt image and return ONLY a raw JSON object \u2014 no explanation, no markdown, no code fences. Just the JSON.\\n\\n{\\n  \\\"vendor\\\": \\\"name of the shop, restaurant or business\\\",\\n  \\\"amount\\\": total final amount paid as a number only (no currency symbol),\\n  \\\"currency\\\": \\\"3-letter ISO currency code based on the country e.g. INR, USD, EUR, GBP\\\",\\n  \\\"date\\\": \\\"date on the receipt in YYYY-MM-DD format\\\",\\n  \\\"category\\\": \\\"pick exactly one from: Food, Transport, Utilities, Shopping, Healthcare, Entertainment, Other\\\",\\n  \\\"description\\\": \\\"one short sentence summarising what was purchased\\\"\\n}\\n\\nRules:\\n- amount must be a number, not a string\\n- If any field is not visible or unclear, set it to null\\n- Never add extra fields\\n- Never wrap in markdown or code blocks\\n- Return only the JSON, nothing else\"\n        },\n        {\n          \"type\": \"image_url\",\n          \"image_url\": {\n            \"url\": \"{{ $json.dataUrl }}\",\n            \"detail\": \"high\"\n          }\n        }\n      ]\n    }\n  ]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        },
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "6387db9f-7a4d-403a-9017-761e6dd4b5a2",
      "name": "Wati Trigger",
      "type": "n8n-nodes-wati.watiTrigger",
      "position": [
        336,
        656
      ],
      "parameters": {
        "event": "messageReceived"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "f920680d-354a-41e5-871a-d8eee6847aeb",
      "name": "Get a media file1",
      "type": "n8n-nodes-wati.wati",
      "position": [
        912,
        528
      ],
      "parameters": {
        "operation": "getMedia",
        "mediaFileUrl": "={{ $json.data }}"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "60f3a0b8-afad-445b-bdd6-7000bef1e452",
      "name": "Send Expenditure message",
      "type": "n8n-nodes-wati.wati",
      "position": [
        2016,
        528
      ],
      "parameters": {
        "target": "={{ $('Wati Trigger').item.json.waId }}",
        "messageText": "=\u2705 *Expense Logged Successfully!*\n\n\ud83c\udfea *Vendor:* {{ $('Parse & Validate Expense').item.json.vendor }}\n\ud83d\udcb0 *Amount:* {{ $('Parse & Validate Expense').item.json.currency }} {{ $('Parse & Validate Expense').item.json.amount }}\n\ud83d\uddc2\ufe0f *Category:* {{ $('Parse & Validate Expense').item.json.category }}\n\ud83d\udcc5 *Date:* {{ $('Parse & Validate Expense').item.json.date }}\n\ud83d\udcdd *Details:* {{ $('Parse & Validate Expense').item.json.description }}\n\nYour expense has been saved! \nType *report* to view your monthly summary \ud83d\udcca"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f7e8beab-a370-420a-af95-b03d0a044c3a",
      "name": "Send Expenditure Report - month",
      "type": "n8n-nodes-wati.wati",
      "position": [
        1424,
        912
      ],
      "parameters": {
        "target": "={{ $('Wati Trigger').item.json.waId }}",
        "messageText": "={{ $('Build Monthly Report').item.json.reportText }}"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Wati Trigger": {
      "main": [
        [
          {
            "node": "Route Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Message": {
      "main": [
        [
          {
            "node": "Get a media file1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Google Sheets \u2013 Read This Month",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get a media file1": {
      "main": [
        [
          {
            "node": "Prepare Image for OpenAI1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Monthly Report": {
      "main": [
        [
          {
            "node": "Send Expenditure Report - month",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Validate Expense": {
      "main": [
        [
          {
            "node": "Google Sheets \u2013 Log Expense",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Image for OpenAI1": {
      "main": [
        [
          {
            "node": "OpenAI \u2013 Extract Receipt Data2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2013 Log Expense": {
      "main": [
        [
          {
            "node": "Send Expenditure message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI \u2013 Extract Receipt Data2": {
      "main": [
        [
          {
            "node": "Parse & Validate Expense",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2013 Read This Month": {
      "main": [
        [
          {
            "node": "Build Monthly Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}