{
  "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
}