{
  "id": "47w19P9mnpVZCSQM",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Telegram Expense Bot",
  "tags": [
    {
      "id": "3TcRK7VvRh8uGaqn",
      "name": "data extraction",
      "createdAt": "2025-12-12T13:08:01.658Z",
      "updatedAt": "2025-12-12T13:08:01.658Z"
    },
    {
      "id": "44ju0IhUpN4VKVFN",
      "name": "business expenses",
      "createdAt": "2025-12-12T13:08:20.394Z",
      "updatedAt": "2025-12-12T13:08:20.394Z"
    },
    {
      "id": "7NMIXD2GpRhg9uI4",
      "name": "reimbursement",
      "createdAt": "2025-12-12T13:08:30.332Z",
      "updatedAt": "2025-12-12T13:08:30.332Z"
    },
    {
      "id": "BWEb3Zh41hhFbcSQ",
      "name": "accounting",
      "createdAt": "2025-12-12T13:07:10.834Z",
      "updatedAt": "2025-12-12T13:07:10.834Z"
    },
    {
      "id": "Gp5jEbBt7L8fk85J",
      "name": "telegram",
      "createdAt": "2025-12-12T13:04:24.221Z",
      "updatedAt": "2025-12-12T13:04:24.221Z"
    },
    {
      "id": "Jlt5gSS4xbm1bcc7",
      "name": "expense management",
      "createdAt": "2025-12-12T13:08:12.395Z",
      "updatedAt": "2025-12-12T13:08:12.395Z"
    },
    {
      "id": "K1by8qOvXnUKE92d",
      "name": "easybits",
      "createdAt": "2025-12-12T13:05:18.235Z",
      "updatedAt": "2025-12-12T13:05:18.235Z"
    },
    {
      "id": "MYII9Z9xxh8ho964",
      "name": "freelancer",
      "createdAt": "2025-12-12T13:07:23.560Z",
      "updatedAt": "2025-12-12T13:07:23.560Z"
    },
    {
      "id": "N3qsTbsX1wYt8YiW",
      "name": "small business",
      "createdAt": "2025-12-12T13:07:16.570Z",
      "updatedAt": "2025-12-12T13:07:16.570Z"
    },
    {
      "id": "N6Py918DfttTm4zI",
      "name": "expense report",
      "createdAt": "2025-12-12T13:07:30.932Z",
      "updatedAt": "2025-12-12T13:07:30.932Z"
    },
    {
      "id": "NtVS0Bdsetvv87PO",
      "name": "document extraction",
      "createdAt": "2025-12-12T13:05:11.248Z",
      "updatedAt": "2025-12-12T13:05:11.248Z"
    },
    {
      "id": "Q3pztLS35bUfHuO9",
      "name": "expense tracking",
      "createdAt": "2025-12-12T13:04:49.723Z",
      "updatedAt": "2025-12-12T13:04:49.723Z"
    },
    {
      "id": "VVxtrVDV8D5mtbnj",
      "name": "PDF",
      "createdAt": "2025-12-12T13:05:36.841Z",
      "updatedAt": "2025-12-12T13:05:36.841Z"
    },
    {
      "id": "Vz8fit7ZzjHDr8TF",
      "name": "image processing",
      "createdAt": "2025-12-12T13:07:51.740Z",
      "updatedAt": "2025-12-12T13:07:51.740Z"
    },
    {
      "id": "YLjRpJfayCMYZn1L",
      "name": "AI",
      "createdAt": "2025-12-12T13:08:43.344Z",
      "updatedAt": "2025-12-12T13:08:43.344Z"
    },
    {
      "id": "pRir2mkxRNXcCpOi",
      "name": "receipts",
      "createdAt": "2025-12-12T13:04:57.030Z",
      "updatedAt": "2025-12-12T13:04:57.030Z"
    },
    {
      "id": "rJdMMvYpxc2rrL01",
      "name": "OCR",
      "createdAt": "2025-12-12T13:05:30.483Z",
      "updatedAt": "2025-12-12T13:05:30.483Z"
    },
    {
      "id": "t2sdzhTGcJHJ7zI7",
      "name": "bookkeeping",
      "createdAt": "2025-12-12T13:07:04.425Z",
      "updatedAt": "2025-12-12T13:07:04.425Z"
    },
    {
      "id": "wFqaBAd3SntqSjps",
      "name": "receipt scanner",
      "createdAt": "2025-12-12T13:07:38.066Z",
      "updatedAt": "2025-12-12T13:07:38.066Z"
    },
    {
      "id": "yOJrszL9zkHtqDFF",
      "name": "finance",
      "createdAt": "2025-12-12T13:05:45.164Z",
      "updatedAt": "2025-12-12T13:05:45.164Z"
    },
    {
      "id": "z5OOhm9sd14YcTAA",
      "name": "invoice",
      "createdAt": "2025-12-12T13:06:57.017Z",
      "updatedAt": "2025-12-12T13:06:57.017Z"
    }
  ],
  "nodes": [
    {
      "id": "040fbbb6-f170-483a-9ba8-476bfbbe10d0",
      "name": "Telegram Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1808,
        448
      ],
      "parameters": {
        "path": "expense-bot",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "27ee3948-7070-48ab-9357-104c0d82cbc0",
      "name": "Get File Info",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1360,
        352
      ],
      "parameters": {
        "url": "=https://api.telegram.org/bot8228247643:AAFzsEi2bJiiNkYRxAXRRIbFmCfCClGM-Uk/getFile",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ file_id: $json.body.message.photo ? $json.body.message.photo.slice(-1)[0].file_id : $json.body.message.document.file_id }) }}\n\n",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "afea0a8f-e7a4-41fe-9bbd-12bacba09b38",
      "name": "Download Photo",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1136,
        352
      ],
      "parameters": {
        "url": "=https://api.telegram.org/file/bot8228247643:AAFzsEi2bJiiNkYRxAXRRIbFmCfCClGM-Uk/{{ $json.result.file_path }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "201f1b32-40a7-473d-85cf-be7167204fde",
      "name": "Convert to Base64",
      "type": "n8n-nodes-base.code",
      "position": [
        -912,
        352
      ],
      "parameters": {
        "jsCode": "// Debug: Check what binary data we have\nconst inputData = $input.first();\nconst binaryKeys = Object.keys(inputData.binary || {});\n\n// Get the first binary key (might not be 'data')\nconst binaryKey = binaryKeys[0] || 'data';\nconst binaryData = inputData.binary[binaryKey];\n\nconst base64 = binaryData.data;\n\n// Detect mime type from the original message\nconst telegramMessage = $('Telegram Webhook').first().json.body.message;\nlet mimeType = 'image/jpeg';\n\nif (telegramMessage.document) {\n  // It's a document (PDF)\n  mimeType = telegramMessage.document.mime_type || 'application/pdf';\n} else if (telegramMessage.photo) {\n  // It's a photo\n  mimeType = 'image/jpeg';\n}\n\nconst dataUri = `data:${mimeType};base64,${base64}`;\n\n// Get chat ID from original webhook\nconst chatId = $('Telegram Webhook').first().json.body.message.chat.id;\n\nreturn {\n  json: {\n    dataUri: dataUri,\n    chatId: chatId,\n    mimeType: mimeType,\n    debug_binaryKeys: binaryKeys,\n    debug_base64_length: base64 ? base64.length : 0\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "3258074d-7b36-4211-a241-56c814114f38",
      "name": "Extract with easybits",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -688,
        352
      ],
      "parameters": {
        "url": "https://extractor.easybits.tech/api/pipelines/dYI3r9xfHldQBlei34Pe",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ files: [\"https://api.telegram.org/file/bot8228247643:AAFzsEi2bJiiNkYRxAXRRIbFmCfCClGM-Uk/\" + $('Get File Info').first().json.result.file_path] }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "4314ccd1-3b8a-4e7a-9637-da2cfa7460eb",
      "name": "Process Extraction",
      "type": "n8n-nodes-base.code",
      "position": [
        -464,
        352
      ],
      "parameters": {
        "jsCode": "const extractionResult = $input.first().json;\nconst chatId = $('Convert to Base64').first().json.chatId;\nconst telegramMessage = $('Telegram Webhook').first().json.body.message;\nconst caption = telegramMessage.caption || '';\n\nlet receiptData;\nif (extractionResult.data && extractionResult.data.receipt_data) {\n  receiptData = extractionResult.data.receipt_data;\n} else if (extractionResult.receipt_data) {\n  receiptData = extractionResult.receipt_data;\n} else {\n  receiptData = extractionResult.data || extractionResult;\n}\n\nlet category = receiptData.category || 'Other';\nconst vendorName = receiptData.vendor_name || 'Unknown';\nconst totalAmount = parseFloat(receiptData.total_amount) || 0;\nconst currency = receiptData.currency || 'EUR';\nconst transactionDate = receiptData.transaction_date || new Date().toLocaleDateString('de-DE');\nconst confidence = receiptData.extraction_confidence || 'medium';\n\nconst validCategories = [\n  'Restaurant',\n  'Transportation', \n  'Mobile Phone',\n  'Office Supplies',\n  'Accommodation',\n  'Marketing',\n  'Software',\n  'Equipment'\n];\n\nlet captionCategory = '';\nlet captionDetails = '';\n\nif (caption.trim()) {\n  const captionTrimmed = caption.trim();\n  \n  if (captionTrimmed.includes(' - ')) {\n    const parts = captionTrimmed.split(' - ');\n    const possibleCategory = parts[0].trim();\n    const possibleDetails = parts.slice(1).join(' - ').trim();\n    \n    for (const validCat of validCategories) {\n      if (possibleCategory.toLowerCase() === validCat.toLowerCase()) {\n        captionCategory = validCat;\n        captionDetails = possibleDetails;\n        break;\n      }\n    }\n    \n    if (!captionCategory) {\n      captionDetails = captionTrimmed;\n    }\n  } else {\n    for (const validCat of validCategories) {\n      if (captionTrimmed.toLowerCase() === validCat.toLowerCase()) {\n        captionCategory = validCat;\n        break;\n      }\n    }\n    \n    if (!captionCategory) {\n      captionDetails = captionTrimmed;\n    }\n  }\n}\n\nif (captionCategory) {\n  category = captionCategory;\n}\n\nlet eurRate = 1;\nif (category === 'Mobile Phone') {\n  eurRate = 0.8;\n}\nconst eurAmount = totalAmount * eurRate;\n\nlet monthName = 'December';\nlet year = '2025';\ntry {\n  const dateParts = transactionDate.split('.');\n  if (dateParts.length === 3) {\n    const monthNum = parseInt(dateParts[1]);\n    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];\n    monthName = monthNames[monthNum - 1] || 'December';\n    year = dateParts[2];\n  }\n} catch (e) {}\n\nconst sheetName = monthName + '_' + year;\n\nlet details = '';\nif (captionDetails) {\n  details = captionDetails;\n} else if (captionCategory) {\n  details = 'Business Expense';\n} else if (category === 'Restaurant') {\n  details = 'Business Meal';\n} else if (category === 'Transportation') {\n  details = 'Business Travel';\n} else if (category === 'Mobile Phone') {\n  details = 'Phone Bill';\n} else if (category !== 'Other') {\n  details = 'Business Expense';\n} else {\n  details = 'Other';\n}\n\nconst needsCategoryInput = (category === 'Other');\n\nreturn {\n  json: {\n    category: category,\n    vendor_name: vendorName,\n    total_amount: totalAmount,\n    currency: currency,\n    transaction_date: transactionDate,\n    confidence: confidence,\n    eur_rate: eurRate,\n    eur_amount: eurAmount,\n    month_name: monthName,\n    year: year,\n    sheet_name: sheetName,\n    details: details,\n    chat_id: chatId,\n    needs_category_input: needsCategoryInput\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "ec2f93d9-128c-4b26-84db-fd7575ce151e",
      "name": "Check Sheet Exists",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        304,
        208
      ],
      "parameters": {
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/1EE-zN63AvO42mRnoLWi8aqRncky7wQjdTj4wOaVB0HI/values/'{{ $('Process Extraction').item.json.sheet_name }}'!A1",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api"
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "9cbcfda6-d34a-4b8c-bdfe-ed3233c3682b",
      "name": "Sheet Exists?",
      "type": "n8n-nodes-base.if",
      "position": [
        480,
        208
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.values ? 'yes' : 'no' }}",
              "rightValue": "no"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "e5c66f0a-d74a-464b-84ce-122c721e0bd8",
      "name": "Create Sheet",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        624,
        208
      ],
      "parameters": {
        "url": "https://sheets.googleapis.com/v4/spreadsheets/1EE-YOUR_AWS_SECRET_KEY_HERE:batchUpdate",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"requests\": [\n    {\n      \"addSheet\": {\n        \"properties\": {\n          \"title\": \"{{ $('Process Extraction').item.json.sheet_name }}\"\n        }\n      }\n    }\n  ]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api"
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "7d6aeb48-6de4-4391-bb29-ad6ed31186bd",
      "name": "Setup Headers",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        960,
        208
      ],
      "parameters": {
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/1EE-zN63AvO42mRnoLWi8aqRncky7wQjdTj4wOaVB0HI:batchUpdate",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.requestBody }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api"
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "6c897a3c-4e65-4bf8-8cdc-c372fd008aef",
      "name": "Add Expense Row",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1344,
        416
      ],
      "parameters": {
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/1EE-zN63AvO42mRnoLWi8aqRncky7wQjdTj4wOaVB0HI/values/{{ $('Process Extraction').item.json.sheet_name }}!A4:G4:append?valueInputOption=USER_ENTERED&insertDataOption=OVERWRITE",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.expenseBody }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api"
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "1012e563-289c-4839-ad1c-910eacfd19e1",
      "name": "Send Confirmation",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1984,
        416
      ],
      "parameters": {
        "url": "https://api.telegram.org/bot8228247643:AAFzsEi2bJiiNkYRxAXRRIbFmCfCClGM-Uk/sendMessage",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"chat_id\": {{ $('Process Extraction').item.json.chat_id }},\n  \"text\": \"\u2705 Receipt Added!\\n\\n\ud83d\udcc2 Category: {{ $('Process Extraction').item.json.category }}\\n\ud83c\udfea Vendor: {{ $('Process Extraction').item.json.vendor_name }}\\n\ud83d\udcb0 Amount: \u20ac{{ $('Process Extraction').item.json.total_amount }}\\n\ud83d\udcc5 Date: {{ $('Process Extraction').item.json.transaction_date }}\\n\ud83d\udcca Coverage: {{ $('Process Extraction').item.json.eur_rate === 1 ? '100%' : '80%' }}\\n\ud83d\udcb6 EUR Value: \u20ac{{ $('Process Extraction').item.json.eur_amount.toFixed(2) }}\\n\\n\ud83d\udcdd Added to: {{ $('Process Extraction').item.json.sheet_name }}\",\n  \"parse_mode\": \"HTML\"\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "8799ca68-52c7-4719-a8a5-c9ed2ec7d815",
      "name": "No Photo Message",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1360,
        544
      ],
      "parameters": {
        "url": "https://api.telegram.org/bot8228247643:AAFzsEi2bJiiNkYRxAXRRIbFmCfCClGM-Uk/sendMessage",
        "method": "POST",
        "options": {},
        "jsonBody": "={\"chat_id\": 413057682, \"text\": \"Please send a photo of your receipt. I can only process images.\"}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "3c286bd2-3993-4128-bb17-4b1c6ee9316a",
      "name": "Respond OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2320,
        352
      ],
      "parameters": {
        "options": {},
        "respondWith": "text",
        "responseBody": "OK"
      },
      "typeVersion": 1.1
    },
    {
      "id": "afd5b5fc-ba86-454b-8429-a24a16a14d47",
      "name": "Respond OK 2",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -1136,
        544
      ],
      "parameters": {
        "options": {},
        "respondWith": "text",
        "responseBody": "OK"
      },
      "typeVersion": 1.1
    },
    {
      "id": "760d18d6-5605-4345-9d5d-91a231dfba25",
      "name": "Build Headers Request",
      "type": "n8n-nodes-base.code",
      "position": [
        800,
        208
      ],
      "parameters": {
        "jsCode": "const monthName = $('Process Extraction').item.json.month_name;\nconst year = $('Process Extraction').item.json.year;\n\n// Get the sheetId from the Create Sheet response\nconst createSheetResponse = $('Create Sheet').item.json;\nconst sheetId = createSheetResponse.replies?.[0]?.addSheet?.properties?.sheetId || 0;\n\nreturn {\n  json: {\n    requestBody: {\n      requests: [\n        // Row 1: Expense Report header (orange background, white text)\n        {\n          updateCells: {\n            rows: [\n              {\n                values: [\n                  { userEnteredValue: { stringValue: \"Expense Report\" }, userEnteredFormat: { textFormat: { bold: true, fontSize: 14, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: \"Month:\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: monthName }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: \"Year:\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { numberValue: parseInt(year) }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } }\n                ]\n              },\n              // Row 2: Empty row (white background)\n              {\n                values: [\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 1, green: 1, blue: 1 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 1, green: 1, blue: 1 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 1, green: 1, blue: 1 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 1, green: 1, blue: 1 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 1, green: 1, blue: 1 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 1, green: 1, blue: 1 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 1, green: 1, blue: 1 } } }\n                ]\n              },\n              // Row 3: Column headers (orange background, white text)\n              {\n                values: [\n                  { userEnteredValue: { stringValue: \"Date\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 }, horizontalAlignment: \"CENTER\" } },\n                  { userEnteredValue: { stringValue: \"Category\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 }, horizontalAlignment: \"CENTER\" } },\n                  { userEnteredValue: { stringValue: \"Vendor\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 }, horizontalAlignment: \"CENTER\" } },\n                  { userEnteredValue: { stringValue: \"Details\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 }, horizontalAlignment: \"CENTER\" } },\n                  { userEnteredValue: { stringValue: \"Amount\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 }, horizontalAlignment: \"CENTER\" } },\n                  { userEnteredValue: { stringValue: \"Currency\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 }, horizontalAlignment: \"CENTER\" } },\n                  { userEnteredValue: { stringValue: \"EUR Amount\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 }, horizontalAlignment: \"CENTER\" } }\n                ]\n              }\n            ],\n            fields: \"userEnteredValue,userEnteredFormat\",\n            start: { sheetId: sheetId, rowIndex: 0, columnIndex: 0 }\n          }\n        },\n        // Pre-format data rows 4-49 with white background and number format for columns E and G\n        {\n          repeatCell: {\n            range: {\n              sheetId: sheetId,\n              startRowIndex: 3,\n              endRowIndex: 49,\n              startColumnIndex: 0,\n              endColumnIndex: 7\n            },\n            cell: {\n              userEnteredFormat: {\n                backgroundColor: { red: 1, green: 1, blue: 1 },\n                wrapStrategy: \"WRAP\"\n              }\n            },\n            fields: \"userEnteredFormat.backgroundColor,userEnteredFormat.wrapStrategy\"\n          }\n        },\n        // Set number format for Amount column (E)\n        {\n          repeatCell: {\n            range: {\n              sheetId: sheetId,\n              startRowIndex: 3,\n              endRowIndex: 49,\n              startColumnIndex: 4,\n              endColumnIndex: 5\n            },\n            cell: {\n              userEnteredFormat: {\n                numberFormat: { type: \"NUMBER\", pattern: \"#,##0.00\" }\n              }\n            },\n            fields: \"userEnteredFormat.numberFormat\"\n          }\n        },\n        // Set number format for EUR Amount column (G)\n        {\n          repeatCell: {\n            range: {\n              sheetId: sheetId,\n              startRowIndex: 3,\n              endRowIndex: 49,\n              startColumnIndex: 6,\n              endColumnIndex: 7\n            },\n            cell: {\n              userEnteredFormat: {\n                numberFormat: { type: \"NUMBER\", pattern: \"#,##0.00\" }\n              }\n            },\n            fields: \"userEnteredFormat.numberFormat\"\n          }\n        },\n        // Row 50: TOTAL row (orange background, white text, with SUM formula)\n        {\n          updateCells: {\n            rows: [\n              {\n                values: [\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: \"\" }, userEnteredFormat: { backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 } } },\n                  { userEnteredValue: { stringValue: \"TOTAL:\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 }, horizontalAlignment: \"RIGHT\" } },\n                  { userEnteredValue: { formulaValue: \"=SUM(G4:G49)\" }, userEnteredFormat: { textFormat: { bold: true, foregroundColor: { red: 1, green: 1, blue: 1 } }, backgroundColor: { red: 0.82, green: 0.29, blue: 0.15 }, numberFormat: { type: \"CURRENCY\", pattern: \"\u20ac#,##0.00\" } } }\n                ]\n              }\n            ],\n            fields: \"userEnteredValue,userEnteredFormat\",\n            start: { sheetId: sheetId, rowIndex: 49, columnIndex: 0 }\n          }\n        },\n        // Set column widths\n        {\n          updateDimensionProperties: {\n            range: { sheetId: sheetId, dimension: \"COLUMNS\", startIndex: 0, endIndex: 7 },\n            properties: { pixelSize: 120 },\n            fields: \"pixelSize\"\n          }\n        },\n        // Make Details column wider\n        {\n          updateDimensionProperties: {\n            range: { sheetId: sheetId, dimension: \"COLUMNS\", startIndex: 3, endIndex: 4 },\n            properties: { pixelSize: 180 },\n            fields: \"pixelSize\"\n          }\n        }\n      ]\n    },\n    sheetId: sheetId\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "43e43d79-146a-4a7c-be6e-04f7533d08e4",
      "name": "Build Expense Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1184,
        416
      ],
      "parameters": {
        "jsCode": "const data = $('Process Extraction').item.json;\n\n// Parse amounts as numbers\nlet totalAmount = Number(data.total_amount);\nlet eurAmount = Number(data.eur_amount);\n\nif (isNaN(totalAmount)) {\n  totalAmount = Number(String(data.total_amount).replace(/[^0-9.-]/g, '')) || 0;\n}\nif (isNaN(eurAmount)) {\n  eurAmount = Number(String(data.eur_amount).replace(/[^0-9.-]/g, '')) || 0;\n}\n\n// Get sheetId from either Build Headers Request (new sheet) or Extract Sheet ID (existing sheet)\nlet sheetId = 0;\n\ntry {\n  sheetId = $('Build Headers Request').item.json.sheetId;\n} catch (e) {\n  // New sheet path did not run\n}\n\nif (!sheetId && sheetId !== 0) {\n  try {\n    sheetId = $('Extract Sheet ID').item.json.sheetId;\n  } catch (e) {\n    // Existing sheet path did not run\n  }\n}\n\n// If still no sheetId, try Extract Sheet ID again (it might be 0 which is falsy)\nif (sheetId === 0) {\n  try {\n    const extractedId = $('Extract Sheet ID').item.json.sheetId;\n    if (extractedId !== undefined) {\n      sheetId = extractedId;\n    }\n  } catch (e) {\n    // Keep default\n  }\n}\n\nreturn {\n  json: {\n    expenseBody: {\n      values: [[\n        data.transaction_date,\n        data.category,\n        data.vendor_name,\n        data.details || \"\",\n        totalAmount,\n        data.currency,\n        eurAmount\n      ]]\n    },\n    sheetName: data.sheet_name,\n    sheetId: sheetId,\n    eurAmount: eurAmount,\n    totalAmount: totalAmount\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "1c2250d1-df50-45bb-be2e-21a3c4cd6dae",
      "name": "Build Row Format",
      "type": "n8n-nodes-base.code",
      "position": [
        1504,
        416
      ],
      "parameters": {
        "jsCode": "// Get the row number from the Add Expense Row response\nconst response = $('Add Expense Row').item.json;\nconst updatedRange = response.updates?.updatedRange || response.updatedRange || \"\";\n\n// Extract row number from range like \"December_2025!A4:G4\"\nconst rowMatch = updatedRange.match(/!A(\\d+)/);\nconst rowNumber = rowMatch ? parseInt(rowMatch[1]) : 4;\n\n// Get sheetId from Build Expense Data\nconst sheetId = $('Build Expense Data').item.json.sheetId;\n\nreturn {\n  json: {\n    formatBody: {\n      requests: [\n        {\n          repeatCell: {\n            range: {\n              sheetId: sheetId,\n              startRowIndex: rowNumber - 1,\n              endRowIndex: rowNumber,\n              startColumnIndex: 0,\n              endColumnIndex: 7\n            },\n            cell: {\n              userEnteredFormat: {\n                backgroundColor: { red: 1, green: 1, blue: 1 },\n                textFormat: { \n                  foregroundColor: { red: 0, green: 0, blue: 0 },\n                  bold: true\n                },\n                wrapStrategy: \"WRAP\"\n              }\n            },\n            fields: \"userEnteredFormat.backgroundColor,userEnteredFormat.textFormat,userEnteredFormat.wrapStrategy\"\n          }\n        }\n      ]\n    },\n    rowNumber: rowNumber\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "5cc3b9c8-8a3b-4d5b-b7db-8a55326c9314",
      "name": "Format Data Row",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1680,
        400
      ],
      "parameters": {
        "url": "https://sheets.googleapis.com/v4/spreadsheets/1EE-YOUR_AWS_SECRET_KEY_HERE:batchUpdate",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.formatBody }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": " application/json"
            }
          ]
        },
        "nodeCredentialType": "googleSheetsOAuth2Api"
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "39dbf545-1897-4475-bb6a-f1a02bdd5f64",
      "name": "Get Sheet Info",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        624,
        416
      ],
      "parameters": {
        "url": "https://sheets.googleapis.com/v4/spreadsheets/1EE-YOUR_AWS_SECRET_KEY_HERE?fields=sheets%28properties%28sheetId%2Ctitle%29%29",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api"
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "7497384a-6268-4a4d-b4af-87006bc2069d",
      "name": "Extract Sheet ID",
      "type": "n8n-nodes-base.code",
      "position": [
        800,
        416
      ],
      "parameters": {
        "jsCode": "const sheetsData = $('Get Sheet Info').item.json;\nconst sheetName = $('Process Extraction').item.json.sheet_name;\n\nlet sheetId = 0;\n\nif (sheetsData.sheets) {\n  for (const sheet of sheetsData.sheets) {\n    if (sheet.properties.title === sheetName) {\n      sheetId = sheet.properties.sheetId;\n      break;\n    }\n  }\n}\n\nreturn {\n  json: {\n    sheetId: sheetId\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "12f6d843-b3d5-4bd4-a0f3-74212f8d6ec7",
      "name": "Needs Category?",
      "type": "n8n-nodes-base.if",
      "position": [
        -240,
        352
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "b9222392-f473-47bb-ba89-9e279639f067",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.needs_category_input }}",
              "rightValue": "{{ true }}"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "21f2b627-2894-4650-acf7-df320abbfa61",
      "name": "Ask for Category",
      "type": "n8n-nodes-base.telegram",
      "position": [
        0,
        352
      ],
      "parameters": {
        "text": "=\u2753 Category Needed\nI could not determine the category for this receipt:\n\ud83d\udccd Vendor: {{ $('Process Extraction').item.json.vendor_name }}\n\ud83d\udcb0 Amount: \u20ac{{ $('Process Extraction').item.json.total_amount }}\n\ud83d\udcc5 Date: {{ $('Process Extraction').item.json.transaction_date }}\nPlease resend the photo with a caption:\n\nOffice Supplies - description\nAccommodation - description\nMarketing - description\nSoftware - description\nEquipment - description\n\nExample: Marketing - Client gift",
        "chatId": "={{ $('Process Extraction').item.json.chat_id }}",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "31759020-dac7-421a-8346-1f1346a2ff92",
      "name": "Has Photo or PDF?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1584,
        448
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.body.message.photo || $json.body.message.document ? 'yes' : 'no' }}",
              "rightValue": "yes"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "9a4d88c3-f8a1-4d2d-8ab1-d1a6f842de2b",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1856,
        352
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 256,
        "content": "## 1. Receive input\nReceive receipt from Telegram. Check if message contains a photo or PDF."
      },
      "typeVersion": 1
    },
    {
      "id": "31bb29ce-fc92-4af1-b36c-c410a6847306",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1392,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 464,
        "content": "## 2. Download file\nDownload file from Telegram servers and convert to Base64 for easybits API."
      },
      "typeVersion": 1
    },
    {
      "id": "8279c562-751c-4b3d-9838-f6866d6d6523",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 288,
        "content": "## 3. Extract data\nSend file to easybits for extraction. Parse vendor, amount, date, and category."
      },
      "typeVersion": 1
    },
    {
      "id": "4807ec76-f37b-41dc-a721-a6ce818342e8",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 272,
        "content": "## 4. Handle unknown category\nIf category is \"Other\", ask user to resend with a caption specifying the category."
      },
      "typeVersion": 1
    },
    {
      "id": "b6745c1d-82c5-41e9-a2d6-c1ee92f05050",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 928,
        "height": 512,
        "content": "## 5. Prepare sheet\nCheck if monthly sheet exists. If not, create new sheet with headers and formatting."
      },
      "typeVersion": 1
    },
    {
      "id": "f8428194-ed14-44f5-a892-362c98d819cc",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1152,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 512,
        "content": "## 6. Add expense row\nAdd expense to the correct monthly sheet. Apply bold formatting to the data row."
      },
      "typeVersion": 1
    },
    {
      "id": "850335ec-7b84-435e-9ed1-dba1eddb0ea1",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1888,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 512,
        "content": "## 7. Confirm\nSend confirmation message to user with expense details."
      },
      "typeVersion": 1
    },
    {
      "id": "d3264444-8027-4330-8a2b-fbefe8167b25",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2048,
        -656
      ],
      "parameters": {
        "width": 896,
        "height": 608,
        "content": "## Telegram Expense Bot\n\nAutomatically extract receipt data and organize expenses in Google Sheets.\n\n### How it works\n1. Send receipt photo or PDF to Telegram bot\n2. easybits extracts vendor, amount, date, category\n3. Data is added to the correct monthly sheet\n4. Bot confirms the expense was logged\n\n### How to set up\n1. Create easybits pipeline at extractor.easybits.tech\n2. Create Telegram bot via @BotFather\n3. Connect Google Sheets account\n4. Update credentials in workflow nodes\n5. Activate and send your first receipt\n\n### Customization\n- Adjust reimbursement rates in \"Process Extraction\" node\n- Add categories in the validCategories array\n- Change sheet formatting in \"Build Headers Request\" node"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "ae30fc1f-315f-4214-93d4-bb908a198cf6",
  "connections": {
    "Create Sheet": {
      "main": [
        [
          {
            "node": "Build Headers Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get File Info": {
      "main": [
        [
          {
            "node": "Download Photo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup Headers": {
      "main": [
        [
          {
            "node": "Build Expense Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheet Exists?": {
      "main": [
        [
          {
            "node": "Create Sheet",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Sheet Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Photo": {
      "main": [
        [
          {
            "node": "Convert to Base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Sheet Info": {
      "main": [
        [
          {
            "node": "Extract Sheet ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Expense Row": {
      "main": [
        [
          {
            "node": "Build Row Format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Data Row": {
      "main": [
        [
          {
            "node": "Send Confirmation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Category?": {
      "main": [
        [
          {
            "node": "Ask for Category",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check Sheet Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask for Category": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Row Format": {
      "main": [
        [
          {
            "node": "Format Data Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Sheet ID": {
      "main": [
        [
          {
            "node": "Build Expense Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No Photo Message": {
      "main": [
        [
          {
            "node": "Respond OK 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Webhook": {
      "main": [
        [
          {
            "node": "Has Photo or PDF?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to Base64": {
      "main": [
        [
          {
            "node": "Extract with easybits",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Photo or PDF?": {
      "main": [
        [
          {
            "node": "Get File Info",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Photo Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Confirmation": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Expense Data": {
      "main": [
        [
          {
            "node": "Add Expense Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Sheet Exists": {
      "main": [
        [
          {
            "node": "Sheet Exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Extraction": {
      "main": [
        [
          {
            "node": "Needs Category?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Headers Request": {
      "main": [
        [
          {
            "node": "Setup Headers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract with easybits": {
      "main": [
        [
          {
            "node": "Process Extraction",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}