{
 "name": "Recordatorios de turnos · Google Calendar → WhatsApp",
 "nodes": [
  {
   "id": "698dc71e-08b4-47db-a639-bd40cd73d39d",
   "name": "Nota 4337",
   "type": "n8n-nodes-base.stickyNote",
   "typeVersion": 1,
   "position": [
    -80,
    -380
   ],
   "parameters": {
    "content": "## Recordatorios de turnos por WhatsApp\nCada 30 minutos lee los turnos de las próximas horas en Google Calendar y le manda a cada paciente su recordatorio (plantilla aprobada de WhatsApp con los botones **Confirmar** y **Cancelar**). El teléfono va en el título, la descripción o el lugar del evento.\nCuando el paciente responde, marca el turno **[Confirmado]** o **[Cancelado]** en el calendario, le contesta y, si cancela, te avisa por Telegram.\n\n**Credenciales:** Google Calendar (OAuth2), una «Header Auth» para WhatsApp (`Authorization: Bearer <token>` de Meta, o `X-API-Key` de Kapso con `waApiBase` = `https://api.kapso.ai/meta/whatsapp/v24.0`) y Telegram. **WhatsApp:** el webhook de mensajes apunta a la URL del nodo «Respuesta de WhatsApp».",
    "width": 900,
    "height": 300,
    "color": 6
   }
  },
  {
   "id": "a7292ab1-5872-4430-b30b-e44bb740e4ae",
   "name": "Cada 30 minutos",
   "type": "n8n-nodes-base.scheduleTrigger",
   "typeVersion": 1.2,
   "position": [
    0,
    0
   ],
   "parameters": {
    "rule": {
     "interval": [
      {
       "field": "minutes",
       "minutesInterval": 30
      }
     ]
    }
   }
  },
  {
   "id": "ab10cadd-bca3-44ea-a5b8-9f030275b886",
   "name": "Probar ahora",
   "type": "n8n-nodes-base.manualTrigger",
   "typeVersion": 1,
   "position": [
    0,
    160
   ],
   "parameters": {}
  },
  {
   "id": "1a24b49e-4789-47b9-a2b0-e8850d54dbf2",
   "name": "Configuración",
   "type": "n8n-nodes-base.set",
   "typeVersion": 3.4,
   "position": [
    240,
    80
   ],
   "parameters": {
    "assignments": {
     "assignments": [
      {
       "id": "e7477df9-8547-4642-9033-b50102111a03",
       "name": "calendarioId",
       "value": "tuconsultorio@gmail.com",
       "type": "string"
      },
      {
       "id": "ced115d4-c32b-4eaf-9501-db0739cb0d30",
       "name": "horasAntes",
       "value": 24,
       "type": "number"
      },
      {
       "id": "3fd54e6e-75b8-4603-850f-3d5e593926b2",
       "name": "zonaHoraria",
       "value": -3,
       "type": "number"
      },
      {
       "id": "689df9ff-0cd0-4e56-ac61-5f26c25905e6",
       "name": "codigoPais",
       "value": "54",
       "type": "string"
      },
      {
       "id": "0d79cb86-8fbd-4142-84c4-87fd7502871a",
       "name": "negocio",
       "value": "Consultorio Pérez",
       "type": "string"
      },
      {
       "id": "5dafab72-b378-49a3-8126-fc178e38e313",
       "name": "waApiBase",
       "value": "https://graph.facebook.com/v23.0",
       "type": "string"
      },
      {
       "id": "ae69108c-e1bc-40f3-8823-ce132837c54e",
       "name": "phoneNumberId",
       "value": "TU_PHONE_NUMBER_ID",
       "type": "string"
      },
      {
       "id": "b1b1934a-906d-46da-bd27-6f0068fc7161",
       "name": "plantilla",
       "value": "recordatorio_turno",
       "type": "string"
      },
      {
       "id": "bf88891b-8153-416c-955a-e460468db4b0",
       "name": "idioma",
       "value": "es",
       "type": "string"
      },
      {
       "id": "86676484-1ee8-4f1b-a844-f020044a112f",
       "name": "linkReprogramar",
       "value": "https://calendar.app.google/…",
       "type": "string"
      },
      {
       "id": "20d8d43c-066f-430a-b575-257ae8071bad",
       "name": "telegramChatId",
       "value": "TU_CHAT_ID",
       "type": "string"
      }
     ]
    },
    "includeOtherFields": true,
    "include": "all",
    "options": {}
   }
  },
  {
   "id": "738a1b7a-0daf-422d-8dfd-38dd770bdbfb",
   "name": "Leer turnos de Google Calendar",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    480,
    80
   ],
   "parameters": {
    "method": "GET",
    "url": "=https://www.googleapis.com/calendar/v3/calendars/{{ encodeURIComponent($json.calendarioId) }}/events",
    "authentication": "predefinedCredentialType",
    "nodeCredentialType": "googleCalendarOAuth2Api",
    "options": {},
    "sendQuery": true,
    "queryParameters": {
     "parameters": [
      {
       "name": "timeMin",
       "value": "={{ new Date(Date.now() + 3600e3).toISOString() }}"
      },
      {
       "name": "timeMax",
       "value": "={{ new Date(Date.now() + $json.horasAntes * 3600e3).toISOString() }}"
      },
      {
       "name": "singleEvents",
       "value": "true"
      },
      {
       "name": "orderBy",
       "value": "startTime"
      }
     ]
    }
   }
  },
  {
   "id": "a9645abe-5adb-48b1-95b6-29ed4b63270b",
   "name": "Turnos para recordar",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    720,
    80
   ],
   "parameters": {
    "jsCode": "// ── Nodo «Turnos para recordar» (Code, una vez para todos los ítems) ──\n// Entrada: la respuesta de Google Calendar (events.list). Salida: un ítem por turno a recordar, con el teléfono en\n// formato de WhatsApp. Lo ya recordado queda en los datos estáticos del flujo: correrlo de más no repite nada.\nconst cfg = $('Configuración').first().json;\nconst memoria = $getWorkflowStaticData('global');\nmemoria.recordados = memoria.recordados || {};\nmemoria.porTelefono = memoria.porTelefono || {};\nconst DIAS = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];\nconst MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];\nfunction telefono(texto) {\n  for (const m of String(texto || '').matchAll(/\\+?\\d[\\d\\s().-]{6,}\\d/g)) {\n    const d = m[0].replace(/\\D/g, '');\n    if (d.length < 8 || d.length > 15) continue;\n    if (m[0].trim().startsWith('+') || d.length >= 11) return d;\n    return cfg.codigoPais === '54' ? '549' + d.replace(/^0/, '').replace(/^9/, '') : cfg.codigoPais + d.replace(/^0/, '');\n  }\n  return '';\n}\nconst salida = [];\nfor (const ev of $input.first().json.items || []) {\n  if (!ev.start?.dateTime || ev.status === 'cancelled' || /^\\[(Confirmado|Cancelado)\\]/i.test(ev.summary || '')) continue;\n  const clave = ev.id + ev.start.dateTime;\n  if (memoria.recordados[clave]) continue;\n  const tel = telefono(ev.description) || telefono(ev.location) || telefono(ev.summary);\n  if (!tel) continue;\n  const d = new Date(Date.parse(ev.start.dateTime) + cfg.zonaHoraria * 3600e3);\n  const fecha = `${DIAS[d.getUTCDay()]} ${d.getUTCDate()} de ${MESES[d.getUTCMonth()]}`, hora = `${d.getUTCHours()}:${String(d.getUTCMinutes()).padStart(2, '0')}`;\n  memoria.recordados[clave] = Date.now();\n  memoria.porTelefono[tel] = { eventoId: ev.id, titulo: ev.summary || '', fecha, hora, enviado: Date.now() };\n  salida.push({ json: { telefono: tel, eventoId: ev.id, titulo: ev.summary || '', fecha, hora } });\n}\n// Limpieza: lo recordado hace más de 7 días ya no hace falta.\nfor (const [k, t] of Object.entries(memoria.recordados)) if (Date.now() - t > 7 * 86400e3) delete memoria.recordados[k];\nreturn salida;\n"
   }
  },
  {
   "id": "0ee0e622-7567-4807-89e7-f8ebd883cd92",
   "name": "Enviar recordatorio por WhatsApp",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    960,
    80
   ],
   "parameters": {
    "method": "POST",
    "url": "={{ $('Configuración').first().json.waApiBase }}/{{ $('Configuración').first().json.phoneNumberId }}/messages",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ messaging_product: 'whatsapp', to: $json.telefono, type: 'template', template: { name: $('Configuración').first().json.plantilla, language: { code: $('Configuración').first().json.idioma }, components: [{ type: 'body', parameters: [{ type: 'text', text: $('Configuración').first().json.negocio }, { type: 'text', text: $json.fecha }, { type: 'text', text: $json.hora }] }] } }) }}",
    "options": {}
   }
  },
  {
   "id": "0086e38b-ea78-4777-9b42-32bd9b9135e3",
   "name": "Respuesta de WhatsApp",
   "type": "n8n-nodes-base.webhook",
   "typeVersion": 2.1,
   "position": [
    0,
    420
   ],
   "parameters": {
    "httpMethod": "POST",
    "path": "whatsapp-recordatorios",
    "options": {}
   },
   "webhookId": "fa765720-e64a-413e-9f6a-a2685f607c21"
  },
  {
   "id": "0c158023-5aca-4a48-9461-bfce04c0f21e",
   "name": "Configuración (respuestas)",
   "type": "n8n-nodes-base.set",
   "typeVersion": 3.4,
   "position": [
    240,
    420
   ],
   "parameters": {
    "assignments": {
     "assignments": [
      {
       "id": "fc34c3a2-57eb-469c-8e3a-941e41b66741",
       "name": "calendarioId",
       "value": "tuconsultorio@gmail.com",
       "type": "string"
      },
      {
       "id": "3b95139a-9d80-4a98-9572-d43e154f7f16",
       "name": "horasAntes",
       "value": 24,
       "type": "number"
      },
      {
       "id": "64ab350b-3879-4072-ac34-06d8200a876b",
       "name": "zonaHoraria",
       "value": -3,
       "type": "number"
      },
      {
       "id": "7a855f35-c3ae-41bf-9ff9-64bb0afa91a8",
       "name": "codigoPais",
       "value": "54",
       "type": "string"
      },
      {
       "id": "8e396fdc-00f7-448d-909b-35aa2e900d65",
       "name": "negocio",
       "value": "Consultorio Pérez",
       "type": "string"
      },
      {
       "id": "44121a89-ba5f-4d58-a7d2-a11cac57d5b4",
       "name": "waApiBase",
       "value": "https://graph.facebook.com/v23.0",
       "type": "string"
      },
      {
       "id": "bfd04902-28cc-40ba-9817-307aba73cfb0",
       "name": "phoneNumberId",
       "value": "TU_PHONE_NUMBER_ID",
       "type": "string"
      },
      {
       "id": "8a3f8d9f-3195-494c-b181-2979c90724a8",
       "name": "plantilla",
       "value": "recordatorio_turno",
       "type": "string"
      },
      {
       "id": "9f04198f-25a2-4966-b82a-27bda041d0db",
       "name": "idioma",
       "value": "es",
       "type": "string"
      },
      {
       "id": "c6b94d35-3056-44c7-a807-9247ccb6d175",
       "name": "linkReprogramar",
       "value": "https://calendar.app.google/…",
       "type": "string"
      },
      {
       "id": "a14986ce-72d7-48cc-816a-e31a17d80bd4",
       "name": "telegramChatId",
       "value": "TU_CHAT_ID",
       "type": "string"
      }
     ]
    },
    "includeOtherFields": true,
    "include": "all",
    "options": {}
   }
  },
  {
   "id": "10e80dc9-5287-4886-a9aa-c010119d3140",
   "name": "¿Respondió un recordatorio?",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    480,
    420
   ],
   "parameters": {
    "jsCode": "// ── Nodo «¿Respondió un recordatorio?» (Code) ──\n// Entrada: el aviso de WhatsApp Cloud API (formato de Meta, directo o reenviado por Kapso). Salida: un ítem si el\n// mensaje es «Confirmar» o «Cancelar» de alguien con un recordatorio de las últimas 48 h; nada en otro caso.\nconst memoria = $getWorkflowStaticData('global');\nconst salida = [];\nfor (const e of $input.first().json.body?.entry || []) for (const ch of e.changes || []) for (const m of ch.value?.messages || []) {\n  const texto = (m.button?.text || m.interactive?.button_reply?.title || m.text?.body || '').trim();\n  const rec = memoria.porTelefono?.[m.from];\n  if (!rec || rec.respondido || Date.now() - rec.enviado > 48 * 3600e3) continue;\n  const si = /^(confirmar|confirmo|s[ií]|ok|dale)\\b/i.test(texto), no = !si && /^(cancelar|cancelo|no)\\b/i.test(texto);\n  if (!si && !no) continue;\n  rec.respondido = si ? 'confirmado' : 'cancelado';\n  salida.push({ json: { telefono: m.from, nombre: ch.value?.contacts?.[0]?.profile?.name || '', estado: rec.respondido, ...rec,\n    tituloNuevo: `[${si ? 'Confirmado' : 'Cancelado'}] ` + rec.titulo.replace(/^\\[(Confirmado|Cancelado)\\]\\s*/i, ''), color: si ? '10' : '11' } });\n}\nreturn salida;\n"
   }
  },
  {
   "id": "26345471-1a55-46a4-b469-feaaf8224d86",
   "name": "Marcar el turno en el calendario",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    720,
    420
   ],
   "parameters": {
    "method": "PATCH",
    "url": "=https://www.googleapis.com/calendar/v3/calendars/{{ encodeURIComponent($('Configuración (respuestas)').first().json.calendarioId) }}/events/{{ $json.eventoId }}",
    "authentication": "predefinedCredentialType",
    "nodeCredentialType": "googleCalendarOAuth2Api",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ summary: $json.tituloNuevo, colorId: $json.color }) }}",
    "options": {}
   }
  },
  {
   "id": "3887c982-0ac2-4ca2-b230-a7b7e0e4792e",
   "name": "Contestarle al paciente",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    960,
    420
   ],
   "parameters": {
    "method": "POST",
    "url": "={{ $('Configuración (respuestas)').first().json.waApiBase }}/{{ $('Configuración (respuestas)').first().json.phoneNumberId }}/messages",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpHeaderAuth",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify({ messaging_product: 'whatsapp', to: $('¿Respondió un recordatorio?').item.json.telefono, type: 'text', text: { body: $('¿Respondió un recordatorio?').item.json.estado === 'confirmado' ? `¡Gracias! Tu turno del ${$('¿Respondió un recordatorio?').item.json.fecha} a las ${$('¿Respondió un recordatorio?').item.json.hora} quedó confirmado. Te esperamos.` : `Listo, cancelamos tu turno del ${$('¿Respondió un recordatorio?').item.json.fecha} a las ${$('¿Respondió un recordatorio?').item.json.hora}. Si quieres elegir otro horario: ${$('Configuración (respuestas)').first().json.linkReprogramar}` } }) }}",
    "options": {}
   }
  },
  {
   "id": "e5f3e60d-c716-40eb-818e-37d2a0464927",
   "name": "¿Canceló?",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    1200,
    420
   ],
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "loose",
      "version": 2
     },
     "conditions": [
      {
       "id": "72df373c-f1ad-474d-aa48-64e99917400b",
       "leftValue": "={{ $('¿Respondió un recordatorio?').item.json.estado }}",
       "rightValue": "cancelado",
       "operator": {
        "type": "string",
        "operation": "equals"
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   }
  },
  {
   "id": "0c832561-14d8-4164-86d0-3c8bb41f5f8a",
   "name": "Avisarme por Telegram",
   "type": "n8n-nodes-base.telegram",
   "typeVersion": 1.2,
   "position": [
    1440,
    400
   ],
   "parameters": {
    "chatId": "={{ $('Configuración (respuestas)').first().json.telegramChatId }}",
    "text": "=<b>Turno cancelado</b>: {{ $('¿Respondió un recordatorio?').item.json.nombre || '+' + $('¿Respondió un recordatorio?').item.json.telefono }}, {{ $('¿Respondió un recordatorio?').item.json.fecha }} a las {{ $('¿Respondió un recordatorio?').item.json.hora }}",
    "additionalFields": {
     "parse_mode": "HTML",
     "appendAttribution": false
    }
   }
  }
 ],
 "settings": {
  "executionOrder": "v1"
 },
 "pinData": {},
 "connections": {
  "Cada 30 minutos": {
   "main": [
    [
     {
      "node": "Configuración",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Probar ahora": {
   "main": [
    [
     {
      "node": "Configuración",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Configuración": {
   "main": [
    [
     {
      "node": "Leer turnos de Google Calendar",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Leer turnos de Google Calendar": {
   "main": [
    [
     {
      "node": "Turnos para recordar",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Turnos para recordar": {
   "main": [
    [
     {
      "node": "Enviar recordatorio por WhatsApp",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Respuesta de WhatsApp": {
   "main": [
    [
     {
      "node": "Configuración (respuestas)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Configuración (respuestas)": {
   "main": [
    [
     {
      "node": "¿Respondió un recordatorio?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "¿Respondió un recordatorio?": {
   "main": [
    [
     {
      "node": "Marcar el turno en el calendario",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Marcar el turno en el calendario": {
   "main": [
    [
     {
      "node": "Contestarle al paciente",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Contestarle al paciente": {
   "main": [
    [
     {
      "node": "¿Canceló?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "¿Canceló?": {
   "main": [
    [
     {
      "node": "Avisarme por Telegram",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 }
}