{
  "name": "Control de gastos y recordatorios",
  "nodes": [
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "id": "d8e2e6b3-ca18-4a5b-b6cd-5ffd881b78ee",
      "name": "Telegram Trigger",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        0
      ],
      "webhookId": "d5feb343-d113-482a-ac3c-2d2ded7533ad"
    },
    {
      "parameters": {
        "jsCode": "// ===== Seguridad =====\nconst PIN = 'CAMBIA_ESTE_PIN';      // PIN para /login. Usa uno largo.\nconst HORAS_SESION = 12;            // la sesión se cierra tras estas horas sin uso\nconst MAX_INTENTOS = 5;             // PIN errados antes de bloquear\nconst MINUTOS_BLOQUEO = 15;\nconst CHATS_PERMITIDOS = [];        // opcional: ['123456789'] para ignorar cualquier otro chat\n\nconst msg = $input.first().json.message || {};\nconst chatId = String(msg.chat?.id ?? '');\nconst messageId = msg.message_id;\nconst texto = (msg.text || '').trim();\nif (!chatId) return [];\nif (CHATS_PERMITIDOS.length && !CHATS_PERMITIDOS.includes(chatId)) return [];\n\n// Las sesiones se guardan en los datos estáticos del flujo (solo persisten con el flujo activo).\nconst datos = $getWorkflowStaticData('global');\ndatos.sesiones = datos.sesiones || {};\ndatos.intentos = datos.intentos || {};\nconst ahoraMs = Date.now();\nconst base = { chatId, texto };\nconst responder = (respuesta, accion = 'mensaje') => [{ json: { ...base, accion, respuesta, messageId } }];\n\nconst login = texto.match(/^\\/(login|entrar)(?:\\s+(.+))?$/i);\nif (login) {\n  const it = datos.intentos[chatId] || { n: 0, hasta: 0 };\n  if (it.hasta > ahoraMs) {\n    const min = Math.ceil((it.hasta - ahoraMs) / 60000);\n    return responder(`Demasiados intentos. Prueba de nuevo en ${min} min.`, 'login');\n  }\n  if ((login[2] || '').trim() === PIN && PIN !== 'CAMBIA_ESTE_PIN') {\n    datos.sesiones[chatId] = ahoraMs + HORAS_SESION * 3600000;\n    delete datos.intentos[chatId];\n    return responder(`Sesión iniciada. Se cierra tras ${HORAS_SESION} h sin uso o con /salir.\\nEscribe /ayuda para ver los comandos.`, 'login');\n  }\n  it.n += 1;\n  if (it.n >= MAX_INTENTOS) { it.hasta = ahoraMs + MINUTOS_BLOQUEO * 60000; it.n = 0; }\n  datos.intentos[chatId] = it;\n  return responder('PIN incorrecto.', 'login');\n}\n\nif (/^\\/(salir|logout)\\b/i.test(texto)) {\n  delete datos.sesiones[chatId];\n  return responder('Sesión cerrada.');\n}\n\nif (!(datos.sesiones[chatId] > ahoraMs)) {\n  delete datos.sesiones[chatId];\n  return responder('Hola. Para continuar escribe /login seguido de tu PIN.');\n}\ndatos.sesiones[chatId] = ahoraMs + HORAS_SESION * 3600000;  // renueva la sesión con cada uso\n\nconst ahora = DateTime.now();\nconst hoyDia = ahora.startOf('day');\n\n// ===== Fechas: 15/12, 15/12/2026, 2026-12-15, 15 dic, 15 de diciembre, hoy, mañana, en 10 dias =====\nconst MESES = { ene: 1, feb: 2, mar: 3, abr: 4, may: 5, jun: 6, jul: 7, ago: 8, sep: 9, set: 9, oct: 10, nov: 11, dic: 12 };\nconst fechaValida = (y, mo, d) => {\n  const f = DateTime.fromObject({ year: y, month: mo, day: d });\n  return f.isValid ? f.startOf('day') : null;\n};\nconst armarFecha = (d, mo, y) => {\n  if (y) { y = Number(y); if (y < 100) y += 2000; return fechaValida(y, mo, d); }\n  const f = fechaValida(hoyDia.year, mo, d);  // sin año: la próxima vez que llegue esa fecha\n  return f && f < hoyDia ? fechaValida(hoyDia.year + 1, mo, d) : f;\n};\nconst leerFecha = s => {\n  const t = String(s || '').trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')\n    .replace(/\\s+de\\s+/g, ' ').replace(/\\s+/g, ' ');\n  if (!t) return null;\n  if (t === 'hoy') return hoyDia;\n  if (t === 'manana') return hoyDia.plus({ days: 1 });\n  if (t === 'pasado manana') return hoyDia.plus({ days: 2 });\n  let m = t.match(/^en (\\d+) (dia|dias|semana|semanas|mes|meses)$/);\n  if (m) {\n    const u = m[2].startsWith('dia') ? 'days' : m[2].startsWith('semana') ? 'weeks' : 'months';\n    return hoyDia.plus({ [u]: Number(m[1]) });\n  }\n  m = t.match(/^(\\d{4})-(\\d{1,2})-(\\d{1,2})$/);\n  if (m) return fechaValida(Number(m[1]), Number(m[2]), Number(m[3]));\n  m = t.match(/^(\\d{1,2})[\\/.-](\\d{1,2})(?:[\\/.-](\\d{2,4}))?$/);\n  if (m) return armarFecha(Number(m[1]), Number(m[2]), m[3]);\n  m = t.match(/^(\\d{1,2}) ([a-z]+)(?: (\\d{2,4}))?$/);\n  if (m && MESES[m[2].slice(0, 3)]) return armarFecha(Number(m[1]), MESES[m[2].slice(0, 3)], m[3]);\n  return null;\n};\nconst EJEMPLOS_FECHA = 'Ejemplos: 15/12, 15 dic, mañana, en 10 dias';\n\nconst nuevoPago = fila => [{ json: { ...base, accion: 'nuevopago',\n  Dia: '', FechaVence: '', CuotasTotal: '', CuotasPagadas: '', ...fila } }];\n\n// Gastos e ingresos: guarda las categorías usadas y las ofrece primero como botones (siempre 9)\nconst CATEGORIAS_BASE = ['comida', 'bebidas', 'mercado', 'transporte', 'servicios', 'salud', 'hogar', 'ocio', 'otros'];\nconst CATEGORIAS_INGRESO_BASE = ['quincena', 'sueldo', 'freelance', 'ventas', 'bonos', 'comisiones', 'reembolsos', 'regalos', 'otros'];\ndatos.categorias = datos.categorias || [];\ndatos.categoriasIngreso = datos.categoriasIngreso || [];\nconst usadas = ingreso => ingreso ? datos.categoriasIngreso : datos.categorias;\nconst conocidasDe = ingreso => [...new Set([...usadas(ingreso), ...(ingreso ? CATEGORIAS_INGRESO_BASE : CATEGORIAS_BASE)])];\nconst listaCategorias = ingreso => conocidasDe(ingreso).filter(c => c !== 'otros').slice(0, 8).concat('otros');\nconst nuevoMovimiento = (ingreso, monto, categoria, descripcion) => {\n  const cat = categoria.toLowerCase().trim().slice(0, 30);\n  const lista = [cat, ...usadas(ingreso).filter(c => c !== cat)].slice(0, 20);\n  if (ingreso) datos.categoriasIngreso = lista; else datos.categorias = lista;\n  return [{ json: { ...base, accion: ingreso ? 'ingreso' : 'gasto',\n    Fecha: ahora.toFormat('yyyy-MM-dd'), Monto: monto, Categoria: cat,\n    Descripcion: descripcion || '', Mes: ahora.toFormat('yyyy-MM') } }];\n};\nconst MONTO_RE = /^\\d+(?:[.,]\\d{1,2})?$/;\nconst leerMonto = s => Number(String(s).replace(',', '.'));\n\n// ===== Asistente paso a paso (/gasto, /nuevopago y /recordar) =====\ndatos.asistente = datos.asistente || {};\nconst preguntar = (asis, pregunta) => {\n  asis.expira = ahoraMs + 15 * 60000;\n  datos.asistente[chatId] = asis;\n  return responder(pregunta + '\\n\\nEscribe /cancelar para salir.');\n};\n// Pregunta la categoría con botones (los dibuja el nodo \"Preguntar categoría\")\nconst preguntarCategoria = (asis, pregunta) => {\n  asis.paso = 'g_categoria';\n  asis.opciones = listaCategorias(asis.ingreso);\n  asis.expira = ahoraMs + 15 * 60000;\n  datos.asistente[chatId] = asis;\n  return [{ json: { ...base, accion: 'categoria', opciones: asis.opciones,\n    respuesta: pregunta + '\\nToca una opción o escribe una categoría nueva.\\n\\nEscribe /cancelar para salir.' } }];\n};\nconst SIN_CONCEPTO = ['0', '-', 'no', 'ninguno', 'ninguna'];\n\nif (/^\\/cancelar\\b/i.test(texto)) {\n  const habia = !!datos.asistente[chatId];\n  delete datos.asistente[chatId];\n  return responder(habia ? 'Cancelado. No se guardó nada.' : 'No hay nada que cancelar.');\n}\n\nlet asis = datos.asistente[chatId];\nif (asis && (asis.expira < ahoraMs || texto.startsWith('/'))) {\n  delete datos.asistente[chatId];\n  asis = null;\n}\n\nconst PREGUNTA_CONCEPTO = {\n  gasto: '¿En qué lo gastaste? Por ejemplo: gaseosa\\nEscribe 0 si no quieres poner concepto.',\n  ingreso: '¿De qué es? Por ejemplo: quincena de noviembre\\nEscribe 0 si no quieres poner concepto.',\n};\nif (/^\\/gasto\\s*$/i.test(texto)) {\n  return preguntar({ paso: 'g_monto', fila: {} }, 'Vamos a registrar un gasto.\\n¿Cuánto gastaste? Escribe solo el número, por ejemplo 2.50');\n}\nif (/^\\/ingreso\\s*$/i.test(texto)) {\n  return preguntar({ paso: 'g_monto', fila: {}, ingreso: true }, 'Vamos a registrar un ingreso.\\n¿Cuánto recibiste? Escribe solo el número, por ejemplo 500');\n}\nif (/^\\/nuevopago\\s*$/i.test(texto)) {\n  return preguntar({ paso: 'nombre', fila: {} }, 'Vamos a agregar un pago.\\n¿Cómo se llama? Por ejemplo: Internet');\n}\nif (/^\\/recordar\\s*$/i.test(texto)) {\n  return preguntar({ paso: 'nombre', fila: { Tipo: 'Unico', Monto: 0 }, recordatorio: true },\n    '¿Qué quieres que te recuerde? Por ejemplo: Renovar pasaporte');\n}\n\nif (asis) {\n  const r = texto.trim();\n  const f = asis.fila;\n  switch (asis.paso) {\n    case 'g_monto': {\n      if (!MONTO_RE.test(r)) return preguntar(asis, 'No entendí el monto. Escribe solo el número, por ejemplo 2.50');\n      f.Monto = leerMonto(r);\n      asis.paso = 'g_concepto';\n      return preguntar(asis, PREGUNTA_CONCEPTO[asis.ingreso ? 'ingreso' : 'gasto']);\n    }\n    case 'g_concepto':\n      f.Descripcion = SIN_CONCEPTO.includes(r.toLowerCase()) ? '' : r.slice(0, 100);\n      return preguntarCategoria(asis, '¿En qué categoría?');\n    case 'g_categoria': {\n      const cat = /^\\d+$/.test(r) ? asis.opciones[Number(r) - 1] : r;\n      if (!cat || cat.length < 2) return preguntarCategoria(asis, 'Elige una categoría.');\n      delete datos.asistente[chatId];\n      return nuevoMovimiento(!!asis.ingreso, f.Monto, cat, f.Descripcion || '');\n    }\n    case 'nombre':\n      if (r.length < 2) return preguntar(asis, 'Escribe un nombre de al menos 2 letras.');\n      f.Nombre = r.slice(0, 60);\n      if (asis.recordatorio) {\n        asis.paso = 'fecha';\n        return preguntar(asis, `¿Qué día? ${EJEMPLOS_FECHA}`);\n      }\n      asis.paso = 'monto';\n      return preguntar(asis, '¿Cuánto pagas? Escribe solo el número, por ejemplo 25 o 25.50');\n    case 'monto':\n      if (!/^\\d+(?:[.,]\\d{1,2})?$/.test(r)) return preguntar(asis, 'No entendí el monto. Escribe solo el número, por ejemplo 25 o 25.50');\n      f.Monto = Number(r.replace(',', '.'));\n      asis.paso = 'tipo';\n      return preguntar(asis, '¿Cómo se repite?\\n1. Cada mes\\n2. Una sola vez\\n3. En cuotas\\n\\nResponde 1, 2 o 3.');\n    case 'tipo': {\n      const t = r.toLowerCase();\n      if (t === '1' || t.startsWith('cada') || t.startsWith('mens')) f.Tipo = 'Mensual';\n      else if (t === '2' || t.startsWith('una') || t.startsWith('unic') || t.startsWith('únic')) f.Tipo = 'Unico';\n      else if (t === '3' || t.startsWith('cuota')) f.Tipo = 'Cuotas';\n      else return preguntar(asis, 'Responde 1 (cada mes), 2 (una sola vez) o 3 (en cuotas).');\n      if (f.Tipo === 'Unico') {\n        asis.paso = 'fecha';\n        return preguntar(asis, `¿Qué día vence? ${EJEMPLOS_FECHA}`);\n      }\n      asis.paso = 'dia';\n      return preguntar(asis, '¿Qué día del mes vence? Escribe un número del 1 al 31.');\n    }\n    case 'dia': {\n      const d = Number(r);\n      if (!/^\\d{1,2}$/.test(r) || d < 1 || d > 31) return preguntar(asis, 'Escribe solo el día, un número del 1 al 31.');\n      f.Dia = d;\n      if (f.Tipo === 'Cuotas') {\n        asis.paso = 'total';\n        return preguntar(asis, '¿Cuántas cuotas son en total?');\n      }\n      break;\n    }\n    case 'total': {\n      const n = Number(r);\n      if (!/^\\d+$/.test(r) || n < 1) return preguntar(asis, 'Escribe el número total de cuotas, por ejemplo 12.');\n      f.CuotasTotal = n;\n      asis.paso = 'pagadas';\n      return preguntar(asis, '¿Cuántas cuotas llevas pagadas? Escribe 0 si ninguna.');\n    }\n    case 'pagadas': {\n      const n = Number(r);\n      if (!/^\\d+$/.test(r) || n >= f.CuotasTotal) return preguntar(asis, `Escribe un número entre 0 y ${f.CuotasTotal - 1}.`);\n      f.CuotasPagadas = n;\n      break;\n    }\n    case 'fecha': {\n      const d = leerFecha(r);\n      if (!d) return preguntar(asis, `No entendí la fecha. ${EJEMPLOS_FECHA}`);\n      if (d < hoyDia) return preguntar(asis, 'Esa fecha ya pasó. Escribe una fecha de hoy en adelante.');\n      f.FechaVence = d.toFormat('yyyy-MM-dd');\n      break;\n    }\n    default:\n      delete datos.asistente[chatId];\n      return responder('Algo salió mal. Escribe /nuevopago para empezar de nuevo.');\n  }\n  delete datos.asistente[chatId];\n  return nuevoPago(f);\n}\n\n// ===== Comandos =====\n// /start nunca muestra el menú: solo /ayuda, y solo con sesión iniciada\nif (/^\\/start\\b/i.test(texto)) return responder('Ya tienes la sesión iniciada. Escribe /ayuda para ver los comandos.');\nif (/^\\/(ayuda|help)\\b/i.test(texto)) return [{ json: { ...base, accion: 'ayuda' } }];\nif (/^\\/resumen\\b/i.test(texto)) return [{ json: { ...base, accion: 'resumen' } }];\nif (/^\\/pagos\\b/i.test(texto)) return [{ json: { ...base, accion: 'pagos' } }];\n\nconst p = texto.match(/^\\/pagado\\b\\s*(.*)$/i);\nif (p) {\n  if (!p[1].trim()) return responder('Escribe el nombre del pago. Por ejemplo: /pagado Internet\\nUsa /pagos para ver la lista.');\n  return [{ json: { ...base, accion: 'pagado', nombre: p[1].trim() } }];\n}\n\nconst el = texto.match(/^\\/eliminar\\b\\s*(.*)$/i);\nif (el) {\n  if (!el[1].trim()) return responder('Escribe el nombre del pago. Por ejemplo: /eliminar Internet\\nUsa /pagos para ver la lista.');\n  return [{ json: { ...base, accion: 'eliminar', nombre: el[1].trim() } }];\n}\n\n// Atajos en una línea: /recordar Pasaporte 15 dic | /nuevopago Internet 25 mensual 15\nconst rec = texto.match(/^\\/recordar\\s+(.+)$/i);\nif (rec) {\n  const partes = rec[1].trim().split(/\\s+/);\n  for (let i = 1; i < partes.length; i++) {\n    const f = leerFecha(partes.slice(i).join(' '));\n    if (!f) continue;\n    if (f < hoyDia) return responder('Esa fecha ya pasó.');\n    return nuevoPago({ Nombre: partes.slice(0, i).join(' '), Monto: 0, Tipo: 'Unico', FechaVence: f.toFormat('yyyy-MM-dd') });\n  }\n  return responder(`No entendí la fecha. Escribe solo /recordar y te pregunto paso a paso.`);\n}\n\nconst np = texto.match(/^\\/nuevopago\\s+(.+)$/i);\nif (np) {\n  const m = np[1].match(/^(.+?)\\s+(\\d+(?:[.,]\\d{1,2})?)\\s+(mensual|unico|único|cuotas?)(?:\\s+(.*))?$/i);\n  if (!m) return responder('No entendí el pago. Escribe solo /nuevopago y te pregunto paso a paso.');\n  const t = m[3].toLowerCase();\n  const fila = { Nombre: m[1].trim(), Monto: Number(m[2].replace(',', '.')),\n    Tipo: t.startsWith('cuota') ? 'Cuotas' : t === 'mensual' ? 'Mensual' : 'Unico' };\n  const resto = (m[4] || '').trim().split(/\\s+/).filter(Boolean);\n  if (fila.Tipo === 'Unico') {\n    const f = leerFecha(resto.join(' '));\n    if (!f) return responder(`Falta la fecha. ${EJEMPLOS_FECHA}`);\n    if (f < hoyDia) return responder('Esa fecha ya pasó.');\n    fila.FechaVence = f.toFormat('yyyy-MM-dd');\n  } else {\n    const dia = parseInt(resto[0], 10);\n    if (!(dia >= 1 && dia <= 31)) return responder('Falta el día del mes en que vence (1 a 31).');\n    fila.Dia = dia;\n    if (fila.Tipo === 'Cuotas') {\n      const total = parseInt(resto[1], 10);\n      if (!(total > 0)) return responder('Falta el número total de cuotas.');\n      fila.CuotasTotal = total;\n      fila.CuotasPagadas = Math.min(parseInt(resto[2], 10) || 0, total - 1);\n    }\n  }\n  return nuevoPago(fila);\n}\n\n// Gasto: \"/gasto 2.50 bebidas gaseosa\" o simplemente \"2.50 bebidas gaseosa\".\n// Si la primera palabra no es una categoría conocida (\"4 gomitas\"), pregunta la categoría.\n// Si solo viene el monto (\"4\"), pregunta el concepto y luego la categoría.\n// Lo mismo con /ingreso: \"/ingreso 500 quincena noviembre\".\nconst esIngreso = /^\\/ingreso\\b/i.test(texto);\nconst pedirCategoria = (monto, descripcion) =>\n  preguntarCategoria({ fila: { Monto: monto, Descripcion: descripcion }, ingreso: esIngreso },\n    `¿En qué categoría va \"${descripcion}\"?`);\nconst sinComando = texto.replace(/^\\/(gasto|ingreso)\\s+/i, '').trim();\nif (MONTO_RE.test(sinComando)) {\n  return preguntar({ paso: 'g_concepto', fila: { Monto: leerMonto(sinComando) }, ingreso: esIngreso },\n    PREGUNTA_CONCEPTO[esIngreso ? 'ingreso' : 'gasto']);\n}\nconst g = sinComando.match(/^(\\d+(?:[.,]\\d{1,2})?)\\s+(.+)$/);\nif (g) {\n  const resto = g[2].trim();\n  const bajo = resto.toLowerCase();\n  const conocidas = conocidasDe(esIngreso).sort((a, b) => b.length - a.length);\n  const cat = conocidas.find(c => bajo === c || bajo.startsWith(c + ' '));\n  if (cat) return nuevoMovimiento(esIngreso, leerMonto(g[1]), cat, resto.slice(cat.length).trim());\n  return pedirCategoria(leerMonto(g[1]), resto.slice(0, 100));\n}\nif (/^\\/gasto\\b/i.test(texto)) return responder('No entendí el gasto. Por ejemplo: /gasto 2.50 bebidas gaseosa\\nO escribe solo /gasto y te pregunto paso a paso.');\nif (esIngreso) return responder('No entendí el ingreso. Por ejemplo: /ingreso 500 quincena\\nO escribe solo /ingreso y te pregunto paso a paso.');\nreturn [{ json: { ...base, accion: 'ayuda' } }];"
      },
      "id": "961d6f5a-b598-49ae-9691-0e3feb67df0c",
      "name": "Interpretar mensaje",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "ad0e7029-ec46-4c15-8b58-07fba9cca6f8",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "gasto",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "gasto"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "2f5f3adf-025f-4b99-8d18-b7e5295308cb",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "resumen",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "resumen"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "372b844d-b45b-4cd8-bfb1-e9998c05c5c4",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "pagos",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "pagos"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "fea70dca-88da-4163-810a-b8098c9a2a33",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "pagado",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "pagado"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "b634fb61-bca9-4b68-aa12-4e09e867e9da",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "ayuda",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "ayuda"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "a312b730-a805-4e33-977c-345b18d5d07b",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "login",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "login"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "82f473a4-73de-4d64-a2ce-d746847c2844",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "mensaje",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "mensaje"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "426cf8c7-79b2-4169-a812-c16db7ef3b95",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "nuevopago",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "nuevopago"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "9a4cc081-489b-4c2b-96c2-d9d03dab59e0",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "eliminar",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "eliminar"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "a7553974-815a-4c2b-88e7-5cde288ebb9d",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "categoria",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "categoria"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "bdeecc07-9666-40dc-9755-50eb8a6f51cf",
                    "leftValue": "={{ $json.accion }}",
                    "rightValue": "ingreso",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "ingreso"
            }
          ]
        },
        "options": {}
      },
      "id": "0b5266bb-c2e6-4be8-807f-715add82c763",
      "name": "Según acción",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Gastos",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Fecha": "={{ $json.Fecha }}",
            "Monto": "={{ $json.Monto }}",
            "Categoria": "={{ $json.Categoria }}",
            "Descripcion": "={{ $json.Descripcion }}",
            "Mes": "={{ $json.Mes }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Fecha",
              "displayName": "Fecha",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Monto",
              "displayName": "Monto",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Categoria",
              "displayName": "Categoria",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Descripcion",
              "displayName": "Descripcion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Mes",
              "displayName": "Mes",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "91f3ab04-b067-4f25-83e0-576a617f5451",
      "name": "Guardar gasto",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        700,
        -320
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "chatId": "={{ $('Interpretar mensaje').item.json.chatId }}",
        "text": "=Gasto registrado: {{ Number($('Interpretar mensaje').item.json.Monto).toFixed(2) }} en {{ $('Interpretar mensaje').item.json.Categoria }}{{ $('Interpretar mensaje').item.json.Descripcion ? ' (' + $('Interpretar mensaje').item.json.Descripcion + ')' : '' }}",
        "additionalFields": {
          "appendAttribution": false
        },
        "replyMarkup": "replyKeyboardRemove",
        "replyKeyboardRemove": {
          "remove_keyboard": true
        }
      },
      "id": "b3049a4f-87cc-4f33-93b1-cb2514c5e904",
      "name": "Confirmar gasto",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        920,
        -320
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Ingresos",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Fecha": "={{ $json.Fecha }}",
            "Monto": "={{ $json.Monto }}",
            "Categoria": "={{ $json.Categoria }}",
            "Descripcion": "={{ $json.Descripcion }}",
            "Mes": "={{ $json.Mes }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Fecha",
              "displayName": "Fecha",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Monto",
              "displayName": "Monto",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Categoria",
              "displayName": "Categoria",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Descripcion",
              "displayName": "Descripcion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Mes",
              "displayName": "Mes",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "512dcaf9-3e82-4558-9b9b-429f6e221041",
      "name": "Guardar ingreso",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        700,
        -480
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "chatId": "={{ $('Interpretar mensaje').item.json.chatId }}",
        "text": "=Ingreso registrado: {{ Number($('Interpretar mensaje').item.json.Monto).toFixed(2) }} en {{ $('Interpretar mensaje').item.json.Categoria }}{{ $('Interpretar mensaje').item.json.Descripcion ? ' (' + $('Interpretar mensaje').item.json.Descripcion + ')' : '' }}",
        "additionalFields": {
          "appendAttribution": false
        },
        "replyMarkup": "replyKeyboardRemove",
        "replyKeyboardRemove": {
          "remove_keyboard": true
        }
      },
      "id": "d8f98cfe-f0e5-4a59-96a9-6dc0721f257e",
      "name": "Confirmar ingreso",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        920,
        -480
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Gastos",
          "mode": "name"
        },
        "options": {}
      },
      "id": "06a2b092-acbf-448a-935e-a2d7d5039773",
      "name": "Leer gastos",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        700,
        -160
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Ingresos",
          "mode": "name"
        },
        "options": {}
      },
      "id": "ddd3af5c-b8a0-42b6-812f-08843f4f2c28",
      "name": "Leer ingresos",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        920,
        -160
      ],
      "alwaysOutputData": true,
      "executeOnce": true
    },
    {
      "parameters": {
        "jsCode": "const num = v => {\n  if (typeof v === 'number') return v;\n  let s = String(v ?? '').replace(/[^\\d.,-]/g, '');\n  if (s.includes(',') && s.includes('.')) {\n    s = s.lastIndexOf(',') > s.lastIndexOf('.') ? s.replace(/\\./g, '').replace(',', '.') : s.replace(/,/g, '');\n  } else s = s.replace(',', '.');\n  return Number(s) || 0;\n};\nconst fmt = n => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\nconst hoy = DateTime.now().startOf('day');\nconst fecha = v => {\n  const s = String(v ?? '').trim();\n  if (!s) return null;\n  for (const f of ['yyyy-MM-dd', 'dd/MM/yyyy', 'd/M/yyyy', 'yyyy-MM']) {\n    const d = DateTime.fromFormat(s, f);\n    if (d.isValid) return d.startOf('day');\n  }\n  return null;\n};\nconst tipoDe = r => {\n  const t = String(r.Tipo || '').trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');\n  if (t.startsWith('unic')) return 'unico';\n  if (t.startsWith('cuota')) return 'cuotas';\n  return 'mensual';\n};\n// Próximo vencimiento pendiente de un pago (null si no aplica)\nconst vencimiento = r => {\n  if (!r.Nombre) return null;\n  if (String(r.Activo ?? 'SI').trim().toUpperCase() === 'NO') return null;\n  const tipo = tipoDe(r);\n  const up = fecha(r.UltimoPago);\n  const pagadoMes = up ? up.toFormat('yyyy-MM') : String(r.UltimoPago || '').trim();\n  if (tipo === 'unico') return pagadoMes ? null : fecha(r.FechaVence);\n  if (tipo === 'cuotas' && num(r.CuotasTotal) && num(r.CuotasPagadas) >= num(r.CuotasTotal)) return null;\n  const dia = Math.max(1, Math.min(31, num(r.Dia) || 1));\n  let mes = hoy.startOf('month');\n  if (pagadoMes && pagadoMes >= mes.toFormat('yyyy-MM')) mes = mes.plus({ months: 1 });\n  return mes.set({ day: Math.min(dia, mes.daysInMonth) }).startOf('day');\n};\nconst cuando = d => {\n  const n = Math.round(d.diff(hoy, 'days').days);\n  if (n === 0) return 'VENCE HOY';\n  if (n < 0) return `VENCIDO hace ${-n} día(s)`;\n  return `en ${n} día(s)`;\n};\nconst detalleCuotas = r => tipoDe(r) === 'cuotas' && num(r.CuotasTotal)\n  ? ` [cuota ${num(r.CuotasPagadas) + 1}/${num(r.CuotasTotal)}]` : '';\nconst montoTxt = r => num(r.Monto) ? `, ${fmt(num(r.Monto))}` : '';\n\nconst { chatId } = $('Interpretar mensaje').first().json;\nconst mes = hoy.toFormat('yyyy-MM');\nconst delMes = items => items.map(i => i.json)\n  .filter(r => r.Monto !== undefined && r.Monto !== '' && String(r.Mes || String(r.Fecha || '').slice(0, 7)) === mes);\nconst gastos = delMes($('Leer gastos').all());\nconst ingresos = delMes($input.all());  // viene de \"Leer ingresos\"\n\nconst agrupar = filas => {\n  const porCat = {};\n  let total = 0;\n  for (const r of filas) {\n    const m = num(r.Monto);\n    const c = String(r.Categoria || 'otros').toLowerCase();\n    porCat[c] = (porCat[c] || 0) + m;\n    total += m;\n  }\n  const lineas = Object.entries(porCat).sort((a, b) => b[1] - a[1])\n    .map(([c, m]) => `- ${c}: ${fmt(m)} (${Math.round(m * 100 / (total || 1))}%)`);\n  return { total, lineas };\n};\nconst g = agrupar(gastos);\nconst i = agrupar(ingresos);\nconst balance = i.total - g.total;\n\nconst titulo = hoy.setLocale('es').toFormat('LLLL yyyy');\nlet texto = `Resumen de ${titulo}\\n\\nIngresos: ${fmt(i.total)}\\nGastos: ${fmt(g.total)}\\nBalance: ${balance < 0 ? '-' : ''}${fmt(Math.abs(balance))}`;\nif (i.lineas.length) texto += `\\n\\nIngresos por categoría:\\n${i.lineas.join('\\n')}`;\nif (g.lineas.length) texto += `\\n\\nGastos por categoría:\\n${g.lineas.join('\\n')}`;\nif (!gastos.length && !ingresos.length) texto = `Aún no hay movimientos registrados en ${titulo}.`;\nreturn [{ json: { chatId, texto } }];"
      },
      "id": "c8a844d1-ddc0-49af-a1d4-3096f3d1722b",
      "name": "Armar resumen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1140,
        -160
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.texto }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "5e3c716b-8cc3-457b-8617-d91126bec112",
      "name": "Enviar resumen",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1360,
        -160
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Pagos",
          "mode": "name"
        },
        "options": {}
      },
      "id": "44df6961-be52-4727-933d-b3e13b9c5aab",
      "name": "Leer pagos",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        700,
        0
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const num = v => {\n  if (typeof v === 'number') return v;\n  let s = String(v ?? '').replace(/[^\\d.,-]/g, '');\n  if (s.includes(',') && s.includes('.')) {\n    s = s.lastIndexOf(',') > s.lastIndexOf('.') ? s.replace(/\\./g, '').replace(',', '.') : s.replace(/,/g, '');\n  } else s = s.replace(',', '.');\n  return Number(s) || 0;\n};\nconst fmt = n => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\nconst hoy = DateTime.now().startOf('day');\nconst fecha = v => {\n  const s = String(v ?? '').trim();\n  if (!s) return null;\n  for (const f of ['yyyy-MM-dd', 'dd/MM/yyyy', 'd/M/yyyy', 'yyyy-MM']) {\n    const d = DateTime.fromFormat(s, f);\n    if (d.isValid) return d.startOf('day');\n  }\n  return null;\n};\nconst tipoDe = r => {\n  const t = String(r.Tipo || '').trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');\n  if (t.startsWith('unic')) return 'unico';\n  if (t.startsWith('cuota')) return 'cuotas';\n  return 'mensual';\n};\n// Próximo vencimiento pendiente de un pago (null si no aplica)\nconst vencimiento = r => {\n  if (!r.Nombre) return null;\n  if (String(r.Activo ?? 'SI').trim().toUpperCase() === 'NO') return null;\n  const tipo = tipoDe(r);\n  const up = fecha(r.UltimoPago);\n  const pagadoMes = up ? up.toFormat('yyyy-MM') : String(r.UltimoPago || '').trim();\n  if (tipo === 'unico') return pagadoMes ? null : fecha(r.FechaVence);\n  if (tipo === 'cuotas' && num(r.CuotasTotal) && num(r.CuotasPagadas) >= num(r.CuotasTotal)) return null;\n  const dia = Math.max(1, Math.min(31, num(r.Dia) || 1));\n  let mes = hoy.startOf('month');\n  if (pagadoMes && pagadoMes >= mes.toFormat('yyyy-MM')) mes = mes.plus({ months: 1 });\n  return mes.set({ day: Math.min(dia, mes.daysInMonth) }).startOf('day');\n};\nconst cuando = d => {\n  const n = Math.round(d.diff(hoy, 'days').days);\n  if (n === 0) return 'VENCE HOY';\n  if (n < 0) return `VENCIDO hace ${-n} día(s)`;\n  return `en ${n} día(s)`;\n};\nconst detalleCuotas = r => tipoDe(r) === 'cuotas' && num(r.CuotasTotal)\n  ? ` [cuota ${num(r.CuotasPagadas) + 1}/${num(r.CuotasTotal)}]` : '';\nconst montoTxt = r => num(r.Monto) ? `, ${fmt(num(r.Monto))}` : '';\n\nconst { chatId } = $('Interpretar mensaje').first().json;\nconst pendientes = $input.all().map(i => i.json)\n  .map(r => ({ r, v: vencimiento(r) }))\n  .filter(x => x.v)\n  .sort((a, b) => a.v - b.v);\n\nconst texto = pendientes.length\n  ? 'Próximos pagos\\n\\n' + pendientes.map(({ r, v }) =>\n      `- ${r.Nombre}${montoTxt(r)}, vence ${v.toFormat('dd/MM/yyyy')} (${cuando(v)})${detalleCuotas(r)}`).join('\\n')\n    + '\\n\\nMarca uno con: /pagado <nombre>'\n  : 'No tienes pagos pendientes.';\nreturn [{ json: { chatId, texto } }];"
      },
      "id": "5ee50e2c-06ae-4ed1-b882-97387f870d44",
      "name": "Listar pagos",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        920,
        0
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.texto }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "9b728ef1-1145-452b-ae12-e8b22d30335e",
      "name": "Enviar lista",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1140,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Pagos",
          "mode": "name"
        },
        "options": {}
      },
      "id": "c29fed82-6f75-46e0-b54d-b3fe77d06122",
      "name": "Leer pagos (pagado)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        700,
        160
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const num = v => {\n  if (typeof v === 'number') return v;\n  let s = String(v ?? '').replace(/[^\\d.,-]/g, '');\n  if (s.includes(',') && s.includes('.')) {\n    s = s.lastIndexOf(',') > s.lastIndexOf('.') ? s.replace(/\\./g, '').replace(',', '.') : s.replace(/,/g, '');\n  } else s = s.replace(',', '.');\n  return Number(s) || 0;\n};\nconst fmt = n => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\nconst hoy = DateTime.now().startOf('day');\nconst fecha = v => {\n  const s = String(v ?? '').trim();\n  if (!s) return null;\n  for (const f of ['yyyy-MM-dd', 'dd/MM/yyyy', 'd/M/yyyy', 'yyyy-MM']) {\n    const d = DateTime.fromFormat(s, f);\n    if (d.isValid) return d.startOf('day');\n  }\n  return null;\n};\nconst tipoDe = r => {\n  const t = String(r.Tipo || '').trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');\n  if (t.startsWith('unic')) return 'unico';\n  if (t.startsWith('cuota')) return 'cuotas';\n  return 'mensual';\n};\n// Próximo vencimiento pendiente de un pago (null si no aplica)\nconst vencimiento = r => {\n  if (!r.Nombre) return null;\n  if (String(r.Activo ?? 'SI').trim().toUpperCase() === 'NO') return null;\n  const tipo = tipoDe(r);\n  const up = fecha(r.UltimoPago);\n  const pagadoMes = up ? up.toFormat('yyyy-MM') : String(r.UltimoPago || '').trim();\n  if (tipo === 'unico') return pagadoMes ? null : fecha(r.FechaVence);\n  if (tipo === 'cuotas' && num(r.CuotasTotal) && num(r.CuotasPagadas) >= num(r.CuotasTotal)) return null;\n  const dia = Math.max(1, Math.min(31, num(r.Dia) || 1));\n  let mes = hoy.startOf('month');\n  if (pagadoMes && pagadoMes >= mes.toFormat('yyyy-MM')) mes = mes.plus({ months: 1 });\n  return mes.set({ day: Math.min(dia, mes.daysInMonth) }).startOf('day');\n};\nconst cuando = d => {\n  const n = Math.round(d.diff(hoy, 'days').days);\n  if (n === 0) return 'VENCE HOY';\n  if (n < 0) return `VENCIDO hace ${-n} día(s)`;\n  return `en ${n} día(s)`;\n};\nconst detalleCuotas = r => tipoDe(r) === 'cuotas' && num(r.CuotasTotal)\n  ? ` [cuota ${num(r.CuotasPagadas) + 1}/${num(r.CuotasTotal)}]` : '';\nconst montoTxt = r => num(r.Monto) ? `, ${fmt(num(r.Monto))}` : '';\n\nconst { chatId, nombre } = $('Interpretar mensaje').first().json;\nconst filas = $input.all().map(i => i.json)\n  .filter(r => r.Nombre && String(r.Activo ?? 'SI').trim().toUpperCase() !== 'NO');\nconst q = nombre.toLowerCase();\nconst r = filas.find(f => String(f.Nombre).toLowerCase() === q)\n  || filas.find(f => String(f.Nombre).toLowerCase().includes(q));\n\nif (!r) return [{ json: { ok: false, chatId, texto: `No encontré el pago \"${nombre}\". Usa /pagos para ver la lista.` } }];\nconst v = vencimiento(r);\nif (!v) return [{ json: { ok: false, chatId, texto: `\"${r.Nombre}\" no tiene pagos pendientes.` } }];\n\nconst tipo = tipoDe(r);\nlet cuotas = r.CuotasPagadas ?? '';\nlet activo = r.Activo || 'SI';\nlet ultimo = v.toFormat('yyyy-MM');\nif (tipo === 'unico') { ultimo = hoy.toFormat('yyyy-MM-dd'); activo = 'NO'; }\nif (tipo === 'cuotas') {\n  cuotas = num(r.CuotasPagadas) + 1;\n  if (num(r.CuotasTotal) && cuotas >= num(r.CuotasTotal)) activo = 'NO';\n}\n\nlet texto = `Pago registrado: ${r.Nombre} — ${fmt(num(r.Monto))}`;\nif (tipo === 'cuotas') texto += `\\nCuota ${cuotas}/${num(r.CuotasTotal) || '?'}`;\nif (activo === 'NO') texto += '\\nPago completado, no te lo recordaré más.';\n\nreturn [{ json: {\n  ok: true, chatId, texto,\n  Nombre: r.Nombre, UltimoPago: ultimo, CuotasPagadas: cuotas, Activo: activo,\n  gasto: {\n    Fecha: hoy.toFormat('yyyy-MM-dd'), Monto: num(r.Monto), Categoria: 'pagos',\n    Descripcion: r.Nombre, Mes: hoy.toFormat('yyyy-MM'),\n  },\n} }];"
      },
      "id": "ad06de0d-9328-4d36-9ef7-30ad27dc118d",
      "name": "Marcar pago",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        920,
        160
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "1b99cd78-a93c-42cb-9218-b76ef8491926",
              "leftValue": "={{ $json.ok }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2b1fac7a-9c12-4d96-95b2-84ae4952f061",
      "name": "¿Encontrado?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1140,
        160
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Pagos",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Nombre": "={{ $('Marcar pago').item.json.Nombre }}",
            "UltimoPago": "={{ $('Marcar pago').item.json.UltimoPago }}",
            "CuotasPagadas": "={{ $('Marcar pago').item.json.CuotasPagadas }}",
            "Activo": "={{ $('Marcar pago').item.json.Activo }}"
          },
          "matchingColumns": [
            "Nombre"
          ],
          "schema": [
            {
              "id": "Nombre",
              "displayName": "Nombre",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "UltimoPago",
              "displayName": "UltimoPago",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CuotasPagadas",
              "displayName": "CuotasPagadas",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Activo",
              "displayName": "Activo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "c5ba7547-1a96-4de0-b6b6-5fdf239d662f",
      "name": "Actualizar pago",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1360,
        100
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Gastos",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Fecha": "={{ $('Marcar pago').item.json.gasto.Fecha }}",
            "Monto": "={{ $('Marcar pago').item.json.gasto.Monto }}",
            "Categoria": "={{ $('Marcar pago').item.json.gasto.Categoria }}",
            "Descripcion": "={{ $('Marcar pago').item.json.gasto.Descripcion }}",
            "Mes": "={{ $('Marcar pago').item.json.gasto.Mes }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Fecha",
              "displayName": "Fecha",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Monto",
              "displayName": "Monto",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Categoria",
              "displayName": "Categoria",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Descripcion",
              "displayName": "Descripcion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Mes",
              "displayName": "Mes",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "404db17b-c056-46c2-885d-066643f6c92d",
      "name": "Registrar como gasto",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1580,
        100
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "chatId": "={{ $('Marcar pago').item.json.chatId }}",
        "text": "={{ $('Marcar pago').item.json.texto }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "53aaf3b8-7f1e-4867-bb75-ff0b02d21511",
      "name": "Confirmar pago",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1800,
        100
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.texto }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "9084b79a-cf00-4a28-bfde-0337d3c5f89d",
      "name": "Pago no encontrado",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1360,
        260
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "Control de gastos\n\nGastos:\n/gasto - te pregunto monto, concepto y categoría\n/gasto 2.50 bebidas gaseosa - todo en una línea\n(también puedes escribir solo: 2.50 bebidas gaseosa)\n\nIngresos:\n/ingreso - te pregunto monto, concepto y categoría\n/ingreso 500 quincena noviembre - todo en una línea\n\nPagos y recordatorios (te pregunto paso a paso):\n/nuevopago - agrega un pago\n/recordar - agrega un recordatorio\n/cancelar - sale del paso a paso\n\nConsultar y actualizar:\n/pagos - próximos pagos\n/pagado Internet - marca un pago como hecho\n/eliminar Internet - deja de recordar un pago\n/resumen - ingresos, gastos y balance del mes\n/salir - cierra la sesión",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "6ee025f6-7537-49a1-9180-7357d1f5e320",
      "name": "Ayuda",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        700,
        330
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Pagos",
          "mode": "name"
        },
        "options": {}
      },
      "id": "a904f4d5-ad20-4828-9bce-cdf9f18f9b78",
      "name": "Leer pagos (nuevo)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        700,
        800
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const num = v => {\n  if (typeof v === 'number') return v;\n  let s = String(v ?? '').replace(/[^\\d.,-]/g, '');\n  if (s.includes(',') && s.includes('.')) {\n    s = s.lastIndexOf(',') > s.lastIndexOf('.') ? s.replace(/\\./g, '').replace(',', '.') : s.replace(/,/g, '');\n  } else s = s.replace(',', '.');\n  return Number(s) || 0;\n};\nconst fmt = n => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\nconst hoy = DateTime.now().startOf('day');\nconst fecha = v => {\n  const s = String(v ?? '').trim();\n  if (!s) return null;\n  for (const f of ['yyyy-MM-dd', 'dd/MM/yyyy', 'd/M/yyyy', 'yyyy-MM']) {\n    const d = DateTime.fromFormat(s, f);\n    if (d.isValid) return d.startOf('day');\n  }\n  return null;\n};\nconst tipoDe = r => {\n  const t = String(r.Tipo || '').trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');\n  if (t.startsWith('unic')) return 'unico';\n  if (t.startsWith('cuota')) return 'cuotas';\n  return 'mensual';\n};\n// Próximo vencimiento pendiente de un pago (null si no aplica)\nconst vencimiento = r => {\n  if (!r.Nombre) return null;\n  if (String(r.Activo ?? 'SI').trim().toUpperCase() === 'NO') return null;\n  const tipo = tipoDe(r);\n  const up = fecha(r.UltimoPago);\n  const pagadoMes = up ? up.toFormat('yyyy-MM') : String(r.UltimoPago || '').trim();\n  if (tipo === 'unico') return pagadoMes ? null : fecha(r.FechaVence);\n  if (tipo === 'cuotas' && num(r.CuotasTotal) && num(r.CuotasPagadas) >= num(r.CuotasTotal)) return null;\n  const dia = Math.max(1, Math.min(31, num(r.Dia) || 1));\n  let mes = hoy.startOf('month');\n  if (pagadoMes && pagadoMes >= mes.toFormat('yyyy-MM')) mes = mes.plus({ months: 1 });\n  return mes.set({ day: Math.min(dia, mes.daysInMonth) }).startOf('day');\n};\nconst cuando = d => {\n  const n = Math.round(d.diff(hoy, 'days').days);\n  if (n === 0) return 'VENCE HOY';\n  if (n < 0) return `VENCIDO hace ${-n} día(s)`;\n  return `en ${n} día(s)`;\n};\nconst detalleCuotas = r => tipoDe(r) === 'cuotas' && num(r.CuotasTotal)\n  ? ` [cuota ${num(r.CuotasPagadas) + 1}/${num(r.CuotasTotal)}]` : '';\nconst montoTxt = r => num(r.Monto) ? `, ${fmt(num(r.Monto))}` : '';\n\nconst n = $('Interpretar mensaje').first().json;\nconst existe = $input.all().map(i => i.json).some(r =>\n  String(r.Nombre || '').trim().toLowerCase() === n.Nombre.toLowerCase()\n  && String(r.Activo ?? 'SI').trim().toUpperCase() !== 'NO');\nif (existe) return [{ json: { ok: false, chatId: n.chatId,\n  texto: `Ya existe un pago activo llamado \"${n.Nombre}\". Usa otro nombre o desactívalo en la hoja.` } }];\n\nconst fila = { Nombre: n.Nombre, Monto: n.Monto, Tipo: n.Tipo, Dia: n.Dia, FechaVence: n.FechaVence,\n  CuotasTotal: n.CuotasTotal, CuotasPagadas: n.CuotasPagadas, UltimoPago: '', Activo: 'SI' };\nconst v = vencimiento(fila);\nconst desc = fila.Tipo === 'Mensual' ? `cada mes, el día ${fila.Dia}`\n  : fila.Tipo === 'Cuotas' ? `${fila.CuotasTotal} cuotas, el día ${fila.Dia} de cada mes (llevas ${num(fila.CuotasPagadas)} pagadas)`\n  : 'una sola vez';\nlet texto = `Listo. ${fila.Nombre}${montoTxt(fila)}, ${desc}.`;\nif (v) texto += `\\nPróximo vencimiento: ${v.toFormat('dd/MM/yyyy')} (${cuando(v)}).`;\ntexto += '\\nTe aviso 3 días antes y el día que vence.';\nreturn [{ json: { ok: true, chatId: n.chatId, texto, ...fila } }];"
      },
      "id": "32264f13-a4e2-4e6b-a6f9-23537dbe5ac0",
      "name": "Validar pago nuevo",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        920,
        800
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "a0dd7b2c-57d9-4672-b10e-a10cefd6d1ee",
              "leftValue": "={{ $json.ok }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "00961306-996f-41e5-9d23-ef9c8bb076ca",
      "name": "¿Pago válido?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1140,
        800
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Pagos",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Nombre": "={{ $('Validar pago nuevo').item.json.Nombre }}",
            "Monto": "={{ $('Validar pago nuevo').item.json.Monto }}",
            "Tipo": "={{ $('Validar pago nuevo').item.json.Tipo }}",
            "Dia": "={{ $('Validar pago nuevo').item.json.Dia }}",
            "FechaVence": "={{ $('Validar pago nuevo').item.json.FechaVence }}",
            "CuotasTotal": "={{ $('Validar pago nuevo').item.json.CuotasTotal }}",
            "CuotasPagadas": "={{ $('Validar pago nuevo').item.json.CuotasPagadas }}",
            "UltimoPago": "={{ $('Validar pago nuevo').item.json.UltimoPago }}",
            "Activo": "={{ $('Validar pago nuevo').item.json.Activo }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Nombre",
              "displayName": "Nombre",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Monto",
              "displayName": "Monto",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Tipo",
              "displayName": "Tipo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Dia",
              "displayName": "Dia",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "FechaVence",
              "displayName": "FechaVence",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CuotasTotal",
              "displayName": "CuotasTotal",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CuotasPagadas",
              "displayName": "CuotasPagadas",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "UltimoPago",
              "displayName": "UltimoPago",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Activo",
              "displayName": "Activo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "29991a8b-fa97-4473-9352-cb49d1d3359a",
      "name": "Guardar pago",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1360,
        740
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "chatId": "={{ $('Validar pago nuevo').item.json.chatId }}",
        "text": "={{ $('Validar pago nuevo').item.json.texto }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "f2e205e0-0949-4340-ade3-1c46d0607bf2",
      "name": "Confirmar pago nuevo",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1580,
        740
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.texto }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "eeb672d0-e896-4d76-9068-9975542cdace",
      "name": "Pago repetido",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1360,
        880
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.respuesta }}",
        "replyMarkup": "replyKeyboard",
        "replyKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "={{ $json.opciones[0] }}",
                    "additionalFields": {}
                  },
                  {
                    "text": "={{ $json.opciones[1] }}",
                    "additionalFields": {}
                  },
                  {
                    "text": "={{ $json.opciones[2] }}",
                    "additionalFields": {}
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "={{ $json.opciones[3] }}",
                    "additionalFields": {}
                  },
                  {
                    "text": "={{ $json.opciones[4] }}",
                    "additionalFields": {}
                  },
                  {
                    "text": "={{ $json.opciones[5] }}",
                    "additionalFields": {}
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "={{ $json.opciones[6] }}",
                    "additionalFields": {}
                  },
                  {
                    "text": "={{ $json.opciones[7] }}",
                    "additionalFields": {}
                  },
                  {
                    "text": "={{ $json.opciones[8] }}",
                    "additionalFields": {}
                  }
                ]
              }
            }
          ]
        },
        "replyKeyboardOptions": {
          "one_time_keyboard": true,
          "resize_keyboard": true
        },
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "193ea1b5-924f-4060-99f9-c3f023bb67f8",
      "name": "Preguntar categoría",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        700,
        1140
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Pagos",
          "mode": "name"
        },
        "options": {}
      },
      "id": "f3ee9e32-65f5-4a10-8628-b7d00fbc7a97",
      "name": "Leer pagos (eliminar)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        700,
        980
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const { chatId, nombre } = $('Interpretar mensaje').first().json;\nconst activos = $input.all().map(i => i.json)\n  .filter(r => r.Nombre && String(r.Activo ?? 'SI').trim().toUpperCase() !== 'NO');\nconst q = nombre.toLowerCase();\nconst exactos = activos.filter(r => String(r.Nombre).toLowerCase() === q);\nconst encontrados = exactos.length ? exactos : activos.filter(r => String(r.Nombre).toLowerCase().includes(q));\n\nif (!encontrados.length) return [{ json: { ok: false, chatId,\n  texto: `No encontré un pago activo llamado \"${nombre}\". Usa /pagos para ver la lista.` } }];\nif (encontrados.length > 1) return [{ json: { ok: false, chatId,\n  texto: `Hay varios pagos que coinciden:\\n${encontrados.map(r => '- ' + r.Nombre).join('\\n')}\\nEscribe el nombre completo.` } }];\n\nconst r = encontrados[0];\nreturn [{ json: { ok: true, chatId, Nombre: r.Nombre, Activo: 'NO',\n  texto: `Eliminado: ${r.Nombre}. Ya no te lo voy a recordar.\\nSi fue un error, cambia Activo a SI en la hoja.` } }];"
      },
      "id": "75f66ff8-c370-4e91-82b4-a089cbfc25dd",
      "name": "Buscar pago a eliminar",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        920,
        980
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "7066c5ed-0dfa-433c-9020-5fd842668ebf",
              "leftValue": "={{ $json.ok }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "34aa439a-fd6a-4699-b92d-d51e3bc89ed5",
      "name": "¿Se puede eliminar?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1140,
        980
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Pagos",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Nombre": "={{ $('Buscar pago a eliminar').item.json.Nombre }}",
            "Activo": "={{ $('Buscar pago a eliminar').item.json.Activo }}"
          },
          "matchingColumns": [
            "Nombre"
          ],
          "schema": [
            {
              "id": "Nombre",
              "displayName": "Nombre",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Activo",
              "displayName": "Activo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "id": "d5604284-6be4-4227-83d6-46bdd538c03c",
      "name": "Desactivar pago",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1360,
        920
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "chatId": "={{ $('Buscar pago a eliminar').item.json.chatId }}",
        "text": "={{ $('Buscar pago a eliminar').item.json.texto }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "af973797-cd6a-48a4-8b0d-5ad46f2fea35",
      "name": "Confirmar eliminado",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1580,
        920
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.texto }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "2eed9a49-0dda-4af6-a0df-f418979d0328",
      "name": "No se pudo eliminar",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1360,
        1060
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "deleteMessage",
        "chatId": "={{ $json.chatId }}",
        "messageId": "={{ $json.messageId }}"
      },
      "id": "5fd279ec-8588-4c96-b15a-aaf63d79efdf",
      "name": "Borrar mensaje con PIN",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        700,
        480
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "chatId": "={{ $('Interpretar mensaje').item.json.chatId }}",
        "text": "={{ $('Interpretar mensaje').item.json.respuesta }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "64a9add8-aee2-4385-a561-9810ac8c7b46",
      "name": "Responder login",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        920,
        480
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.respuesta }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "1ae00aa6-1dfa-4e3b-a1f1-a4aeba3c931e",
      "name": "Responder",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        700,
        630
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * *"
            }
          ]
        }
      },
      "id": "e2c545a2-4906-4603-b290-bb47108e1a8b",
      "name": "Cada día 8:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        620
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "TU_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Pagos",
          "mode": "name"
        },
        "options": {}
      },
      "id": "8a44b3f3-f278-4b85-a6a5-19b6e2ed66c5",
      "name": "Leer pagos (recordatorio)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        620
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const num = v => {\n  if (typeof v === 'number') return v;\n  let s = String(v ?? '').replace(/[^\\d.,-]/g, '');\n  if (s.includes(',') && s.includes('.')) {\n    s = s.lastIndexOf(',') > s.lastIndexOf('.') ? s.replace(/\\./g, '').replace(',', '.') : s.replace(/,/g, '');\n  } else s = s.replace(',', '.');\n  return Number(s) || 0;\n};\nconst fmt = n => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\nconst hoy = DateTime.now().startOf('day');\nconst fecha = v => {\n  const s = String(v ?? '').trim();\n  if (!s) return null;\n  for (const f of ['yyyy-MM-dd', 'dd/MM/yyyy', 'd/M/yyyy', 'yyyy-MM']) {\n    const d = DateTime.fromFormat(s, f);\n    if (d.isValid) return d.startOf('day');\n  }\n  return null;\n};\nconst tipoDe = r => {\n  const t = String(r.Tipo || '').trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');\n  if (t.startsWith('unic')) return 'unico';\n  if (t.startsWith('cuota')) return 'cuotas';\n  return 'mensual';\n};\n// Próximo vencimiento pendiente de un pago (null si no aplica)\nconst vencimiento = r => {\n  if (!r.Nombre) return null;\n  if (String(r.Activo ?? 'SI').trim().toUpperCase() === 'NO') return null;\n  const tipo = tipoDe(r);\n  const up = fecha(r.UltimoPago);\n  const pagadoMes = up ? up.toFormat('yyyy-MM') : String(r.UltimoPago || '').trim();\n  if (tipo === 'unico') return pagadoMes ? null : fecha(r.FechaVence);\n  if (tipo === 'cuotas' && num(r.CuotasTotal) && num(r.CuotasPagadas) >= num(r.CuotasTotal)) return null;\n  const dia = Math.max(1, Math.min(31, num(r.Dia) || 1));\n  let mes = hoy.startOf('month');\n  if (pagadoMes && pagadoMes >= mes.toFormat('yyyy-MM')) mes = mes.plus({ months: 1 });\n  return mes.set({ day: Math.min(dia, mes.daysInMonth) }).startOf('day');\n};\nconst cuando = d => {\n  const n = Math.round(d.diff(hoy, 'days').days);\n  if (n === 0) return 'VENCE HOY';\n  if (n < 0) return `VENCIDO hace ${-n} día(s)`;\n  return `en ${n} día(s)`;\n};\nconst detalleCuotas = r => tipoDe(r) === 'cuotas' && num(r.CuotasTotal)\n  ? ` [cuota ${num(r.CuotasPagadas) + 1}/${num(r.CuotasTotal)}]` : '';\nconst montoTxt = r => num(r.Monto) ? `, ${fmt(num(r.Monto))}` : '';\n\n// Avisa 3 días antes, el día del vencimiento y cada día mientras esté vencido.\nconst DIAS_ANTES = 3;\nconst avisos = $input.all().map(i => i.json)\n  .map(r => ({ r, v: vencimiento(r) }))\n  .filter(x => x.v)\n  .map(x => ({ ...x, n: Math.round(x.v.diff(hoy, 'days').days) }))\n  .filter(x => x.n === DIAS_ANTES || x.n <= 0)\n  .sort((a, b) => a.n - b.n);\n\nif (!avisos.length) return [];\n\nconst lineas = avisos.map(({ r, v }) =>\n  `- ${r.Nombre}${montoTxt(r)}, vence ${v.toFormat('dd/MM/yyyy')} (${cuando(v)})${detalleCuotas(r)}`);\nconst total = avisos.reduce((s, x) => s + num(x.r.Monto), 0);\nconst texto = `Recordatorio de pagos\\n\\n${lineas.join('\\n')}\\n\\nTotal: ${fmt(total)}\\nCuando pagues: /pagado <nombre>`;\nreturn [{ json: { texto, fecha: hoy.toFormat('dd/MM/yyyy'), cantidad: avisos.length } }];"
      },
      "id": "43fa6421-92f3-4bf8-bee3-e63c790d798a",
      "name": "Pagos por vencer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        620
      ]
    },
    {
      "parameters": {
        "chatId": "TU_CHAT_ID",
        "text": "={{ $json.texto }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "715647f3-2298-42bf-966f-dc0b6c726200",
      "name": "Aviso Telegram",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        700,
        540
      ]
    },
    {
      "parameters": {
        "sendTo": "tu@correo.com",
        "subject": "={{ $json.cantidad }} pago(s) por vencer – {{ $json.fecha }}",
        "emailType": "text",
        "message": "={{ $json.texto }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "b10f22f3-644f-4aef-87b5-d0e0c7ec55f5",
      "name": "Aviso Gmail",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        700,
        700
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $('Interpretar mensaje').first().json.chatId }}",
        "text": "=No pude guardar en la hoja de Google.\nDetalle: {{ $json.error?.message || $json.error || $json.message || 'sin detalle' }}\n\nRevisa en n8n que los nodos de Google Sheets tengan la credencial asignada.",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "1ce61896-075d-4576-92f9-44dc47c81f43",
      "name": "Aviso de error",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1800,
        -640
      ]
    }
  ],
  "connections": {
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Interpretar mensaje",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Interpretar mensaje": {
      "main": [
        [
          {
            "node": "Según acción",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Según acción": {
      "main": [
        [
          {
            "node": "Guardar gasto",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Leer gastos",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Leer pagos",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Leer pagos (pagado)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ayuda",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Borrar mensaje con PIN",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Responder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Leer pagos (nuevo)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Leer pagos (eliminar)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Preguntar categoría",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Guardar ingreso",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Guardar ingreso": {
      "main": [
        [
          {
            "node": "Confirmar ingreso",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aviso de error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leer pagos (eliminar)": {
      "main": [
        [
          {
            "node": "Buscar pago a eliminar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar pago a eliminar": {
      "main": [
        [
          {
            "node": "¿Se puede eliminar?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "¿Se puede eliminar?": {
      "main": [
        [
          {
            "node": "Desactivar pago",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No se pudo eliminar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Desactivar pago": {
      "main": [
        [
          {
            "node": "Confirmar eliminado",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aviso de error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leer pagos (nuevo)": {
      "main": [
        [
          {
            "node": "Validar pago nuevo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validar pago nuevo": {
      "main": [
        [
          {
            "node": "¿Pago válido?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "¿Pago válido?": {
      "main": [
        [
          {
            "node": "Guardar pago",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Pago repetido",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Guardar pago": {
      "main": [
        [
          {
            "node": "Confirmar pago nuevo",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aviso de error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Borrar mensaje con PIN": {
      "main": [
        [
          {
            "node": "Responder login",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Guardar gasto": {
      "main": [
        [
          {
            "node": "Confirmar gasto",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aviso de error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leer gastos": {
      "main": [
        [
          {
            "node": "Leer ingresos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leer ingresos": {
      "main": [
        [
          {
            "node": "Armar resumen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Armar resumen": {
      "main": [
        [
          {
            "node": "Enviar resumen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leer pagos": {
      "main": [
        [
          {
            "node": "Listar pagos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Listar pagos": {
      "main": [
        [
          {
            "node": "Enviar lista",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leer pagos (pagado)": {
      "main": [
        [
          {
            "node": "Marcar pago",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Marcar pago": {
      "main": [
        [
          {
            "node": "¿Encontrado?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "¿Encontrado?": {
      "main": [
        [
          {
            "node": "Actualizar pago",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Pago no encontrado",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Actualizar pago": {
      "main": [
        [
          {
            "node": "Registrar como gasto",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aviso de error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar como gasto": {
      "main": [
        [
          {
            "node": "Confirmar pago",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aviso de error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cada día 8:00": {
      "main": [
        [
          {
            "node": "Leer pagos (recordatorio)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leer pagos (recordatorio)": {
      "main": [
        [
          {
            "node": "Pagos por vencer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pagos por vencer": {
      "main": [
        [
          {
            "node": "Aviso Telegram",
            "type": "main",
            "index": 0
          },
          {
            "node": "Aviso Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1",
    "timezone": "America/New_York"
  },
  "active": false
}