{
  "name": "Bot ROVEEb",
  "nodes": [
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.message.text }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    },
                    "id": "0f04348c-a5af-4874-a8cd-8da747c5271f"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "text"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "a122a7ee-7f24-4d3b-aaf5-267688ed7176",
                    "leftValue": "={{ $json.message.voice.file_id }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "voice"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "507ebe51-f956-4fdd-bd2b-6589a7e3c206",
                    "leftValue": "={{ $json.message.document.file_id }}",
                    "rightValue": "",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "csv"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        -2320,
        -672
      ],
      "id": "571cb7f5-a835-4bb7-8ea9-61792de2f361",
      "name": "Switch"
    },
    {
      "parameters": {
        "resource": "audio",
        "operation": "transcribe",
        "options": {
          "language": "pt"
        }
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        -1888,
        -656
      ],
      "id": "1dbe237c-c086-4c74-a292-1cab20d9e640",
      "name": "OpenAI",
      "retryOnFail": true,
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5-nano",
          "mode": "list",
          "cachedResultName": "GPT-5-NANO"
        },
        "responses": {
          "values": [
            {
              "content": "=Voc\u00ea \u00e9 um extrator de transa\u00e7\u00f5es financeiras em PT-BR no n8n. Sua tarefa \u00e9 LER o texto bruto e DEVOLVER apenas JSON v\u00e1lido seguindo o esquema abaixo.\n\nEntrada (texto bruto):\n{{$json[\"message\"]}}\n\nSa\u00edda (obrigat\u00f3ria, SOMENTE JSON v\u00e1lido):\n{\"transactions\":[ ... ]} \nSe nada for encontrado: {\"transactions\":[]}\n\nPara cada item em \"transactions\" use exatamente estas chaves:\n- categoria: UMA dentre {Alimenta\u00e7\u00e3o,Transporte,Moradia,Lazer,Sa\u00fade,Compras,Servi\u00e7os,Educa\u00e7\u00e3o,D\u00edvidas/Empr\u00e9stimos,Cart\u00e3o de Cr\u00e9dito,Viagem,Outros}. Se for ENTRADA, use \"Receita\".\n- descricao: resumo curto, normalizado e leg\u00edvel (ex.: \"Almo\u00e7o no X\", \"Gasolina no Y\"). N\u00c3O escreva \u201ccompra de\u201d, \u201crecebido de\u201d, \u201ccategoria\u2026\u201d.\n- valor: n\u00famero BRL com ponto decimal (ex.: 50.00). Converta valores por extenso (ex.: \"cinquenta reais\" \u2192 50.00).\n- data: AAAA-MM-DD. Inferir \u201choje/ontem/dia da semana\u201d considerando hoje = {{$now.setZone('America/Sao_Paulo').toFormat('yyyy-MM-dd HH:mm:ss')}}. Se ausente, usar a data de hoje.\n- tipo: \"entrada\" (receita/sal\u00e1rio/recebido) ou \"saida\" (despesa/compra/custo).\n\nREGRAS DE NORMALIZA\u00c7\u00c3O (aplique antes de classificar):\n1) Remova caracteres de ru\u00eddo em nomes/descri\u00e7\u00f5es: asterisco (*) , barras (/), sublinhado (_), h\u00edfen isolado (- quando usado como separador), pontos isolados (.) e m\u00faltiplos espa\u00e7os \u2192 substitua por um \u00fanico espa\u00e7o.\n2) Aparar espa\u00e7os no in\u00edcio/fim.\n3) \"Formato de frase\": \n   - Coloque iniciais de palavras em Mai\u00fasculas e demais min\u00fasculas (Title Case), mantendo min\u00fasculas para conectivos/curtas: {de, da, do, das, dos, e, em, no, na, nos, nas, com, para, por}.\n   - Preserve siglas comuns (2\u20134 letras) se vierem todas em mai\u00fasculas (ex.: \"USP\", \"DM\u201d, \"IFD\" permanecem mai\u00fasculas).\n4) Exemplos:\n   - \"PET LOVE*CLUBE\" \u2192 \"Pet Love Clube\"\n   - \"UBER * PENDING\" \u2192 \"Uber Pending\"\n   - \"FARMACIA SAO JOAO\" \u2192 \"Farmacia S\u00e3o Jo\u00e3o\" (n\u00e3o invente acentos se n\u00e3o souber)\n\nREGRAS DE VALOR:\n- Aceite formatos: \"R$ 3.032,81\", \"R$ -50,00\", \"50,00\", \"-12,06\", ou por extenso (\"cinquenta reais\").\n- Converter para n\u00famero com ponto decimal: 3032.81, -50.00, etc.\n- Se o valor for negativo, respeite o sinal no n\u00famero final.\n- Se houver mais de um valor no texto, escolha o da transa\u00e7\u00e3o em quest\u00e3o.\n\nREGRAS DE DATA:\n- Se houver \"hoje/ontem/anteontem\" ou \"segunda/ter\u00e7a/...\": infira a data relativa usando a refer\u00eancia de \"hoje\" acima (America/Sao_Paulo).\n- Se a data estiver ausente, use a data de hoje.\n- Se houver dia e m\u00eas sem ano, use o ano corrente.\n\nREGRAS DE TIPO:\n- \"entrada\" se o texto indicar cr\u00e9dito/recebimento: {recebido, cr\u00e9dito, creditado, estorno, reembolso, sal\u00e1rio, dep\u00f3sito, ajuste a cr\u00e9dito}.\n- Caso contr\u00e1rio, \"saida\".\n- Se o valor vier com sinal negativo mas o contexto indicar PAGAMENTO na fatura do cart\u00e3o (ajuste a cr\u00e9dito por exemplo) ou indicar estorno/reembolso, classifique como \"entrada\" e lance o valor positivo.\n\nREGRAS DE CATEGORIA (heur\u00edsticas, escolha UMA):\n- Viagem: hospedagem/hotel/pousada/airbnb/booking, tarifas de viagem, passagens.\n- Alimenta\u00e7\u00e3o: restaurante, caf\u00e9, iFood, padaria, mercado com itens de consumo imediato; \"almo\u00e7o\", \"jantar\", \"lanche\".\n- Transporte: Uber/99, combust\u00edvel (posto), ped\u00e1gio, estacionamento.\n- Sa\u00fade: farm\u00e1cia, cl\u00ednica, exames, plano de sa\u00fade.\n- Compras: varejo geral (Amazon, Shopee, Magazine Luiza, etc.).\n- Servi\u00e7os: assinaturas/servi\u00e7os digitais (Spotify, Netflix, Wasabi, Contabo, AWS), manuten\u00e7\u00e3o/servi\u00e7o recorrente.\n- Educa\u00e7\u00e3o: escola, curso, mensalidade educacional.\n- Moradia: aluguel, condom\u00ednio, luz, \u00e1gua, internet residencial.\n- Lazer: cinema, eventos, entretenimento n\u00e3o alimentar.\n- Cart\u00e3o de Cr\u00e9dito: tarifas e encargos expl\u00edcitos do cart\u00e3o (anuidade/IOF) quando n\u00e3o se encaixar melhor em outra.\n- D\u00edvidas/Empr\u00e9stimos: parcelas/financiamentos/empr\u00e9stimos banc\u00e1rios.\n- Receita: para entradas.\n- Outros: caso n\u00e3o se enquadre nas anteriores.\n\nREGRAS DE FILTRO:\n- Ignore linhas que sejam totais, rodap\u00e9s, cabe\u00e7alhos ou descri\u00e7\u00f5es gen\u00e9ricas sem transa\u00e7\u00e3o (ex.: \"Pagamentos Validos Normais\", \"Total da Fatura\").\n\nFORMATO FINAL:\n- Extraia TODAS as transa\u00e7\u00f5es que encontrar na entrada.\n- N\u00e3o inclua coment\u00e1rios, textos extras ou quebras indevidas \u2014 SOMENTE o JSON v\u00e1lido pedido.\n- Quando houver hospedagem na descri\u00e7\u00e3o, categorize como \"Viagem\".\n\nExemplo de sa\u00edda:\n{\"transactions\":[\n  {\"categoria\":\"Compras\",\"descricao\":\"Pet Love Clube\",\"valor\":12.06,\"data\":\"2025-11-04\",\"tipo\":\"saida\"},\n  {\"categoria\":\"Servi\u00e7os\",\"descricao\":\"Spotify\",\"valor\":40.90,\"data\":\"2025-11-04\",\"tipo\":\"saida\"}\n]}\n"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2,
      "position": [
        -1408,
        -624
      ],
      "id": "93d66d70-6ccd-4473-ac53-e3ab71275121",
      "name": "Message a model",
      "retryOnFail": true,
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ad1ddc47-fe87-4364-95ae-c9945a2dd4cf",
              "name": "message",
              "value": "={{ $json.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1744,
        -656
      ],
      "id": "249efe3c-0b4d-4f59-9820-d1941062764b",
      "name": "Set Message from Audio"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "6e1e69d7-6962-4646-a064-665ed8ea089e",
              "name": "message",
              "value": "={{ $json.message.text }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1888,
        -880
      ],
      "id": "439990a1-fc59-4de4-a3a7-96dcf4e9e56b",
      "name": "Set Message from Text"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "3038539e-653b-40da-920b-69ea72ec3dc0",
              "name": "parsedJson",
              "value": "={{ JSON.parse($json.output[0].content[0].text) }}",
              "type": "object"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -928,
        -624
      ],
      "id": "571a82bb-62c7-4abc-96dc-a40f9515364f",
      "name": "Parse AI JSON"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "fb611a02-1d8d-4726-b701-075738cbe52a",
              "leftValue": "={{ $json.parsedJson.transactions.length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -768,
        -592
      ],
      "id": "124574b2-c1c3-4590-a255-0d003bba7222",
      "name": "Has transactions?"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "73c5a33d-8319-4b29-b377-cb0888c272ce",
              "name": "message",
              "value": "N\u00e3o consegui encontrar nenhuma transa\u00e7\u00e3o no seu \u00e1udio/texto. \ud83d\ude41",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        48,
        -480
      ],
      "id": "168511b6-07b0-44e4-9adc-554874508983",
      "name": "Fail message"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "73c5a33d-8319-4b29-b377-cb0888c272ce",
              "name": "message",
              "value": "=*{{ $json.user }}!* \ud83d\udc4b\n\nSeu lan\u00e7amento foi feito com sucesso:\n\n\u2705 *Tipo:* {{ $json.type }}\n\ud83d\uddd3\ufe0f *Data:* {{ DateTime.fromISO($('Edit Fields').item.json.date).toFormat('dd/MM/yyyy') }}\n\ud83e\uddfe *Descri\u00e7\u00e3o:* {{ $json.description }}\n\ud83d\udcb0 *Valor:* R$ {{ $json.value.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}\n\ud83c\udff7\ufe0f *Categoria:* {{ $json.category }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        80,
        -704
      ],
      "id": "5fd4b4c1-1b8c-4dc0-b025-5f0627e15d04",
      "name": "Sucess message"
    },
    {
      "parameters": {
        "dataTableId": {
          "__rl": true,
          "value": "rcz6CPiAZbvYcdSw",
          "mode": "list",
          "cachedResultName": "video",
          "cachedResultUrl": "/projects/Fk5PIHrixMIVGbVy/datatables/rcz6CPiAZbvYcdSw"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "value": "={{ $json.value }}",
            "date": "={{ $json.date }}",
            "type": "={{ $json.type }}",
            "category": "={{ $json.category }}",
            "description": "={{ $json.description }}",
            "user": "={{ $('Map Chat').item.json.user }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "value",
              "displayName": "value",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "date",
              "displayName": "date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "type",
              "displayName": "type",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "category",
              "displayName": "category",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "description",
              "displayName": "description",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "user",
              "displayName": "user",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1,
      "position": [
        -48,
        -704
      ],
      "id": "d119a50b-1760-4533-b506-974992f8b3ca",
      "name": "Save"
    },
    {
      "parameters": {
        "content": "## Receive message",
        "height": 272,
        "width": 368,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2816,
        -768
      ],
      "typeVersion": 1,
      "id": "e6344816-956a-493f-a33d-3f2aa83c7321",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Extract informations",
        "height": 1008,
        "width": 1440,
        "color": 2
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2432,
        -1056
      ],
      "typeVersion": 1,
      "id": "7cb26b1b-2330-48ef-a995-4b658f2da0f6",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## Store and response to telegram",
        "height": 576,
        "width": 1536,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -976,
        -912
      ],
      "typeVersion": 1,
      "id": "1bae0ed3-36f1-43f9-85ac-befc9d150747",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "fieldToSplitOut": "parsedJson.transactions",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        -624,
        -752
      ],
      "id": "a078b6db-bb12-4c13-a0c2-07d2d9f9ef2f",
      "name": "Split Out"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a3ba42dc-84ea-4833-acd8-e77f4f7d4ddf",
              "name": "type",
              "value": "={{ $json.tipo }}",
              "type": "string"
            },
            {
              "id": "fe50a7bf-d43c-43b0-bb2d-c0d9ac9cd909",
              "name": "date",
              "value": "={{ $json.data }}",
              "type": "string"
            },
            {
              "id": "f9afcd79-07ca-47fb-88f9-11b358e58417",
              "name": "description",
              "value": "={{ $json.descricao }}",
              "type": "string"
            },
            {
              "id": "2b5aa2b9-7349-4f6d-91e0-ec34e2416be7",
              "name": "value",
              "value": "={{ $json.valor }}",
              "type": "string"
            },
            {
              "id": "3c5215ad-b557-4772-a5b2-cc1997ae142b",
              "name": "category",
              "value": "={{ $json.categoria }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -192,
        -704
      ],
      "id": "c31405d8-08a3-4812-a1ad-63448098577e",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -384,
        -768
      ],
      "id": "f6f84425-e963-4496-97eb-b7d6ab5ef12e",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "42bd45d0-768b-4c47-bb1b-fdc9f5480025",
              "leftValue": "={{ $json.message.document.mime_type }}",
              "rightValue": "csv",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "12f09a33-f1b2-435b-88bf-0f593d50159f",
              "leftValue": "={{ $json.message.document.file_name }}",
              "rightValue": "csv",
              "operator": {
                "type": "string",
                "operation": "endsWith"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -2160,
        -432
      ],
      "id": "da8111a7-6770-406b-9405-4c5f7c10b6d3",
      "name": "If"
    },
    {
      "parameters": {
        "resource": "file",
        "fileId": "={{ $('Telegram Trigger').item.json.message.document.file_id }}",
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -1952,
        -448
      ],
      "id": "dc9b8143-abf7-44ae-8b15-00c436900781",
      "name": "Get CSV",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "file",
        "fileId": "={{ $('Telegram Trigger').item.json.message.voice.file_id }}",
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        -2032,
        -656
      ],
      "id": "76aa214a-c682-4a73-9a80-f7cfef756c63",
      "name": "Get audio",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "options": {
          "rawData": true,
          "readAsString": true
        }
      },
      "id": "1860907f-6abe-4fee-8935-8afd0a0ecc04",
      "name": "Convert To Spreadsheet",
      "type": "n8n-nodes-base.spreadsheetFile",
      "position": [
        -1792,
        -448
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "jsCode": "// ======= Configura\u00e7\u00e3o =======\nconst FIELD_CANDIDATES = {\n  desc:   ['Estabelecimento','estabelecimento','descricao','Descri\u00e7\u00e3o','title','Title','merchant','nome','estab'],\n  amount: ['Valor','valor','valor_total','amount','Amount','price'],\n  date:   ['Data','data','date','Date'] // n\u00e3o usado na frase (usamos \"hoje\"), mas deixado para futura expans\u00e3o\n};\n\nconst IGNORE_DESCRIPTIONS = [\n  'pagamentos validos normais',\n  'pagamento v\u00e1lido normal',\n  'pagamento recebido'\n];\n\n// ======= Helpers =======\nconst items = await $input.all();\n\nconst fmtHoje = new Intl.DateTimeFormat('pt-BR', { timeZone: 'America/Sao_Paulo' });\nconst hojeBR = fmtHoje.format(new Date());\n\n// remove BOM/acentos/caixa e normaliza espa\u00e7os\nfunction normalizeKey(k){\n  return String(k||'')\n    .replace(/\\uFEFF/g,'')\n    .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n    .toLowerCase().trim();\n}\nfunction normalizeText(s){\n  return String(s||'')\n    .replace(/\\uFEFF/g,'')\n    .replace(/\\*/g,' ')           // remove asteriscos\n    .replace(/\\s+/g,' ')          // compacta espa\u00e7os\n    .trim();\n}\nfunction normForCompare(s){\n  return normalizeText(s)\n    .normalize('NFD').replace(/[\\u0300-\\u036f]/g,'')\n    .toLowerCase();\n}\nfunction toTitleCase(s){\n  return normalizeText(s).toLowerCase().replace(/\\b([a-z\u00e0-\u00fa])([a-z\u00e0-\u00fa]*)/gi, (_,a,b)=> a.toUpperCase()+b);\n}\n\nfunction pick(obj, candidates){\n  // tenta direto\n  for (const c of candidates) if (obj[c] !== undefined) return obj[c];\n  // tenta por chaves normalizadas\n  const map = new Map(Object.keys(obj).map(k => [normalizeKey(k), k]));\n  for (const c of candidates){\n    const nk = normalizeKey(c);\n    if (map.has(nk)) return obj[map.get(nk)];\n  }\n  return undefined;\n}\n\n// \"R$ 3.032,81\" -> 3032.81 ; \"3032.81\" -> 3032.81 ; \"3.032,81\" -> 3032.81\nfunction parseAmountAny(s){\n  if (s == null) return 0;\n  const str = String(s).trim();\n  // se tem v\u00edrgula como decimal (pt-BR)\n  if (/,/.test(str) && !/^\\d+(\\.\\d+)?$/.test(str)){\n    const cleaned = str.replace(/[R$\\s]/g,'').replace(/\\./g,'').replace(',', '.');\n    const n = Number(cleaned);\n    return Number.isFinite(n) ? n : 0;\n  }\n  // caso \"Nubank\" j\u00e1 venha n\u00famero (ou string en-US)\n  const n = Number(str.replace(/[R$\\s]/g,''));\n  return Number.isFinite(n) ? n : 0;\n}\n\n// ======= Processamento =======\nconst lines = [];\n\nfor (const it of items){\n  const row = it.json ?? it;\n\n  const rawDesc   = pick(row, FIELD_CANDIDATES.desc);\n  const rawAmount = pick(row, FIELD_CANDIDATES.amount);\n\n  const desc = toTitleCase(rawDesc || '');\n  const descCmp = normForCompare(desc);\n\n  // ignorar linhas espec\u00edficas\n  if (IGNORE_DESCRIPTIONS.includes(descCmp)) continue;\n\n  const amount = parseAmountAny(rawAmount);\n  if (!desc || !Number.isFinite(amount)) continue;\n\n  const valorBR = Math.abs(amount).toLocaleString('pt-BR', { minimumFractionDigits:2, maximumFractionDigits:2 });\n  lines.push(`Compra de ${valorBR} no ${desc} na data de ${hojeBR}`);\n}\n\n// ======= Sa\u00edda preservando contexto =======\nconst baseBinary = items[0]?.binary ? { ...items[0].binary } : undefined;\n\nconst out = {\n  json: { message: lines.join('\\n') },\n  pairedItem: items.map((_, i) => ({ item: i }))\n};\nif (baseBinary) out.binary = baseBinary;\n\nreturn [out];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1616,
        -448
      ],
      "id": "a07ac806-c9f1-49ef-8785-7a51bd7cbff3",
      "name": "Code in JavaScript"
    },
    {
      "parameters": {
        "jsCode": "const items = await $input.all();\n\nfunction pickName(from) {\n  if (!from) return '';\n  // tenta username; se n\u00e3o houver, usa \"first_name last_name\" (quando houver)\n  const fname = from.first_name;\n  return fname;\n}\n\nreturn items.map((it, idx) => {\n  const j = it.json ?? it;\n\n  const message = j.message ?? j; // aceita quando j\u00e1 est\u00e1 \"achatado\"\n  const chat    = message.chat ?? j.chat ?? {};\n  const from    = message.from ?? j.from ?? {};\n\n  const chatId     = chat.id ?? null;\n  const chatType   = chat.type ?? null;\n  const chatTitle  = chat.title ?? null;\n  const fromId     = from.id ?? null;\n  const userLabel  = pickName(from);\n\n  return {\n    json: {\n      ...j,\n      chat_id: chatId,\n      chat_type: chatType,\n      chat_title: chatTitle,\n      from_id: fromId,\n      user: userLabel\n    },\n    binary: it.binary,\n    pairedItem: { item: idx }\n  };\n});\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2608,
        -704
      ],
      "id": "ec97b8f8-a3e6-49d5-b752-c3e699eb6eac",
      "name": "Map Chat"
    },
    {
      "parameters": {
        "chatId": "={{ $('Map Chat').item.json.chat_id }}",
        "text": "={{ $json.message }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        368,
        -720
      ],
      "id": "e1611863-a363-4687-9a74-5572bcdcddad",
      "name": "Send message",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const items = await $input.all();\n\n// Coleta todas as mensagens\nconst msgs = items.map(i => i.json?.message).filter(Boolean);\n\n// Junta respeitando limite do Telegram\nconst MAX = 3800;\nconst out = [];\nlet bucket = \"\";\n\nfor (const m of msgs) {\n  const piece = (bucket ? \"\\n\\n\" : \"\") + m;\n  if ((bucket.length + piece.length) > MAX) {\n    out.push({ json: { message: bucket } });\n    bucket = m;\n  } else {\n    bucket += piece;\n  }\n}\nif (bucket) out.push({ json: { message: bucket } });\n\nreturn out.map(o => ({\n  ...o,\n  pairedItem: [{ item: 0 }]\n}));\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -192,
        -864
      ],
      "id": "a8020ed1-f874-4d59-b864-8d5a31034456",
      "name": "Aggregate"
    },
    {
      "parameters": {
        "content": "## Retrive data and generate HTML view",
        "height": 416,
        "width": 1376,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2832,
        64
      ],
      "typeVersion": 1,
      "id": "34cb849c-8277-4799-9da0-4ddf0971a108",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "path": "finances-video",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -2800,
        224
      ],
      "id": "ba31b11d-b969-4736-b21c-3b3004006057",
      "name": "Webhook"
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "value": "rcz6CPiAZbvYcdSw",
          "mode": "list",
          "cachedResultName": "video",
          "cachedResultUrl": "/projects/Fk5PIHrixMIVGbVy/datatables/rcz6CPiAZbvYcdSw"
        },
        "filters": {
          "conditions": [
            {
              "keyName": "date",
              "condition": "gte",
              "keyValue": "={{ $json.start }}"
            },
            {
              "keyName": "date",
              "condition": "lte",
              "keyValue": "={{ $json.end }}"
            }
          ]
        },
        "returnAll": true
      },
      "name": "List Finance (Data Table)",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1,
      "position": [
        -2288,
        224
      ],
      "id": "e5fd6190-dc90-47eb-a766-78e48c510dde",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "// ================= helpers =================\nfunction isEntrada(t){ const v=(t||'').toLowerCase(); return ['entrada','receita','salario','sal\u00e1rio','recebido','recebidos'].some(x=>v.includes(x)); }\nfunction isSaida(t){ const v=(t||'').toLowerCase(); return ['saida','sa\u00edda','despesa','despesas','compra','custo'].some(x=>v.includes(x)); }\nfunction brl(x){ return (Number(x)||0).toLocaleString('pt-BR',{minimumFractionDigits:2, maximumFractionDigits:2}); }\nfunction normDate(s){ if(!s) return null; if(/^\\d{4}-\\d{2}$/.test(s)) return s + '-01'; return s; }\n\n// N\u00c3O CONVERTE PARA Date: usa s\u00f3 a string YYYY-MM-DD\nfunction ymdToBR(ymd){\n  const s = (ymd||'').slice(0,10);\n  const [y,m,d] = s.split('-');\n  return (y && m && d) ? `${d}/${m}/${y}` : '';\n}\n\nfunction ptMonthYear(ym){\n  const [y, m] = (ym||'').split('-').map(Number);\n  // Representa\u00e7\u00e3o est\u00e1tica sem usar Date: m\u00eas por nome em pt-BR\n  const meses = ['janeiro','fevereiro','mar\u00e7o','abril','maio','junho','julho','agosto','setembro','outubro','novembro','dezembro'];\n  if (!y || !m) return ym || '';\n  return `${meses[m-1]} de ${y}`;\n}\n\n// ================ inputs =====================\nconst datesItem = $items('Set Infos', 0, 0)[0]?.json || {};\nconst fromStrIn = datesItem.start;\nconst toStrIn   = datesItem.end;\nconst initialBalance = Number(datesItem.initialBalance ?? datesItem.ib ?? 0) || 0;\n\nconst from = normDate(fromStrIn);\nconst to   = normDate(toStrIn) || new Date().toISOString().slice(0,10);\n\n// Limites em STRING (sem timezone)\nconst fromY = (from || '0000-00-00').slice(0,10);\nconst toY   = (to   || '9999-12-31').slice(0,10);\n\nif (!fromY || fromY === '0000-00-00') {\n  return [{ json: { html_content: `<!doctype html><meta charset=\"utf-8\"><meta name=\"color-scheme\" content=\"light\"><body style=\"font:14px system-ui;background:#fff;color:#111\"><h3>Par\u00e2metro ?start=AAAA-MM-DD (ou AAAA-MM) \u00e9 obrigat\u00f3rio</h3></body>` } }];\n}\n\n// rows de entrada (Data Table -> Code)\nconst rows = (await $input.all()).map(i => i.json);\n\n// normaliza (sem parse de data)\nconst normalized = rows.map(r=>({\n  date: r.date,                         // mant\u00e9m original\n  ymd: (r.date||'').slice(0,10),        // YYYY-MM-DD (base para tudo)\n  ym:  (r.date||'').slice(0,7),         // YYYY-MM\n  value: Number(r.value)||0,\n  type: r.type||'',\n  description: r.description||'',\n  category: r.category||'',\n  user: r.user || ''\n}));\n\n// filtra por string (evita timezone)\nconst inRange = normalized.filter(r => r.ymd && r.ymd >= fromY && r.ymd <= toY);\n\n// meses presentes\nconst months = Array.from(new Set(inRange.map(x=>x.ym))).filter(Boolean).sort();\n\n// agrega\u00e7\u00e3o mensal\nconst perMonth = months.map(m=>{\n  const xs = inRange.filter(x=>x.ym===m);\n  const recebidos = xs.filter(x=>isEntrada(x.type)).reduce((a,b)=>a+b.value,0);\n  const despesas  = xs.filter(x=>isSaida(x.type)).reduce((a,b)=>a+b.value,0);\n  return { month:m, recebidos, despesas, saldo: recebidos - despesas };\n});\n\n// totais do per\u00edodo\nconst totalRecebidos = inRange.filter(x=>isEntrada(x.type)).reduce((a,b)=>a+b.value,0);\nconst totalDespesas  = inRange.filter(x=>isSaida(x.type)).reduce((a,b)=>a+b.value,0);\nconst saldoPeriodo   = totalRecebidos - totalDespesas;\n\n// \u2014\u2014 saldo acumulado (sem converter datas) \u2014\u2014\nconst timeline = inRange\n  .map(x => ({ ...x, delta: isEntrada(x.type) ? Number(x.value||0) : -Number(x.value||0) }))\n  .sort((a,b) => a.ymd.localeCompare(b.ymd) || a.description.localeCompare(b.description));\n\nlet running = initialBalance;\nconst saldoAcumSeries = timeline.map(t => {\n  running += t.delta;\n  return { ymd: t.ymd, label: ymdToBR(t.ymd), balance: Number(running.toFixed(2)) };\n});\nconst saldoInicial = initialBalance;\nconst saldoFinal   = Number((initialBalance + saldoPeriodo).toFixed(2));\n\n// ---- s\u00e9ries linha (recebidos\u00d7despesas) ----\nlet lineLabels = [];\nlet serieRecebidos = [];\nlet serieDespesas  = [];\nconst isSingleMonth = months.length === 1;\n\nif (isSingleMonth) {\n  const days = Array.from(new Set(inRange.map(x=>x.ymd))).sort(); // YYYY-MM-DD\n  lineLabels = days.map(d => ymdToBR(d));\n  const recMap = new Map(), desMap = new Map();\n  for (const d of days) { recMap.set(d, 0); desMap.set(d, 0); }\n  for (const it of inRange) {\n    if (isEntrada(it.type)) recMap.set(it.ymd, (recMap.get(it.ymd) || 0) + it.value);\n    else if (isSaida(it.type)) desMap.set(it.ymd, (desMap.get(it.ymd) || 0) + it.value);\n  }\n  serieRecebidos = days.map(d => Number((recMap.get(d)||0).toFixed(2)));\n  serieDespesas  = days.map(d => Number((desMap.get(d)||0).toFixed(2)));\n} else {\n  lineLabels     = perMonth.map(x=> ptMonthYear(x.month));\n  serieRecebidos = perMonth.map(x=> Number(x.recebidos.toFixed(2)));\n  serieDespesas  = perMonth.map(x=> Number(x.despesas.toFixed(2)));\n}\n\n// ---- gr\u00e1fico da direita (din\u00e2mico) ----\nlet rightLabels, rightData, rightTitle;\nif (isSingleMonth) {\n  rightLabels    = ['Receitas', 'Despesas'];\n  rightData      = [Number(totalRecebidos.toFixed(2)), Number(totalDespesas.toFixed(2))];\n  rightTitle     = 'Receitas \u00d7 Despesas';\n} else {\n  rightLabels    = perMonth.map(x => ptMonthYear(x.month));\n  rightData      = perMonth.map(x => Number(x.saldo.toFixed(2)));\n  rightTitle     = 'Saldo por m\u00eas';\n}\n\n// \u2014\u2014 despesas por categoria (barras) \u2014\u2014\nconst catMap = new Map();\nfor (const r of inRange) {\n  if (isSaida(r.type)) {\n    const k = r.category || 'Sem categoria';\n    catMap.set(k, (catMap.get(k) || 0) + (Number(r.value) || 0));\n  }\n}\nconst catLabels = Array.from(catMap.keys());\nconst catValues = Array.from(catMap.values());\nconst catTitle  = `Despesas por categoria ${isSingleMonth ? '(do m\u00eas)' : '(no per\u00edodo)'}`;\n\n// tabela saldos por m\u00eas\nconst saldoRows = perMonth.map(x =>\n  `<tr>\n     <td>${ptMonthYear(x.month)}</td>\n     <td class=\"td-right\">R$ ${brl(x.recebidos)}</td>\n     <td class=\"td-right\">R$ ${brl(x.despesas)}</td>\n     <td class=\"td-right\" style=\"font-weight:600;color:${x.saldo>=0?'#1a7f37':'#c62828'}\">R$ ${brl(x.saldo)}</td>\n   </tr>`\n).join('');\n\n// lan\u00e7amentos (ordenados por data ASC)\nconst lancamentos = [...inRange].sort((a,b)=> a.ymd.localeCompare(b.ymd) || a.description.localeCompare(b.description));\nconst lancRows = lancamentos.map(x=>{\n  const isIn = isEntrada(x.type);\n  const color = isIn ? '#1a7f37' : '#c62828';\n  const sign = isIn ? '+' : '-';\n  return `<tr>\n    <td>${ymdToBR(x.ymd)}</td>\n    <td>${x.user || ''}</td>\n    <td>${x.description || ''}</td>\n    <td>${x.category || ''}</td>\n    <td class=\"td-right\" style=\"color:${color};font-weight:600\">${sign} R$ ${brl(x.value)}</td>\n  </tr>`;\n}).join('');\n\n// =================== HTML =====================\nconst html = `<!doctype html>\n<html lang=\"pt-BR\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<meta name=\"color-scheme\" content=\"light\">\n<title>Relat\u00f3rio Financeiro (${fromY} a ${toY})</title>\n<style>\n  :root{\n    --bg:#ffffff; --card:#ffffff; --text:#0b1220; --muted:#5f6b7a; --border:#e6eaf2;\n    --pos:#1a7f37; --neg:#c62828; --shadow:0 8px 20px rgba(0,0,0,.06); --radius:14px; --wrap:1080px;\n  }\n  *{box-sizing:border-box}\n  html,body{margin:0; background:#ffffff; color:var(--text); font:14px/1.45 ui-sans-serif,system-ui,Inter,Roboto,Arial;}\n  .wrap{max-width:var(--wrap); margin:32px auto; padding:0 20px;}\n  header{display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:18px;}\n  h1{margin:0; font-weight:700; font-size:26px; letter-spacing:.2px;}\n  .badge{display:inline-flex; gap:8px; align-items:center; padding:8px 12px; border:1px solid var(--border); border-radius:999px; background:#f3f4f6; color:#374151; font-weight:700;}\n  .cards{display:grid;grid-template-columns:repeat(12,1fr); gap:16px; margin:18px 0;}\n  .card{grid-column:span 12; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); padding:16px;}\n  .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:14px;}\n  .kpi{background:var(--card); border:1px solid var(--border); border-radius:12px; padding:14px;}\n  .kpi .label{color:var(--muted); font-size:12px;}\n  .kpi .value{margin-top:6px; font-weight:800; font-size:22px;}\n  .chart-2col{display:grid; grid-template-columns: 1fr 1fr; gap:16px;}\n  .chart-box{border:1px solid var(--border); border-radius:12px; padding:12px; background:var(--card);}\n  .chart-title{margin:0 0 10px 0; font-weight:700; font-size:16px;}\n  @media (max-width: 900px){ .chart-2col{grid-template-columns:1fr} }\n\n  .table-wrap{overflow:auto; border:1px solid var(--border); border-radius:12px; background:var(--card);}\n  table{width:100%; border-collapse:separate; border-spacing:0; min-width:720px;}\n  thead th{position:sticky; top:0; background:#ffffff; color:var(--text); text-align:left; z-index:1; border-bottom:1px solid var(--border);}\n  th, td{padding:10px 12px;}\n  tbody tr:nth-child(odd){background-color:#00000005;}\n  tbody tr:hover{background:#00000008;}\n  .td-right{text-align:right;}\n</style>\n</head>\n<body>\n  <div class=\"wrap\">\n    <header>\n      <h1>Relat\u00f3rio Financeiro</h1>\n      <span class=\"badge\">Per\u00edodo: ${fromY} \u2192 ${toY}</span>\n    </header>\n\n    <section class=\"cards\">\n      <div class=\"card\">\n        <div class=\"kpis\">\n          <div class=\"kpi\"><div class=\"label\">Saldo Inicial</div><div class=\"value\">R$ ${brl(saldoInicial)}</div></div>\n          <div class=\"kpi\"><div class=\"label\">Total Recebidos</div><div class=\"value\" style=\"color:var(--pos)\">R$ ${brl(totalRecebidos)}</div></div>\n          <div class=\"kpi\"><div class=\"label\">Total Despesas</div><div class=\"value\" style=\"color:var(--neg)\">R$ ${brl(totalDespesas)}</div></div>\n          <div class=\"kpi\"><div class=\"label\">Saldo no Per\u00edodo</div><div class=\"value\" style=\"color:${saldoPeriodo>=0?'var(--pos)':'var(--neg)'}\">R$ ${brl(saldoPeriodo)}</div></div>\n          <div class=\"kpi\"><div class=\"label\">Saldo Final</div><div class=\"value\" style=\"color:${saldoFinal>=0?'var(--pos)':'var(--neg)'}\">R$ ${brl(saldoFinal)}</div></div>\n        </div>\n      </div>\n\n      <div class=\"card\">\n        <div class=\"chart-2col\">\n          <div class=\"chart-box\">\n            <h3 class=\"chart-title\">${isSingleMonth ? 'Recebidos \u00d7 Despesas por dia' : 'Recebidos \u00d7 Despesas por m\u00eas'}</h3>\n            <div style=\"height:260px\"><canvas id=\"chartLine\"></canvas></div>\n          </div>\n          <div class=\"chart-box\">\n            <h3 class=\"chart-title\">${isSingleMonth ? 'Receitas \u00d7 Despesas' : 'Saldo por m\u00eas'}</h3>\n            <div style=\"height:260px\"><canvas id=\"chartRight\"></canvas></div>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"card\">\n        <h3 class=\"chart-title\">${catTitle}</h3>\n        ${\n          catLabels.length\n            ? `<div style=\"height:260px\"><canvas id=\"chartCat\"></canvas></div>`\n            : `<div style=\"padding:8px;color:#5f6b7a\">Sem despesas no per\u00edodo.</div>`\n        }\n      </div>\n\n      <div class=\"card\">\n        <h3 class=\"chart-title\">Saldo por m\u00eas</h3>\n        <div class=\"table-wrap\">\n          <table>\n            <thead><tr><th>M\u00eas</th><th class=\"td-right\">Recebidos</th><th class=\"td-right\">Despesas</th><th class=\"td-right\">Saldo</th></tr></thead>\n            <tbody>${perMonth.length ? saldoRows : '<tr><td colspan=\"4\" style=\"padding:16px;color:#5f6b7a\">Sem dados no per\u00edodo.</td></tr>'}</tbody>\n          </table>\n        </div>\n      </div>\n\n      <div class=\"card\">\n        <h3 class=\"chart-title\">Lan\u00e7amentos</h3>\n        <div class=\"table-wrap\">\n          <table>\n            <thead><tr><th>Data</th><th>Quem</th><th>Descri\u00e7\u00e3o</th><th>Categoria</th><th class=\"td-right\">Valor</th></tr></thead>\n            <tbody>${lancRows || '<tr><td colspan=\"5\" style=\"padding:16px;color:#5f6b7a\">Sem lan\u00e7amentos.</td></tr>'}</tbody>\n          </table>\n        </div>\n      </div>\n    </section>\n  </div>\n\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"><\\/script>\n<script>\n(() => {\n  if (!window.Chart) return;\n\n  Chart.defaults.color = '#0b1220';\n  const grid = '#e6eaf2';\n  const tick = '#5f6b7a';\n  Chart.defaults.scales = Chart.defaults.scales || {};\n  Chart.defaults.scales.x = { grid: { color: grid }, ticks: { color: tick } };\n  Chart.defaults.scales.y = { grid: { color: grid }, ticks: { color: tick }, beginAtZero:true };\n\n  // --- line (Recebidos \u00d7 Despesas) ---\n  const lineCtx = document.getElementById('chartLine').getContext('2d');\n  new Chart(lineCtx, {\n    type: 'line',\n    data: {\n      labels: ${JSON.stringify(lineLabels)},\n      datasets: [\n        { label: 'Recebidos', data: ${JSON.stringify(serieRecebidos)}, borderWidth: 2, tension: 0.25, pointRadius: 3 },\n        { label: 'Despesas',  data: ${JSON.stringify(serieDespesas)},  borderWidth: 2, tension: 0.25, pointRadius: 3 }\n      ]\n    },\n    options: {\n      responsive: true,\n      maintainAspectRatio: false,\n      plugins: { legend: { position: 'bottom', labels: { color: '#0b1220' } } },\n      scales: {\n        x:{ grid:{ color: grid }, ticks:{ color: tick } },\n        y:{ grid:{ color: grid }, ticks:{ color: tick }, beginAtZero:true }\n      }\n    }\n  });\n\n  // --- right (pizza 1 m\u00eas; barras empilhadas >1 m\u00eas) ---\n  const rightCtx = document.getElementById('chartRight').getContext('2d');\n  const isSingle = ${JSON.stringify(isSingleMonth)};\n  const labelsRight = ${JSON.stringify(rightLabels)};\n  const dataArrRight = ${JSON.stringify(rightData)};\n\n  const baseOptions = {\n    responsive: true,\n    maintainAspectRatio: false,\n    plugins: { legend: { position: 'bottom', labels: { color: '#0b1220' } } }\n  };\n\n  if (isSingle) {\n    new Chart(rightCtx, {\n      type: 'pie',\n      data: { labels: labelsRight, datasets: [{ data: dataArrRight }] },\n      options: baseOptions\n    });\n  } else {\n    const positivos = dataArrRight.map(v => v > 0 ? v : 0);\n    const negativos = dataArrRight.map(v => v < 0 ? v : 0);\n\n    new Chart(rightCtx, {\n      type: 'bar',\n      data: {\n        labels: labelsRight,\n        datasets: [\n          { label: 'Positivo', data: positivos, backgroundColor: 'rgba(26,127,55,0.25)', borderColor: '#1a7f37', borderWidth: 1, stack: 'saldo' },\n          { label: 'Negativo', data: negativos, backgroundColor: 'rgba(198,40,40,0.20)', borderColor: '#c62828', borderWidth: 1, stack: 'saldo' }\n        ]\n      },\n      options: {\n        ...baseOptions,\n        scales: {\n          x: { stacked: true, grid: { color: grid }, ticks: { color: tick } },\n          y: { stacked: true, grid: { color: grid }, ticks: { color: tick } }\n        }\n      }\n    });\n  }\n\n  // --- despesas por categoria (barras) ---\n  const catLabels = ${JSON.stringify(catLabels)};\n  const catValues = ${JSON.stringify(catValues)};\n  if (catLabels.length) {\n    const catCtx = document.getElementById('chartCat').getContext('2d');\n    new Chart(catCtx, {\n      type: 'bar',\n      data: {\n        labels: catLabels,\n        datasets: [\n          { label: 'Despesas', data: catValues, borderWidth: 1 }\n        ]\n      },\n      options: {\n        responsive: true,\n        maintainAspectRatio: false,\n        plugins: { legend: { position: 'bottom', labels: { color: '#0b1220' } } },\n        scales: {\n          x: { grid: { color: grid }, ticks: { color: tick } },\n          y: { grid: { color: grid }, ticks: { color: tick }, beginAtZero: true }\n        }\n      }\n    });\n  }\n})();\n<\\/script>\n</body>\n</html>`;\n\nreturn [{ json: { html_content: html } }];\n"
      },
      "name": "Generate HTML",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2032,
        224
      ],
      "id": "6434bb32-7f20-4e61-bc44-c6874c0219f5"
    },
    {
      "parameters": {
        "keepOnlySet": true,
        "values": {
          "string": [
            {
              "name": "start",
              "value": "={{ $json.query.start ?? '2024-01-01' }}"
            },
            {
              "name": "end",
              "value": "={{ $json.query.end ?? (new Date().toISOString().slice(0,10)) }}"
            }
          ],
          "number": [
            {
              "name": "initialBalance",
              "value": "={{ Number($json.query.ib) ?? 0 }}"
            }
          ]
        },
        "options": {}
      },
      "name": "Set Infos",
      "type": "n8n-nodes-base.set",
      "typeVersion": 2,
      "position": [
        -2544,
        224
      ],
      "id": "0f5bcd37-894d-476a-a148-44f3d21ebc86"
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "={{ $json[\"html_content\"] }}",
        "options": {}
      },
      "name": "Response with HTML",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        -1728,
        224
      ],
      "id": "4c7bbd6b-aeb2-44af-a910-e7018e822551"
    },
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.2,
      "position": [
        -2768,
        -704
      ],
      "id": "2730c018-093d-4d18-8332-600dda1f0354",
      "name": "Telegram Trigger",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Switch": {
      "main": [
        [
          {
            "node": "Set Message from Text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get audio",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI": {
      "main": [
        [
          {
            "node": "Set Message from Audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Parse AI JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Message from Audio": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Message from Text": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI JSON": {
      "main": [
        [
          {
            "node": "Has transactions?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has transactions?": {
      "main": [
        [
          {
            "node": "Split Out",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fail message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fail message": {
      "main": [
        [
          {
            "node": "Send message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sucess message": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save": {
      "main": [
        [
          {
            "node": "Sucess message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Save",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "Get CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get CSV": {
      "main": [
        [
          {
            "node": "Convert To Spreadsheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get audio": {
      "main": [
        [
          {
            "node": "OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert To Spreadsheet": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Chat": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Send message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Set Infos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Finance (Data Table)": {
      "main": [
        [
          {
            "node": "Generate HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate HTML": {
      "main": [
        [
          {
            "node": "Response with HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Infos": {
      "main": [
        [
          {
            "node": "List Finance (Data Table)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Map Chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "3f5ea76e-6c0d-4c4c-856b-b776c00eae32",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "N1cHhvnFycILn5LP",
  "tags": []
}