{
  "name": "weekly report test",
  "nodes": [
    {
      "parameters": {
        "jsCode": "const now = new Date(); // n8n уже в нужной TZ, если настроен\nconst day = now.getDay(); // 0 - воскресенье, 1 - понедельник ...\n// найдём понедельник этой недели:\nconst diffToMonday = (day + 6) % 7; \nconst monday = new Date(now);\nmonday.setDate(now.getDate() - diffToMonday);\n// конец недели - воскресенье:\nconst sunday = new Date(monday);\nsunday.setDate(monday.getDate() + 6);\n\nreturn [{\n  weekStart: monday.toISOString().slice(0,10), // 'YYYY-MM-DD'\n  weekEnd: sunday.toISOString().slice(0,10)\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        496,
        -80
      ],
      "id": "84b381bd-906f-433e-80d2-c017fd6077d1",
      "name": "WeekRange"
    },
    {
      "parameters": {
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "value": "ble1AoPSImwhtECI",
          "mode": "list",
          "cachedResultName": "VariablesFinance",
          "cachedResultUrl": "/projects/0Ib0uCxwfuMKI6HN/datatables/ble1AoPSImwhtECI"
        },
        "filters": {
          "conditions": [
            {
              "keyName": "Key",
              "keyValue": "RowStartFinAlalizeCost"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Value": "={{ $json.transactions[0].row_number }}",
            "Key": "RowStartFinAlalizeCost"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Key",
              "displayName": "Key",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "Value",
              "displayName": "Value",
              "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": [
        1280,
        -80
      ],
      "id": "36e54ca5-cad6-44a7-8425-d7917170eda2",
      "name": "Upsert row(s)"
    },
    {
      "parameters": {
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "value": "ble1AoPSImwhtECI",
          "mode": "list",
          "cachedResultName": "VariablesFinance",
          "cachedResultUrl": "/projects/0Ib0uCxwfuMKI6HN/datatables/ble1AoPSImwhtECI"
        },
        "filters": {
          "conditions": [
            {
              "keyName": "Key",
              "keyValue": "RowStartFinAlalizeIn"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Value": "={{ $json.transactions[0].row_number }}",
            "Key": "RowStartFinAlalizeIn"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Key",
              "displayName": "Key",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "Value",
              "displayName": "Value",
              "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": [
        1280,
        160
      ],
      "id": "c890c2a4-a79d-4f5a-9373-9895b412a226",
      "name": "Upsert row(s)1"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1Zl8_Q-t6s3hN8yr2Pn85_8cwk0iNV5qDOvAglVmVMdo",
          "mode": "list",
          "cachedResultName": "Семейный бюджет (Test)",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Zl8_Q-t6s3hN8yr2Pn85_8cwk0iNV5qDOvAglVmVMdo/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 598940202,
          "mode": "list",
          "cachedResultName": "Расходы",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Zl8_Q-t6s3hN8yr2Pn85_8cwk0iNV5qDOvAglVmVMdo/edit#gid=598940202"
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "firstDataRow": "={{ $json.variableStartRowCost }}"
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        896,
        -80
      ],
      "id": "e6df379b-99f5-462f-982b-626fe4ff598c",
      "name": "Get row(s) in sheet Cost",
      "alwaysOutputData": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "hHZp1V422Hra92yA",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1Zl8_Q-t6s3hN8yr2Pn85_8cwk0iNV5qDOvAglVmVMdo",
          "mode": "list",
          "cachedResultName": "Семейный бюджет (Test)",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Zl8_Q-t6s3hN8yr2Pn85_8cwk0iNV5qDOvAglVmVMdo/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 1490011688,
          "mode": "list",
          "cachedResultName": "Доходы",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Zl8_Q-t6s3hN8yr2Pn85_8cwk0iNV5qDOvAglVmVMdo/edit#gid=1490011688"
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "firstDataRow": "={{ $json.variableStartRowIn }}"
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        896,
        160
      ],
      "id": "2d74825c-6c43-41cb-b57b-1abd16b9e891",
      "name": "Get row(s) in sheet In",
      "alwaysOutputData": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "hHZp1V422Hra92yA",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7aadea42-e6d3-49ad-9ba3-75e5cd07cf3b",
              "name": "variableStartRowIn",
              "value": "={{ $('Set variables Finance').item.json.RowStartFinAlalizeIn }}",
              "type": "number"
            },
            {
              "id": "27d435b7-4dde-4de9-84c9-0214e6555696",
              "name": "variableStartRowCost",
              "value": "={{ $('Set variables Finance').item.json.RowStartFinAlalizeCost }}",
              "type": "string"
            },
            {
              "id": "3fc97901-0c92-43ce-90ac-72221db568d3",
              "name": "weekStart",
              "value": "={{ $json.weekStart }}",
              "type": "string"
            },
            {
              "id": "85a3cef1-c5e6-4cd0-bafa-4f015b9c6cf5",
              "name": "weekEnd",
              "value": "={{ $json.weekEnd }}",
              "type": "string"
            },
            {
              "id": "75f803bc-66fe-41c0-9659-8b83f9ced6ac",
              "name": "chatId",
              "value": "={{ $('Set variables Finance').item.json.chatId }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        672,
        -80
      ],
      "id": "2cc74d93-83d0-4e0e-9418-c4abb97a054b",
      "name": "Params"
    },
    {
      "parameters": {
        "jsCode": "const input = items; // данные из Sheets\n\n// Берём диапазон недели из ноды WeekRange\nconst weekStart = $node['Params'].json.weekStart;\nconst weekEnd = $node['Params'].json.weekEnd;\n\nconst start = new Date(weekStart);\nconst end = new Date(weekEnd);\n\nfunction parseDate(raw) {\n  if (raw == null || raw === '') return null;\n\n  // приводим к строке и убираем пробелы\n  const str = String(raw).trim();\n\n  // ожидаем формат \"08.01.25\"\n  const parts = str.split('.');\n  if (parts.length < 3) {\n    // например, заголовок \"Текущая дата\" и т.п.\n    return null;\n  }\n\n  let [dd, mm, yy] = parts;\n\n  if (yy.length === 2) {\n    yy = '20' + yy; // \"25\" -> \"2025\"\n  }\n\n  dd = dd.padStart(2, '0');\n  mm = mm.padStart(2, '0');\n\n  const iso = `${yy}-${mm}-${dd}`; // \"2025-01-08\"\n  const d = new Date(iso);\n  if (isNaN(d)) return null;\n\n  return d;\n}\n\nconst tx = input\n  .map(i => i.json)\n  .filter(row => {\n    const d = parseDate(row['Текущая дата']);\n    const uuid = row['UUID'];\n    if (!d) return false;      // заголовок/пустые строки пролетают\n    //if (uuid == \"\") return false;\n    return d >= start && d <= end;\n  });\n\nreturn [{\n  json: {\n    transactions: tx,\n    weekStart,\n    weekEnd,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1088,
        -80
      ],
      "id": "c321268b-b0b0-4680-807d-1ee68e7c0b11",
      "name": "Filter by data (Cost)"
    },
    {
      "parameters": {
        "jsCode": "const input = items; // данные из Sheets\n\n// Берём диапазон недели из ноды WeekRange\nconst weekStart = $node['Params'].json.weekStart;\nconst weekEnd = $node['Params'].json.weekEnd;\n\nconst start = new Date(weekStart);\nconst end = new Date(weekEnd);\n\nfunction parseDate(raw) {\n  if (raw == null || raw === '') return null;\n\n  // приводим к строке и убираем пробелы\n  const str = String(raw).trim();\n\n  // ожидаем формат \"08.01.25\"\n  const parts = str.split('.');\n  if (parts.length < 3) {\n    // например, заголовок \"Текущая дата\" и т.п.\n    return null;\n  }\n\n  let [dd, mm, yy] = parts;\n\n  if (yy.length === 2) {\n    yy = '20' + yy; // \"25\" -> \"2025\"\n  }\n\n  dd = dd.padStart(2, '0');\n  mm = mm.padStart(2, '0');\n\n  const iso = `${yy}-${mm}-${dd}`; // \"2025-01-08\"\n  const d = new Date(iso);\n  if (isNaN(d)) return null;\n\n  return d;\n}\n\nconst tx = input\n  .map(i => i.json)\n  .filter(row => {\n    const d = parseDate(row['Текущая дата']);\n    const uuid = row['UUID'];\n    if (!d) return false;      // заголовок/пустые строки пролетают\n  //  if (uuid == \"\") return false;\n    return d >= start && d <= end;\n  });\n\nreturn [{\n  json: {\n    transactions: tx,\n    weekStart,\n    weekEnd,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        160
      ],
      "id": "9cb9726e-01dc-4f86-8752-ffbb51344247",
      "name": "Filter by data (In)"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        1504,
        16
      ],
      "id": "7112b61c-58d5-4b6b-a3e6-db93298ec0e7",
      "name": "Merge1"
    },
    {
      "parameters": {
        "jsCode": "function catKey(name) {\n  if (!name) return '';\n  return String(name).trim().toLowerCase();\n}\n\n// Берём данные прямо из нод фильтрации\nconst costNode = $node['Filter by data (Cost)'];\nconst inNode   = $node['Filter by data (In)'];\n\n// Массивы транзакций\nconst costTx = (costNode.json.transactions || []);\nconst inTx   = (inNode.json.transactions || []);\n\n// Диапазон недели — можно взять из одной из нод (они одинаковые)\nconst weekStart = costNode.json.weekStart;\nconst weekEnd   = costNode.json.weekEnd;\n\n// Итоговые суммы\nlet totalIncome  = 0;\nlet totalExpense = 0;\n\n// Группировка по категориям и по дням\n// byCategory: { \"Продукты\": { income: 0, expense: 12345 }, ... }\n// byDay:      { \"01.12.25\": { income: 1000, expense: 500 }, ... }\nconst byCategory = {};\nconst byDay = {};\n\nfunction addTx(t, type) {\n  const amount = Number(t['Сумма']) || 0;\n  const cat = catKey(t['Категория']) || 'Без категории';\n  const day = t['Текущая дата'] || 'unknown';\n\n  if (type === 'income') {\n    totalIncome += amount;\n  } else {\n    totalExpense += amount;\n  }\n\n  if (!byCategory[cat]) {\n    byCategory[cat] = { income: 0, expense: 0 };\n  }\n  byCategory[cat][type] += amount;\n\n  if (!byDay[day]) {\n    byDay[day] = { income: 0, expense: 0 };\n  }\n  byDay[day][type] += amount;\n}\n\n// Добавляем все расходы и доходы\ncostTx.forEach(t => addTx(t, 'expense'));\ninTx.forEach(t => addTx(t, 'income'));\n\n// Топы\nconst topExpenses = [...costTx]\n  .sort((a, b) => Number(b['Сумма'] || 0) - Number(a['Сумма'] || 0))\n  .slice(0, 5);\n\nconst topIncome = [...inTx]\n  .sort((a, b) => Number(b['Сумма'] || 0) - Number(a['Сумма'] || 0))\n  .slice(0, 5);\n\n// Результат — один item с общей сводкой\nreturn [\n  {\n    json: {\n      weekStart,\n      weekEnd,\n      income: totalIncome,\n      expense: totalExpense,\n      balance: totalIncome - totalExpense,\n      byCategory,\n      byDay,\n      topExpenses,\n      topIncome,\n    },\n  },\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        16
      ],
      "id": "35f4e523-ba44-4d51-8e19-42416a2fa217",
      "name": "WeeklyAggregate"
    },
    {
      "parameters": {
        "chatId": "={{ $('Params').first().json.chatId }}",
        "text": "={{ $json.html }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        2176,
        16
      ],
      "id": "3eba778b-dff8-4f3d-85d7-76322ca4da53",
      "name": "Send a text message",
      "webhookId": "9ecfb03b-b441-464e-ae14-3d20150ecb7e",
      "credentials": {
        "telegramApi": {
          "id": "kWZPSKCyBNQSK019",
          "name": "Test finance bot"
        }
      }
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "value": "ble1AoPSImwhtECI",
          "mode": "list",
          "cachedResultName": "VariablesFinance",
          "cachedResultUrl": "/projects/0Ib0uCxwfuMKI6HN/datatables/ble1AoPSImwhtECI"
        },
        "returnAll": true
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1,
      "position": [
        -80,
        -80
      ],
      "id": "c2a2e4f5-d9e2-4757-8a1a-66d3bd83eede",
      "name": "Get variables Finance",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "// ===== 1. ДЕФОЛТНЫЕ ЗНАЧЕНИЯ ПЕРЕМЕННЫХ =====\n// Здесь задаёшь все параметры и их значения \"по умолчанию\"\nconst defaults = {\n  RowStartFinAlalizeCost: 4,  // если нет в таблице — берём 1\n  RowStartFinAlalizeIn: 4,    // если нет в таблице — берём 1\n  chatId: null,               // если нет — останется null\n  // Можно добавлять свои:\n  // limitPerWeek: 10000,\n};\n\n// ===== 2. СЧИТЫВАЕМ ВСЕ СТРОКИ ИЗ ПРЕДЫДУЩЕЙ НОДЫ =====\nconst rows = items.map(i => i.json); // то, что вернула нода DataTables\n\n// Копируем defaults, чтобы не портить исходный объект\nconst params = { ...defaults };\n\n// ===== 3. Накладываем значения из таблицы поверх дефолтов =====\nfor (const row of rows) {\n  const key = row.Key;\n  const rawValue = row.Value;\n\n  if (!key) continue;\n\n  // Если для этого ключа есть дефолт — приводим тип к нему\n  if (Object.prototype.hasOwnProperty.call(defaults, key)) {\n    const defVal = defaults[key];\n\n    if (typeof defVal === 'number') {\n      const num = Number(rawValue);\n      params[key] = Number.isFinite(num) ? num : defVal;\n    } else if (typeof defVal === 'boolean') {\n      const v = String(rawValue).toLowerCase();\n      params[key] = v === 'true' || v === '1' || v === 'yes';\n    } else {\n      // строка / null / что угодно ещё — просто берём как есть\n      params[key] = rawValue;\n    }\n  } else {\n    // Ключа не было в defaults — просто добавим как строку\n    params[key] = rawValue;\n  }\n}\n\n// ===== 4. Возвращаем один item с объектом params =====\nreturn [\n  {\n    json: params,\n  },\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        128,
        -80
      ],
      "id": "ea1ca93e-1435-4c61-aa33-75ac82307e5e",
      "name": "Set variables Finance"
    },
    {
      "parameters": {
        "jsCode": "const data = $node['WeeklyAggregate'].json;\nconst rowJsonData = $node['Set variables Finance']?.json || {};\nconst currencyName = rowJsonData.currencyName || '₽';\n\nconst {\n  weekStart,\n  weekEnd,\n  income,\n  expense,\n  balance,\n  byCategory,\n  topExpenses,\n  topIncome,\n} = data;\n\nconst totalExpense = expense || 1;\n\nfunction catKey(name) {\n  if (!name) return '';\n  return String(name).trim().toLowerCase();\n}\n\n// Форматирование ₽\nfunction rub(n) {\n  const num = Number(n) || 0;\n  return new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(num);\n}\n\n// ---------- ТОПЫ ЗА ДЕНЬ ----------\nconst filtered = Object.fromEntries(\n  Object.entries(byCategory).filter(([cat, val]) => (val.expense || 0) > 0)\n);\n// Категорий по расходам\nconst catLines = Object.entries(filtered || {})\n  .sort(([, a], [, b]) => (b.expense || 0) - (a.expense || 0))\n  .map(([name, v]) => {\n    const share = (v.expense || 0) / totalExpense * 100;\n    return `• <b>${name}</b>: ${rub((v.expense || 0).toFixed(0))} ₽ (${share.toFixed(1)}%)`;\n  })\n  .join('\\n');\n\n// Топ трат\nconst topExpLines = (topExpenses || [])\n  .map(t => {\n    const date = t['Текущая дата'] || '';\n    const cat = t['Категория'] || '';\n    const amount = rub(Number(t['Сумма'] || 0).toFixed(0));\n    const desc = t['Описание'] ? ` — ${t['Описание']}` : '';\n    return `• ${date}, <i>${cat}</i>: <b>${amount} ₽</b>${desc}`;\n  })\n  .join('\\n');\n\n// Топ доходов\nconst topIncLines = (topIncome || [])\n  .map(t => {\n    const date = t['Текущая дата'] || '';\n    const cat = t['Категория'] || '';\n    const amount = rub(Number(t['Сумма'] || 0).toFixed(0));\n    const desc = t['Описание'] ? ` — ${t['Описание']}` : '';\n    return `• ${date}, <i>${cat}</i>: <b>${amount} ₽</b>${desc}`;\n  })\n  .join('\\n');\n\n// Savings rate за день\nconst savingsRate = income > 0 ? (1 - expense / income) * 100 : 0;\n\n// ---------- АГРЕГИРОВАННЫЙ JSON ПО ГОДУ (obj) ----------\n\n// В YearAggregates лежит JSON-строка с агрегатами\nconst raw = rowJsonData.YearAggregates || '{}';\n\nlet obj;\ntry {\n  obj = JSON.parse(raw);\n} catch (e) {\n  obj = {};\n}\n\n// ---------- МЕСЯЧНЫЙ БЛОК: ТЕКУЩИЙ МЕСЯЦ ----------\n\n// 1. Текущий месяц из даты today (формат MM.DD.YY или DD.MM.YYYY)\nconst [yyyy, mm, dd] = weekStart.split('-');\nconst weekStartFormat = `${dd}.${mm}.${yyyy}`;\nconst [yyyyEnd, mmEnd, ddEnd] = weekEnd.split('-');\nconst weekEndFormat = `${ddEnd}.${mmEnd}.${yyyyEnd}`;\nconst monthNum = Number(mm);\n\n// 2. Месяц -> имя как в агрегатах\nconst monthNames = {\n  1: 'январь',\n  2: 'февраль',\n  3: 'март',\n  4: 'апрель',\n  5: 'май',\n  6: 'июнь',\n  7: 'июль',\n  8: 'август',\n  9: 'сентябрь',\n  10: 'октябрь',\n  11: 'ноябрь',\n  12: 'декабрь',\n};\nconst monthName = monthNames[monthNum];\n\n// 3. Данные по месяцу из агрегированного JSON\nconst monthDataExp = obj.expenses?.byMonth?.[monthName] || { total: 0, categories: {} };\nconst monthDataIn  = obj.incomes?.byMonth?.[monthName]  || { total: 0, categories: {} };\n\n// Сколько потрачено / заработано за текущий месяц\nconst totalMonthExpense = Number(monthDataExp.total || 0);\nconst totalMonthIncome  = Number(monthDataIn.total || 0);\n\n// 4. Проверка категорий на превышение плана (по месяцу)\nconst planByCat = obj.expenses?.planByCategory || {};\nconst overLimit = [];   // план уже превышен\nconst nearLimit = [];   // ≥ 90% плана\n\nfor (const [catKeyName, planVal] of Object.entries(planByCat)) {\n  const plan = Number(planVal) || 0;\n  if (plan <= 0) continue;\n\n  const spentMonth = Number(monthDataExp.categories?.[catKeyName] || 0);\n  if (spentMonth <= 0) continue;\n\n  const ratio = spentMonth / plan;\n  const diff = spentMonth - plan;\n  const percent = ratio * 100;\n\n  const displayName = catKeyName.charAt(0).toUpperCase() + catKeyName.slice(1);\n\n  const info = {\n    key: catKeyName,\n    name: displayName,\n    plan,\n    spent: spentMonth,\n    diff,\n    percent,\n  };\n\n  if (ratio > 1.0) {\n    overLimit.push(info);\n  } else if (ratio >= 0.9) {\n    nearLimit.push(info);\n  }\n}\n\n// ---------- 5. AI-блок (JSON от модели) ----------\n\nlet aiRaw = '';\n\ntry {\n  const aiNode = $node['AI Advice'].json;\n  const msg = aiNode.output && aiNode.output[0];\n  const firstContent = msg && msg.content && msg.content[0];\n\n  if (firstContent && typeof firstContent.text === 'string') {\n    aiRaw = firstContent.text;\n  }\n} catch (e) {\n  aiRaw = '';\n}\n\nlet aiSummary = '';\nlet aiRecommendations = [];\n\nif (aiRaw) {\n  try {\n    let jsonStr = aiRaw.trim();\n    const m = jsonStr.match(/\\{[\\s\\S]*\\}/);\n    if (m) {\n      jsonStr = m[0];\n    }\n\n    const aiObj = JSON.parse(jsonStr);\n\n    if (aiObj && typeof aiObj === 'object') {\n      if (typeof aiObj.summary === 'string') {\n        aiSummary = aiObj.summary.trim();\n      }\n      if (Array.isArray(aiObj.recommendations)) {\n        aiRecommendations = aiObj.recommendations\n          .map(r => String(r).trim())\n          .filter(r => r.length > 0);\n      }\n    }\n  } catch (e) {\n    // JSON не распарсился — просто игнорируем\n  }\n}\n\n// ---------- 6. Собираем HTML ----------\n\nlet html = '';\n\n// Итоги дня\nhtml += `<b>💰 Итоги недели (${weekStartFormat} – ${weekEndFormat})</b>\\n`;\nhtml += `Приход: <b>${rub(income.toFixed(0))} ${currencyName}</b>\\n`;\nhtml += `Расход: <b>${rub(expense.toFixed(0))} ${currencyName}</b>\\n`;\nhtml += `Баланс: <b>${balance >= 0 ? '+' : ''}${rub(balance.toFixed(0))} ${currencyName}</b>\\n`;\nhtml += `Сбережения: <b>${savingsRate.toFixed(1)}%</b>\\n`;\nhtml += `\\n`;\n\n// Категории недели\nif (catLines) {\n  html += `<b>📊 Категории расходов за неделю</b>\\n`;\n  html += `${catLines}\\n\\n`;\n}\n\n// Топы недели\nif (topExpLines) {\n  html += `<b>🔥 Топ трат за неделю</b>\\n`;\n  html += `${topExpLines}\\n\\n`;\n}\n\nif (topIncLines) {\n  html += `<b>📈 Топ доходов за неделю</b>\\n`;\n  html += `${topIncLines}\\n\\n`;\n}\n\n// Итоги месяца\nhtml += `<b>📅 Итоги за текущий месяц (${monthName})</b>\\n`;\nhtml += `Расходы за месяц: <b>${rub(totalMonthExpense)}</b> ${currencyName}\\n`;\nhtml += `Доходы за месяц: <b>${rub(totalMonthIncome)}</b> ${currencyName}\\n`;\nhtml += `\\n`;\n\n// Категории с превышением / почти лимитом\nif (overLimit.length || nearLimit.length) {\n  html += `<b>⚠️ Категории, где план по месяцу уже почти/уже превышен</b>\\n`;\n\n  if (overLimit.length) {\n    html += `<i>План уже превышен:</i>\\n`;\n    for (const c of overLimit) {\n      html += `• <b>${c.name}</b>: план ${rub(c.plan)} ${currencyName}, факт ${rub(c.spent)} ${currencyName} (перерасход ${rub(c.diff)} ${currencyName})\\n`;\n    }\n  }\n\n  if (nearLimit.length) {\n    html += `\\n<i>Почти выбрали лимит (&ge; 90% плана):</i>\\n`;\n    for (const c of nearLimit) {\n      const left = c.plan - c.spent;\n      html += `• <b>${c.name}</b>: план ${rub(c.plan)} ${currencyName}, факт ${rub(c.spent)} ${currencyName} (осталось ~${rub(left)} ${currencyName})\\n`;\n    }\n  }\n\n  html += `\\n`;\n}\n\n// Рекомендации от AI\nif (aiSummary || aiRecommendations.length) {\n  html += `<b>💡 Рекомендации</b>\\n`;\n  if (aiSummary) {\n    html += `${aiSummary}\\n`;\n  }\n  if (aiRecommendations.length) {\n    for (const rec of aiRecommendations) {\n      html += `• ${rec}\\n`;\n    }\n  }\n  html += `\\n`;\n}\n\nreturn [\n  {\n    json: {\n      html,\n      aiRaw,\n      aiSummary,\n      aiRecommendations,\n      monthName,\n      totalMonthExpense,\n      totalMonthIncome,\n      overLimit,\n      nearLimit,\n    },\n  },\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1936,
        16
      ],
      "id": "6b52f967-d71f-4883-919d-49bc6c10a5aa",
      "name": "Prepare HTML"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtHour": 11,
              "triggerAtMinute": 30
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        -304,
        -80
      ],
      "id": "e4d659a2-db25-4b42-a50a-f986257c630f",
      "name": "Schedule Trigger"
    }
  ],
  "pinData": {},
  "connections": {
    "WeekRange": {
      "main": [
        [
          {
            "node": "Params",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert row(s)": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert row(s)1": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Get row(s) in sheet Cost": {
      "main": [
        [
          {
            "node": "Filter by data (Cost)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get row(s) in sheet In": {
      "main": [
        [
          {
            "node": "Filter by data (In)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Params": {
      "main": [
        [
          {
            "node": "Get row(s) in sheet Cost",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get row(s) in sheet In",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter by data (Cost)": {
      "main": [
        [
          {
            "node": "Upsert row(s)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter by data (In)": {
      "main": [
        [
          {
            "node": "Upsert row(s)1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge1": {
      "main": [
        [
          {
            "node": "WeeklyAggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WeeklyAggregate": {
      "main": [
        [
          {
            "node": "Prepare HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get variables Finance": {
      "main": [
        [
          {
            "node": "Set variables Finance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set variables Finance": {
      "main": [
        [
          {
            "node": "WeekRange",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare HTML": {
      "main": [
        [
          {
            "node": "Send a text message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get variables Finance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Novosibirsk",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "errorWorkflow": "uGI41Im58ZqjhMoo"
  },
  "versionId": "056bd803-1ec8-4529-aba0-41b459929d6f",
  "meta": {
    "instanceId": "6b779df2618ebb6bc876b995c5ec4ed47ada0e4a4a97c876e9ad299e510eacb1"
  },
  "id": "ynB01vTQDl1TEF9r",
  "tags": [
    {
      "updatedAt": "2025-08-21T09:01:53.658Z",
      "createdAt": "2025-08-21T09:01:53.658Z",
      "id": "oJKWWf2RDOmYAbjb",
      "name": "AIvDelo"
    }
  ]
}