395 lines
23 KiB
JSON
395 lines
23 KiB
JSON
{
|
|
"id": "qtvB3r0cgejyCxUp",
|
|
"name": "💰 Actual — Import Estratto Conto [Telegram]",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"updates": [
|
|
"message"
|
|
],
|
|
"additionalFields": {}
|
|
},
|
|
"id": "n_tg_trigger",
|
|
"name": "📩 Telegram Trigger",
|
|
"type": "n8n-nodes-base.telegramTrigger",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
0,
|
|
304
|
|
],
|
|
"webhookId": "actual-import-tg-webhook",
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "uTXHLqcCJxbOvqN3",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "cond1",
|
|
"leftValue": "={{ ($json.message?.caption || '').toLowerCase() }}",
|
|
"rightValue": "estratto",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
},
|
|
{
|
|
"id": "cond2",
|
|
"leftValue": "={{ $json.message?.document?.file_id }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "exists",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "n_check",
|
|
"name": "🔍 È un Estratto?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
240,
|
|
304
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "https://api.github.com/copilot_internal/v2/token",
|
|
"authentication": "predefinedCredentialType",
|
|
"nodeCredentialType": "httpHeaderAuth",
|
|
"options": {}
|
|
},
|
|
"id": "n_token",
|
|
"name": "🔑 Ottieni Token Copilot",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
464,
|
|
160
|
|
],
|
|
"credentials": {
|
|
"httpHeaderAuth": {
|
|
"id": "vBwUxlzKrX3oDHyN",
|
|
"name": "GitHub Copilot OAuth Token"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "file",
|
|
"fileId": "={{ $json.file_id }}",
|
|
"additionalFields": {}
|
|
},
|
|
"id": "n_scarica",
|
|
"name": "📥 Scarica File Telegram",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
912,
|
|
160
|
|
],
|
|
"webhookId": "e983ec4e-194c-44bf-9a2a-c78a3280fcf5",
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "uTXHLqcCJxbOvqN3",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const chatId = $('📋 Prepara File ID').first().json.chat_id || -4814221197;\n\n// Use getBinaryDataBuffer() — works with both memory and filesystem binary modes\nconst buf = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst csvText = buf.toString('utf-8').replace(/\\r/g, '');\n\nfunction parseLine(line) {\n const parts = []; let cur = '', inQ = false;\n for (const ch of line) {\n if (ch === '\"') { inQ = !inQ; }\n else if (ch === ';' && !inQ) { parts.push(cur.trim()); cur = ''; }\n else { cur += ch; }\n }\n parts.push(cur.trim());\n return parts;\n}\n\nfunction simpleHash(str) {\n let h = 0;\n for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;\n return Math.abs(h).toString(16).padStart(8, '0');\n}\n\nconst lines = csvText.split('\\n').map(l => l.trim()).filter(Boolean);\nconst dataLines = lines.filter(line => {\n const upper = line.toUpperCase();\n if (upper.startsWith('\"DATA\"') || upper.startsWith('DATA;')) return false;\n if (upper.includes('SALDO FINALE') || upper.includes('SALDO INIZIALE')) return false;\n return true;\n});\n\nconst transactions = [];\nfor (const line of dataLines) {\n if (!line || line.length < 10) continue;\n const cols = parseLine(line);\n if (cols.length < 5) continue;\n const [dataStr, , causale, descrizione, importoStr] = cols;\n if (!dataStr || !importoStr) continue;\n const parts = dataStr.split('/');\n if (parts.length !== 3) continue;\n const [day, month, year] = parts;\n if (!year || year.length !== 4) continue;\n const date = `${year}-${month.padStart(2,'0')}-${day.padStart(2,'0')}`;\n const cleanAmt = importoStr.replace(/[\"+\\s]/g, '');\n const amount = Math.round(parseFloat(cleanAmt) * 100);\n if (isNaN(amount)) continue;\n const idMatch = (descrizione || '').match(/Id\\.\\s*(\\d+)/);\n const importId = idMatch\n ? `banca-sella-${idMatch[1]}`\n : `banca-sella-${date}-${simpleHash(`${date}|${amount}|${descrizione || causale}`)}`;\n transactions.push({ date, amount, causale: causale || '', descrizione: descrizione || '', importId });\n}\n\nconst dates = transactions.map(t => t.date).sort();\nconst minDate = dates[0] || new Date().toISOString().split('T')[0];\nreturn [{ json: { transactions, min_date: minDate, total_parsed: transactions.length, chat_id: chatId } }];\n"
|
|
},
|
|
"id": "n_parse",
|
|
"name": "🔄 Parse CSV",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1152,
|
|
160
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const BUDGET_ID = '56c4e1c8-634b-4ebc-8fee-e7f0fe2b4e52';\nconst API_KEY = 'jcGZGgiSTX3ptQr+m3Z61VVzAjFNxeqcJjM266I+S2Vd8QS4wX';\nconst BASE_URL = 'http://actual.home.svc.cluster.local:5007';\nconst H = { 'x-api-key': API_KEY, 'accept': 'application/json' };\n\nconst { transactions, min_date, total_parsed, chat_id } = $input.first().json;\n\n// Retry helper: actual-http-api may need to sync budget on first call\nasync function fetchWithRetry(url, maxAttempts = 3, delayMs = 2000) {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n const res = await this.helpers.httpRequest({ method: 'GET', url, headers: H });\n return res;\n } catch(e) {\n if (i < maxAttempts - 1) {\n await new Promise(r => setTimeout(r, delayMs));\n } else {\n throw e;\n }\n }\n }\n}\n\n// Sequential calls (not parallel) to avoid hitting actual-http-api concurrently on first sync\nconst payeesRes = await fetchWithRetry.call(this, `${BASE_URL}/v1/budgets/${BUDGET_ID}/payees`);\nconst categoriesRes = await fetchWithRetry.call(this, `${BASE_URL}/v1/budgets/${BUDGET_ID}/categories`);\n\nreturn [{ json: {\n transactions,\n total_parsed,\n existing_payees: payeesRes.data || [],\n existing_categories: categoriesRes.data || [],\n chat_id\n}}];\n"
|
|
},
|
|
"id": "n_fetch",
|
|
"name": "📊 Fetch Dati Actual",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1392,
|
|
160
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "cond_empty",
|
|
"leftValue": "={{ $json.total_parsed }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "gt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "n_nonuove",
|
|
"name": "🚫 Nuove Transazioni?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1632,
|
|
160
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"chatId": "={{ $json.chat_id || '-4814221197' }}",
|
|
"text": "={{ '✅ Nessuna nuova transazione da importare — tutte le ' + $json.total_parsed + ' transazioni risultano già presenti su Actual Budget.' }}",
|
|
"additionalFields": {
|
|
"parse_mode": "Markdown"
|
|
}
|
|
},
|
|
"id": "n_tg_niente",
|
|
"name": "📱 Nessuna Tx Nuova",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1920,
|
|
384
|
|
],
|
|
"webhookId": "49aca71e-f36b-4c65-9502-58b07f5f7e9e",
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "uTXHLqcCJxbOvqN3",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"chatId": "={{ $json.chat_id || '-4814221197' }}",
|
|
"text": "={{ $json.message }}",
|
|
"additionalFields": {
|
|
"parse_mode": "Markdown"
|
|
}
|
|
},
|
|
"id": "n_report",
|
|
"name": "📱 Report Telegram",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2224,
|
|
-48
|
|
],
|
|
"webhookId": "872c3805-f7d6-4342-987b-e3d641af693c",
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "uTXHLqcCJxbOvqN3",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "\n// Mark Google Task \"Actual - Estratto conto\" in list \"Finanze\" as completed\n// Uses Calendar OAuth credential via predefinedCredentialType\n// Non-blocking: errors are logged but don't fail the workflow\nconst TASK_NAME = 'Actual - Estratto conto';\nconst TASKLIST_NAME = 'Finanze';\n\ntry {\n // Get task lists\n const listsRes = await this.helpers.requestWithAuthentication.call(\n this,\n 'googleCalendarOAuth2Api',\n {\n method: 'GET',\n url: 'https://tasks.googleapis.com/tasks/v1/users/@me/lists',\n json: true\n }\n );\n \n const taskList = (listsRes.items || []).find(l => l.title === TASKLIST_NAME);\n if (!taskList) {\n console.log(`Task list \"${TASKLIST_NAME}\" not found`);\n return $input.all();\n }\n \n // Get tasks from the list\n const tasksRes = await this.helpers.requestWithAuthentication.call(\n this,\n 'googleCalendarOAuth2Api',\n {\n method: 'GET',\n url: `https://tasks.googleapis.com/tasks/v1/lists/${taskList.id}/tasks?showCompleted=false&showHidden=false`,\n json: true\n }\n );\n \n const task = (tasksRes.items || []).find(t => t.title === TASK_NAME);\n if (!task) {\n console.log(`Task \"${TASK_NAME}\" not found or already completed`);\n return $input.all();\n }\n \n // Mark as completed\n await this.helpers.requestWithAuthentication.call(\n this,\n 'googleCalendarOAuth2Api',\n {\n method: 'PATCH',\n url: `https://tasks.googleapis.com/tasks/v1/lists/${taskList.id}/tasks/${task.id}`,\n body: { status: 'completed' },\n json: true\n }\n );\n \n console.log(`Task \"${TASK_NAME}\" marked as completed`);\n} catch(e) {\n // Non-blocking - just log the error\n console.error('Google Tasks error (non-blocking):', e.message);\n}\n\nreturn $input.all();\n"
|
|
},
|
|
"id": "n_mark_task",
|
|
"name": "✅ Marca Task Completato",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2528,
|
|
-48
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const tg = $('📩 Telegram Trigger').first().json;\nreturn [{ json: {\n file_id: tg.message.document.file_id,\n chat_id: tg.message.chat.id,\n caption: tg.message.caption || ''\n}}];\n"
|
|
},
|
|
"id": "n_prepara_file",
|
|
"name": "📋 Prepara File ID",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
688,
|
|
160
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const BUDGET_ID = '56c4e1c8-634b-4ebc-8fee-e7f0fe2b4e52';\nconst ACCOUNT_ID = '7558ab93-e795-4b3b-9766-5a6d47d99f92';\nconst API_KEY = 'jcGZGgiSTX3ptQr+m3Z61VVzAjFNxeqcJjM266I+S2Vd8QS4wX';\nconst BASE_URL = 'http://actual.home.svc.cluster.local:5007';\nconst EXPENSE_GROUP = 'fc3825fd-b982-4b72-b768-5b30844cf832';\nconst INCOME_GROUP = '2E1F5BDB-209B-43F9-AF2C-3CE28E380C00';\nconst BATCH_SIZE = 30;\n\nconst H_JSON = { 'x-api-key': API_KEY, 'content-type': 'application/json', 'accept': 'application/json' };\nconst H_GET = { 'x-api-key': API_KEY, 'accept': 'application/json' };\n\nconst { transactions, total_parsed, chat_id } = $input.first().json;\nconst copilotToken = $('🔑 Ottieni Token Copilot').first().json.token;\n\n// ── 1. Fetch payees & categories ──────────────────────────────────────────\nasync function apiGet(path) {\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n return await this.helpers.httpRequest({ method: 'GET', url: `${BASE_URL}${path}`, headers: H_GET });\n } catch(e) {\n if (attempt < 2) await new Promise(r => setTimeout(r, 2000));\n else throw e;\n }\n }\n}\n\nconst [payeesRes, catsRes] = await Promise.all([\n apiGet.call(this, `/v1/budgets/${BUDGET_ID}/payees`),\n apiGet.call(this, `/v1/budgets/${BUDGET_ID}/categories`),\n]);\n\nlet existingPayees = payeesRes.data || [];\nlet existingCategories = catsRes.data || [];\n\nconst payeeNameMap = () => Object.fromEntries(existingPayees.map(p => [p.name.toLowerCase(), p.id]));\nconst catNameMap = () => Object.fromEntries(existingCategories.map(c => [c.name.toLowerCase(), c.id]));\n\n// ── 2. Loop batches ───────────────────────────────────────────────────────\nlet totalAdded = 0, totalUpdated = 0;\nconst allNewPayeeNames = [], allNewCatNames = [];\n\nfor (let batchStart = 0; batchStart < transactions.length; batchStart += BATCH_SIZE) {\n const batch = transactions.slice(batchStart, batchStart + BATCH_SIZE);\n const batchIndex = Math.floor(batchStart / BATCH_SIZE);\n\n // Build GPT prompt\n const payeeList = existingPayees.map(p => `${p.id}|${p.name}`).join('\\n');\n const catList = existingCategories.map(c => `${c.id}|${c.name}|${c.is_income ? 'entrata' : 'uscita'}`).join('\\n');\n const txList = batch.map((t, i) =>\n `${i+1}. ${t.date} | ${(t.amount/100).toFixed(2)}€ | ${t.causale} | ${(t.descrizione||'').substring(0,120)}`\n ).join('\\n');\n\n const userPrompt = `Classifica le seguenti transazioni bancarie per il budget personale di Martin.\n\nPAYEE ESISTENTI (id|nome):\n${payeeList}\n\nCATEGORIE ESISTENTI (id|nome|tipo):\n${catList}\n\nTRANSAZIONI (indice. data | importo | causale | descrizione):\n${txList}\n\nRispondi SOLO con JSON valido in questo formato:\n{\n \"new_payees\": [\"Nome Payee da creare\"],\n \"new_categories\": [{\"name\": \"Nome Categoria\", \"is_income\": false}],\n \"transactions\": [\n {\"index\": 1, \"payee_id\": \"uuid-esistente-o-null\", \"payee_name\": \"nome pulito\", \"category_id\": \"uuid-esistente-o-null\", \"category_name\": \"nome categoria\", \"notes\": \"\"}\n ]\n}\n\nRegole:\n- payee_id: UUID dalla lista se corrisponde, altrimenti null e aggiungi a new_payees\n- category_id: UUID dalla lista se corrisponde, altrimenti null e aggiungi a new_categories\n- payee_name e category_name SEMPRE valorizzati\n- Importi negativi = uscite (categorie tipo \"uscita\")\n- Importi positivi = entrate (categorie tipo \"entrata\")\n- PAGAMENTO MASTERCARD: classifica per contenuto descrizione\n- PRELIEVO ATM/CASSA → categoria Prelievo\n- ADDEBITO SDD/utenze → categoria Bollette\n- Nomi payee leggibili (Glovo non GLOVO ITALIA SRL)\n- Consistenza: stesso esercente = stesso payee sempre`;\n\n // Call GPT\n let gptResult = { new_payees: [], new_categories: [], transactions: [] };\n try {\n const gptRes = await this.helpers.httpRequest({\n method: 'POST',\n url: 'https://api.githubcopilot.com/chat/completions',\n headers: {\n 'Authorization': `Bearer ${copilotToken}`,\n 'editor-version': 'vscode/1.95.3',\n 'Copilot-Integration-Id': 'vscode-chat',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n model: 'gpt-4.1',\n messages: [\n { role: 'system', content: 'Sei un assistente per finanze personali. Rispondi SEMPRE con JSON valido e niente altro.' },\n { role: 'user', content: userPrompt }\n ],\n temperature: 0.1,\n response_format: { type: 'json_object' }\n })\n });\n const content = gptRes.choices?.[0]?.message?.content || '{}';\n gptResult = JSON.parse(content);\n } catch(e) {\n // If GPT fails for this batch, skip it (don't fail the whole workflow)\n console.error(`Batch ${batchIndex} GPT error: ${e.message}`);\n continue;\n }\n\n const { new_payees = [], new_categories = [], transactions: classified = [] } = gptResult;\n\n // Create missing payees\n const pMap = payeeNameMap();\n const createdPayees = {};\n for (const payeeName of new_payees) {\n const existId = pMap[payeeName.toLowerCase()];\n if (existId) { createdPayees[payeeName] = existId; continue; }\n try {\n const res = await this.helpers.httpRequest({\n method: 'POST', url: `${BASE_URL}/v1/budgets/${BUDGET_ID}/payees`, headers: H_JSON,\n body: JSON.stringify({ payee: { name: payeeName } })\n });\n const newId = res.data;\n createdPayees[payeeName] = newId;\n existingPayees.push({ id: newId, name: payeeName, is_income: false });\n allNewPayeeNames.push(payeeName);\n } catch(e) { createdPayees[payeeName] = pMap[payeeName.toLowerCase()] || null; }\n }\n\n // Create missing categories\n const cMap = catNameMap();\n const createdCategories = {};\n for (const cat of new_categories) {\n const existId = cMap[cat.name.toLowerCase()];\n if (existId) { createdCategories[cat.name] = existId; continue; }\n const groupId = cat.is_income ? INCOME_GROUP : EXPENSE_GROUP;\n try {\n const res = await this.helpers.httpRequest({\n method: 'POST', url: `${BASE_URL}/v1/budgets/${BUDGET_ID}/categories`, headers: H_JSON,\n body: JSON.stringify({ category: { name: cat.name, group_id: groupId } })\n });\n const newId = res.data;\n createdCategories[cat.name] = newId;\n existingCategories.push({ id: newId, name: cat.name, is_income: cat.is_income || false });\n allNewCatNames.push(cat.name);\n } catch(e) { createdCategories[cat.name] = cMap[cat.name.toLowerCase()] || null; }\n }\n\n // Map classified transactions\n const freshPMap = payeeNameMap();\n const freshCMap = catNameMap();\n const toImport = classified.map((c, i) => {\n const tx = batch[c.index - 1] ?? batch[i];\n if (!tx) return null;\n const payeeId = (c.payee_id && existingPayees.find(p => p.id === c.payee_id))\n ? c.payee_id\n : (createdPayees[c.payee_name] || freshPMap[c.payee_name?.toLowerCase()]);\n const catId = (c.category_id && existingCategories.find(cat => cat.id === c.category_id))\n ? c.category_id\n : (createdCategories[c.category_name] || freshCMap[c.category_name?.toLowerCase()]);\n return {\n account: ACCOUNT_ID, date: tx.date, amount: tx.amount,\n ...(payeeId ? { payee: payeeId } : { payee_name: c.payee_name || tx.descrizione?.substring(0,50) || 'Sconosciuto' }),\n ...(catId ? { category: catId } : {}),\n notes: c.notes || '', imported_id: tx.importId, cleared: true\n };\n }).filter(Boolean);\n\n if (toImport.length > 0) {\n try {\n const importRes = await this.helpers.httpRequest({\n method: 'POST',\n url: `${BASE_URL}/v1/budgets/${BUDGET_ID}/accounts/${ACCOUNT_ID}/transactions/import`,\n headers: H_JSON,\n body: JSON.stringify({ transactions: toImport })\n });\n totalAdded += importRes.data?.added?.length || 0;\n totalUpdated += importRes.data?.updated?.length || 0;\n } catch(e) {\n console.error(`Batch ${batchIndex} import error: ${e.message}`);\n }\n }\n}\n\n// ── 3. Build report message ───────────────────────────────────────────────\nlet msg = '💰 *Estratto Conto Importato*\\n\\n';\nmsg += `✅ Nuove: *${totalAdded}* transazioni\\n`;\nif (totalUpdated > 0) msg += `⏭️ Già presenti: *${totalUpdated}*\\n`;\nmsg += `📊 Totale CSV: *${total_parsed || 0}*`;\n\nreturn [{ json: { message: msg, chat_id: chat_id || -4814221197, total_parsed, totalAdded } }];"
|
|
},
|
|
"id": "n_classifica",
|
|
"name": "🧠 Classifica e Importa",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1920,
|
|
-48
|
|
]
|
|
}
|
|
],
|
|
"connections": {
|
|
"📩 Telegram Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "🔍 È un Estratto?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"🔍 È un Estratto?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "🔑 Ottieni Token Copilot",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"🔑 Ottieni Token Copilot": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "📋 Prepara File ID",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"📥 Scarica File Telegram": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "🔄 Parse CSV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"🔄 Parse CSV": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "📊 Fetch Dati Actual",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"📊 Fetch Dati Actual": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "🚫 Nuove Transazioni?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"🚫 Nuove Transazioni?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "🧠 Classifica e Importa",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "📱 Nessuna Tx Nuova",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"📱 Report Telegram": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "✅ Marca Task Completato",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"📋 Prepara File ID": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "📥 Scarica File Telegram",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"🧠 Classifica e Importa": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "📱 Report Telegram",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"callerPolicy": "workflowsFromSameOwner",
|
|
"availableInMCP": false
|
|
},
|
|
"triggerCount": 1,
|
|
"versionId": "43101c9f-8626-4d1c-8c48-2f2f467a4e17",
|
|
"owner": {
|
|
"type": "personal",
|
|
"projectId": "Hdttz401OqqtObPo",
|
|
"projectName": "Martin Tahiraj <tahiraj.martin@gmail.com>",
|
|
"personalEmail": "tahiraj.martin@gmail.com"
|
|
},
|
|
"parentFolderId": null,
|
|
"isArchived": false
|
|
} |