{
  "name": "LL Scheduling Automation",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ll-telegram",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "2aac85a8-2501-45cf-a016-872e70eaed76",
      "name": "Telegram Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        200,
        200
      ],
      "webhookId": "66b7637d-62c9-49f2-bf35-65a84140c56e"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ll-schedule",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "ca84194f-7a94-4e2f-b72d-72b4af14564e",
      "name": "GHL Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        200,
        500
      ],
      "webhookId": "55a9c4df-f8ab-42a4-977a-18a6817a8d9d"
    },
    {
      "parameters": {
        "respondWith": "noData",
        "options": {}
      },
      "id": "755d64c5-76d2-485d-8494-aabbcd7b0493",
      "name": "Ack Telegram",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        420,
        100
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\"ok\":true}",
        "options": {}
      },
      "id": "734b03c3-2694-4a97-8267-2f083a1302dd",
      "name": "Ack GHL",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        420,
        600
      ]
    },
    {
      "parameters": {
        "jsCode": "// Route Telegram message: extract text + chat ID, check for pending state, classify\nconst BOT_TOKEN = '8762360352:AAHjG_joJQiKgl97G0X3MW7zxZr8ibkUo24';\nconst msg = $input.first().json.body?.message || $input.first().json.message || {};\nconst chatId = msg.chat?.id?.toString() || '';\nconst text = (msg.text || '').trim();\nconst fromUser = msg.from?.first_name || 'User';\n\nif (!chatId || !text) return [];\n\n// Check for pending state in workflow static data\nconst state = $getWorkflowStaticData('global');\nconst session = state[`session_${chatId}`];\n\n// Handle responses to pending proposals\nif (session && session.status === 'awaiting_confirmation') {\n  const lc = text.toLowerCase();\n  if (['yes', 'confirm', 'book it', 'book', 'y'].includes(lc)) {\n    return [{ json: { action: 'confirm', chatId, session, botToken: BOT_TOKEN, triggerPath: 'telegram' } }];\n  } else if (['cancel', 'nevermind', 'never mind', 'no', 'n'].includes(lc)) {\n    delete state[`session_${chatId}`];\n    return [{ json: { action: 'cancel', chatId, contactName: session.contactName, botToken: BOT_TOKEN, triggerPath: 'telegram' } }];\n  } else {\n    // Treat as correction\n    return [{ json: { action: 'correction', chatId, text, session, botToken: BOT_TOKEN, triggerPath: 'telegram' } }];\n  }\n}\n\n// Handle undo\nif (session && session.status === 'undo_window') {\n  if (text.toLowerCase() === 'undo') {\n    return [{ json: { action: 'undo', chatId, session, botToken: BOT_TOKEN, triggerPath: 'telegram' } }];\n  }\n}\n\n// New message \u2014 check if scheduling request\nreturn [{ json: { action: 'new_message', chatId, text, fromUser, botToken: BOT_TOKEN, triggerPath: 'telegram' } }];"
      },
      "id": "9d691f3d-d69c-44aa-a382-49365d56bfb7",
      "name": "Route Telegram",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// Route GHL webhook: extract contact + note, format for scheduling\nconst BOT_TOKEN = '8762360352:AAHjG_joJQiKgl97G0X3MW7zxZr8ibkUo24';\nconst body = $input.first().json.body || $input.first().json;\nconst contactId = body.contactId || body.contact_id || '';\nconst firstName = body.firstName || body.first_name || '';\nconst lastName = body.lastName || body.last_name || '';\nconst contactName = `${firstName} ${lastName}`.trim();\nconst email = body.email || body.contactEmail || '';\nconst phone = body.phone || body.contactPhone || '';\nconst noteBody = body.body || body.noteBody || body.note || '';\n\n// We need Amanda's Telegram chat ID to send confirmations\n// This should be configured \u2014 for now use static data or env\nconst state = $getWorkflowStaticData('global');\nconst amandaChatId = state.amanda_chat_id || '';\n\nif (!contactId || !noteBody) {\n  return [{ json: { action: 'error', error: 'Missing contactId or note body', triggerPath: 'ghl' } }];\n}\n\nreturn [{ json: {\n  action: 'ghl_schedule',\n  chatId: amandaChatId,\n  contactId,\n  contactName,\n  contactEmail: email,\n  contactPhone: phone,\n  noteBody,\n  botToken: BOT_TOKEN,\n  triggerPath: 'ghl',\n} }];"
      },
      "id": "dab4064f-cda4-44c9-9c04-a41cb5443bbb",
      "name": "Route GHL",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        500
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "is-new",
              "leftValue": "={{ $json.action }}",
              "rightValue": "new_message",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "09945166-0e7f-4645-bf5c-a2f6e431fcc3",
      "name": "Is New Message?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        640,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// Parse message with OpenAI: classify + extract scheduling params\nconst data = $input.first().json;\nconst text = data.text || data.noteBody || '';\nconst isGhl = data.triggerPath === 'ghl';\n\nconst resp = await this.helpers.httpRequest({\n  method: 'POST',\n  url: 'https://api.openai.com/v1/chat/completions',\n  headers: {\n    'Authorization': 'Bearer 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',\n    'Content-Type': 'application/json',\n  },\n  body: {\n    model: 'gpt-4o-mini',\n    max_tokens: 300,\n    response_format: { type: 'json_object' },\n    messages: [\n      { role: 'system', content: 'You parse scheduling messages from a speech therapist into structured JSON. Return ONLY valid JSON with these fields: patient_name (string, empty if not mentioned), days (array of lowercase weekday names, empty array if not specified or \"any day\"), preference (\"am\" or \"pm\" or \"any\"), frequency (\"weekly\" or \"biweekly\" or \"2x_week\" or \"3x_week\"), sessions (number, default 8), session_type (\"adult\" or \"myo\", default \"adult\"), is_scheduling (boolean, true if this is a patient scheduling request). Mapping: \"every other week\"=\"biweekly\", \"twice a week\"=\"2x_week\", \"3 times a week\"=\"3x_week\", \"morning\"/\"AM\"=\"am\", \"afternoon\"/\"PM\"=\"pm\", \"myo\"/\"myofunctional\"=session_type \"myo\".' },\n      { role: 'user', content: text }\n    ],\n  },\n});\n\nlet parsed = {};\ntry {\n  parsed = JSON.parse(resp.choices[0].message.content);\n} catch (e) {\n  return [{ json: { ...data, parseError: true } }];\n}\n\nif (!parsed.is_scheduling) {\n  return [{ json: { ...data, action: 'not_scheduling', parsed } }];\n}\n\n// Count defaulted params\nconst defaults = { days: 0, preference: 'am', frequency: 'weekly', sessions: 8, session_type: 'adult' };\nconst defaulted = [];\nif (!parsed.days || parsed.days.length === 0) defaulted.push('days (any weekday)');\nif (parsed.preference === defaults.preference && !text.toLowerCase().match(/\\bam\\b|morning|before noon/)) defaulted.push('time (AM)');\nif (parsed.frequency === defaults.frequency && !text.toLowerCase().match(/weekly|week/)) defaulted.push('frequency (weekly)');\nif (parsed.sessions === defaults.sessions && !text.match(/\\b8\\b/)) defaulted.push('sessions (8)');\n\nconst calendarIds = { adult: 'blAjNdl84YudbyBaiSvu', myo: 'RBSNb6Vn1pqiZ6Kmjf3z' };\n\nreturn [{ json: {\n  ...data,\n  action: isGhl ? 'ghl_schedule' : 'schedule',\n  parsed,\n  patientName: parsed.patient_name || data.contactName || '',\n  days: parsed.days || [],\n  preference: parsed.preference || 'am',\n  frequency: parsed.frequency || 'weekly',\n  sessions: parsed.sessions || 8,\n  sessionType: parsed.session_type || 'adult',\n  calendarId: calendarIds[parsed.session_type || 'adult'],\n  duration: (parsed.session_type || 'adult') === 'adult' ? 45 : 30,\n  defaultedParams: defaulted,\n  parseError: false,\n} }];"
      },
      "id": "b63ea4d4-b68d-4d72-944b-763a1e61fc81",
      "name": "Parse with OpenAI",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        860,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Handle all routing: parse errors, not scheduling, search contact, schedule\nconst data = $input.first().json;\nconst BOT = data.botToken;\nconst CHAT = data.chatId;\nconst GHL_KEY = 'pit-cd82bbf6-d31f-47cd-8177-662ca2d817e1';\nconst GHL_LOC = '8HGLSPECfIaQfJaFO7Ef';\n\nasync function sendTelegram(chatId, text) {\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: `https://api.telegram.org/bot${BOT}/sendMessage`,\n    body: { chat_id: chatId, text, parse_mode: 'HTML' },\n    headers: { 'Content-Type': 'application/json' },\n  });\n}\n\n// Parse error\nif (data.parseError) {\n  if (CHAT) await sendTelegram.call(this, CHAT, \"I couldn't parse that scheduling request. Can you give me:\\n- Patient name\\n- Session type (adult/myo)\\n- Available days\\n- AM or PM\\n- Frequency (weekly/biweekly)\\n- Number of sessions\");\n  return [];\n}\n\n// Not a scheduling request\nif (data.action === 'not_scheduling') {\n  // Let it pass \u2014 Dakota would handle normally. For now, just acknowledge.\n  if (CHAT) await sendTelegram.call(this, CHAT, \"That doesn't look like a scheduling request. To schedule, say something like: \\\"Schedule Jay Smith Mon/Tue AM biweekly\\\"\");\n  return [];\n}\n\n// GHL path already has contact info — override for test mode\nif (data.triggerPath === 'ghl' && data.contactId) {\n  const TEST_MODE = true;\n  return [{ json: { ...data, action: 'run_scheduling', contactId: TEST_MODE ? '3V9Q7cRcAd8yGdvxS9r7' : data.contactId, contactEmail: TEST_MODE ? '1brycefolsom@gmail.com' : data.contactEmail, contactPhone: TEST_MODE ? '+14042131213' : data.contactPhone } }];\n}\n\n// Telegram path: search for patient in GHL\nconst searchName = data.patientName;\nif (!searchName) {\n  if (CHAT) await sendTelegram.call(this, CHAT, \"I need the patient's name to schedule. Who should I schedule?\");\n  return [];\n}\n\ntry {\n  const searchResp = await this.helpers.httpRequest({\n    method: 'GET',\n    url: 'https://services.leadconnectorhq.com/contacts/',\n    headers: { 'Authorization': `Bearer ${GHL_KEY}`, 'Version': '2021-07-28' },\n    qs: { locationId: GHL_LOC, query: searchName },\n  });\n  const contacts = searchResp.contacts || [];\n\n  if (contacts.length === 0) {\n    if (CHAT) await sendTelegram.call(this, CHAT, `I couldn't find a contact named \"${searchName}\" in GHL. Can you check the spelling?`);\n    return [];\n  }\n\n  if (contacts.length === 1) {\n    const c = contacts[0];\n    // TEST MODE: override contact for booking/notifications to Bryce Test\n    const TEST_MODE = true;\n    const TEST_CONTACT_ID = '3V9Q7cRcAd8yGdvxS9r7';\n    const TEST_EMAIL = '1brycefolsom@gmail.com';\n    const TEST_PHONE = '+14042131213';\n    return [{ json: {\n      ...data,\n      action: 'run_scheduling',\n      contactId: TEST_MODE ? TEST_CONTACT_ID : c.id,\n      contactName: `${c.firstName || ''} ${c.lastName || ''}`.trim(),\n      contactEmail: TEST_MODE ? TEST_EMAIL : (c.email || ''),\n      contactPhone: TEST_MODE ? TEST_PHONE : (c.phone || ''),\n      realContactId: c.id,\n    } }];\n  }\n\n  // Multiple matches\n  let msg = `I found ${contacts.length} contacts matching \"${searchName}\":\\n\\n`;\n  contacts.slice(0, 5).forEach((c, i) => {\n    msg += `${i + 1}. ${c.firstName || ''} ${c.lastName || ''} (${c.email || 'no email'}, ${c.phone || 'no phone'})\\n`;\n  });\n  msg += '\\nWhich one? Reply with the number.';\n\n  // Save disambiguation state\n  const state = $getWorkflowStaticData('global');\n  state[`session_${CHAT}`] = {\n    status: 'disambiguating',\n    contacts: contacts.slice(0, 5).map(c => ({ id: '3V9Q7cRcAd8yGdvxS9r7', realId: c.id, name: `${c.firstName || ''} ${c.lastName || ''}`.trim(), email: '1brycefolsom@gmail.com', phone: '+14042131213' })),\n    params: { days: data.days, preference: data.preference, frequency: data.frequency, sessions: data.sessions, sessionType: data.sessionType, calendarId: data.calendarId, duration: data.duration, defaultedParams: data.defaultedParams },\n  };\n\n  if (CHAT) await sendTelegram.call(this, CHAT, msg);\n  return [];\n} catch (e) {\n  if (CHAT) await sendTelegram.call(this, CHAT, `Error searching GHL: ${e.message}`);\n  return [];\n}"
      },
      "id": "750bc292-05a3-4acf-b716-458ad8deef92",
      "name": "Search Contact",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1080,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Fetch GHL calendar availability + run scheduling algorithm + send proposal\nconst data = $input.first().json;\nconst GHL_KEY = 'pit-cd82bbf6-d31f-47cd-8177-662ca2d817e1';\nconst BOT = data.botToken;\nconst CHAT = data.chatId;\n\nasync function sendTelegram(chatId, text) {\n  try {\n    await this.helpers.httpRequest({\n      method: 'POST', url: `https://api.telegram.org/bot${BOT}/sendMessage`,\n      body: { chat_id: chatId, text, parse_mode: 'HTML' },\n      headers: { 'Content-Type': 'application/json' },\n    });\n  } catch(e) { /* Telegram send failed — chat may not exist */ }\n}\n\nconst calendarId = data.calendarId;\nconst freqMap = { biweekly: 14, weekly: 7, '2x_week': 3, '3x_week': 2 };\nconst freq = freqMap[data.frequency] || 7;\nconst weeksNeeded = Math.ceil((data.sessions * freq) / 7) + 4;\n\n// Fetch slots\nconst start = new Date(); start.setDate(start.getDate() + 1); start.setHours(0,0,0,0);\nconst end = new Date(start); end.setDate(end.getDate() + weeksNeeded * 7);\nconst allSlots = {};\nlet cs = new Date(start);\n\nwhile (cs < end) {\n  const ce = new Date(Math.min(cs.getTime() + 14*86400000, end.getTime()));\n  try {\n    const r = await this.helpers.httpRequest({\n      method: 'GET', url: `https://services.leadconnectorhq.com/calendars/${calendarId}/free-slots`,\n      headers: { 'Authorization': `Bearer ${GHL_KEY}`, 'Version': '2021-04-15' },\n      qs: { startDate: cs.getTime(), endDate: ce.getTime(), timezone: 'America/New_York' },\n    });\n    for (const [dk, v] of Object.entries(r)) {\n      if (dk === 'traceId') continue;\n      const ds = dk.substring(0,10);\n      const sl = v?.slots || (Array.isArray(v) ? v : []);\n      const ts = sl.filter(s => typeof s === 'string' && s.includes('T')).map(s => s.split('T')[1].substring(0,5));\n      if (ts.length) allSlots[ds] = ts.sort();\n    }\n  } catch (e) {\n    if (e.message?.includes('429')) { await new Promise(r => setTimeout(r, 5000)); }\n  }\n  cs = ce;\n}\n\nif (Object.keys(allSlots).length === 0) {\n  if (CHAT) await sendTelegram.call(this, CHAT, `No available slots on the ${data.sessionType} calendar in the next ${weeksNeeded} weeks. Amanda may need to open more availability.`);\n  return [];\n}\n\n// Scheduling algorithm\nconst DAY_MAP = {monday:0,tuesday:1,wednesday:2,thursday:3,friday:4,saturday:5,sunday:6};\nconst PREF = {am:[480,720],morning:[480,720],pm:[720,1020],afternoon:[720,1020],any:[480,1020]};\nconst [prefS, prefE] = PREF[data.preference] || PREF.any;\nconst allowed = new Set((data.days||[]).map(d => DAY_MAP[d.toLowerCase()]).filter(d => d !== undefined));\nif (allowed.size === 0) [0,1,2,3,4].forEach(d => allowed.add(d));\n\nfunction toMin(t) { const [h,m] = t.split(':').map(Number); return h*60+m; }\nfunction inPref(t) { const m = toMin(t); return m >= prefS && m < prefE; }\n\nconst candidates = [];\nfor (const [dateStr, times] of Object.entries(allSlots)) {\n  const dt = new Date(dateStr + 'T12:00:00');\n  const wd = dt.getDay() === 0 ? 6 : dt.getDay() - 1;\n  for (const t of times) {\n    candidates.push({ date: dateStr, time: t, weekday: wd, onAllowed: allowed.has(wd), inPref: inPref(t) });\n  }\n}\n\ncandidates.sort((a,b) => {\n  if (a.onAllowed !== b.onAllowed) return a.onAllowed ? -1 : 1;\n  if (a.inPref !== b.inPref) return a.inPref ? -1 : 1;\n  if (a.date !== b.date) return a.date < b.date ? -1 : 1;\n  return a.time < b.time ? -1 : 1;\n});\n\nconst selected = [];\nconst earliestDate = candidates.reduce((min, c) => c.date < min ? c.date : min, candidates[0].date);\nlet windowStart = new Date(earliestDate+'T00:00:00');\nlet remaining = [...candidates];\nlet maxA = data.sessions * 10, att = 0;\n\nwhile (selected.length < data.sessions && remaining.length > 0 && att < maxA) {\n  att++;\n  const wEnd = new Date(windowStart.getTime() + freq * 86400000);\n  let inW = remaining.filter(c => { const d = new Date(c.date+'T00:00:00'); return d >= windowStart && d < wEnd; });\n  if (!inW.length) {\n    windowStart = wEnd;\n    const fut = remaining.filter(c => new Date(c.date+'T00:00:00') >= windowStart);\n    if (!fut.length) break;\n    windowStart = new Date(fut[0].date+'T00:00:00');\n    continue;\n  }\n  inW.sort((a,b) => {\n    if (a.onAllowed !== b.onAllowed) return a.onAllowed ? -1 : 1;\n    if (a.inPref !== b.inPref) return a.inPref ? -1 : 1;\n    return a.date < b.date ? -1 : 1;\n  });\n  const best = inW[0];\n  selected.push({ date: best.date, time: best.time });\n  remaining = remaining.filter(c => c.date !== best.date);\n  windowStart = new Date(new Date(best.date+'T00:00:00').getTime() + freq * 86400000);\n}\n\nselected.sort((a,b) => a.date < b.date ? -1 : 1);\n\nif (selected.length === 0) {\n  if (CHAT) await sendTelegram.call(this, CHAT, 'Could not find any slots matching your criteria. Try different days or time preference?');\n  return [];\n}\n\n// Build proposal message\nconst typeLabel = data.sessionType === 'myo' ? 'Myo (30 min)' : 'Adult (45 min)';\nconst fn = data.contactName || 'Patient';\nlet msg = `<b>Proposed schedule for ${fn}:</b>\\n\\n`;\nmsg += `${selected.length} ${typeLabel} sessions, ${(data.days||[]).join('/')||'any day'} ${data.preference.toUpperCase()}, ${data.frequency}\\n\\n`;\n\nfor (let i = 0; i < selected.length; i++) {\n  const a = selected[i];\n  const dt = new Date(a.date+'T12:00:00');\n  const dayN = dt.toLocaleDateString('en-US',{weekday:'short'});\n  const dateD = dt.toLocaleDateString('en-US',{month:'short',day:'numeric'});\n  const [h,m] = a.time.split(':').map(Number);\n  const ap = h >= 12 ? 'PM' : 'AM';\n  const h12 = h > 12 ? h-12 : (h===0 ? 12 : h);\n  msg += `${i+1}. ${dayN} ${dateD} @ ${h12}:${String(m).padStart(2,'0')} ${ap}\\n`;\n}\n\nif (selected.length < data.sessions) {\n  msg += `\\n\u26a0\ufe0f Only found ${selected.length} of ${data.sessions} requested sessions.\\n`;\n}\nif (data.defaultedParams && data.defaultedParams.length >= 3) {\n  msg += `\\n\u26a0\ufe0f I used defaults for: ${data.defaultedParams.join(', ')}\\n`;\n}\nmsg += '\\nReply <b>YES</b> to book, or tell me what to change.';\n\n// Save state\nconst state = $getWorkflowStaticData('global');\nstate[`session_${CHAT}`] = {\n  status: 'awaiting_confirmation',\n  contactId: data.contactId,\n  contactName: data.contactName,\n  contactEmail: data.contactEmail,\n  contactPhone: data.contactPhone,\n  calendarId: data.calendarId,\n  sessionType: data.sessionType,\n  params: { days: data.days, preference: data.preference, frequency: data.frequency, sessions: data.sessions },\n  proposedSlots: selected,\n  triggerPath: data.triggerPath,\n  defaultedParams: data.defaultedParams,\n  createdAt: new Date().toISOString(),\n};\n\nif (CHAT) await sendTelegram.call(this, CHAT, msg);\n\n// Also save Amanda's chat ID for future GHL triggers\nif (data.triggerPath === 'telegram' && CHAT) {\n  state.amanda_chat_id = CHAT;\n}\n\nreturn [];"
      },
      "id": "90872b68-742d-49cb-9211-334a37819b32",
      "name": "Schedule + Propose",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1300,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Handle confirmation: book appointments + send notifications\nconst data = $input.first().json;\nconst GHL_KEY = 'pit-cd82bbf6-d31f-47cd-8177-662ca2d817e1';\nconst GHL_LOC = '8HGLSPECfIaQfJaFO7Ef';\nconst BOT = data.botToken;\nconst CHAT = data.chatId;\nconst session = data.session;\n\nasync function sendTelegram(chatId, text) {\n  try {\n    await this.helpers.httpRequest({\n      method: 'POST', url: `https://api.telegram.org/bot${BOT}/sendMessage`,\n      body: { chat_id: chatId, text, parse_mode: 'HTML' },\n      headers: { 'Content-Type': 'application/json' },\n    });\n  } catch(e) { /* Telegram send failed — chat may not exist */ }\n}\n\nconst slots = session.proposedSlots;\nconst contactId = session.contactId;\nconst fn = (session.contactName || '').split(' ')[0] || 'there';\nconst typeLabel = session.sessionType === 'myo' ? 'Myofunctional Therapy (30 min)' : 'Adult Speech Therapy (45 min)';\n\n// Book all appointments with 1s delay\nconst bookedIds = [];\nconst failures = [];\nfor (const slot of slots) {\n  try {\n    const r = await this.helpers.httpRequest({\n      method: 'POST', url: 'https://services.leadconnectorhq.com/calendars/events/appointments',\n      headers: { 'Authorization': `Bearer ${GHL_KEY}`, 'Content-Type': 'application/json', 'Version': '2021-04-15' },\n      body: { calendarId: session.calendarId, locationId: GHL_LOC, contactId, startTime: `${slot.date}T${slot.time}:00`, timezone: 'America/New_York' },\n    });\n    bookedIds.push(r.id || r.appointmentId || 'ok');\n    await new Promise(r => setTimeout(r, 1000));\n  } catch (e) {\n    failures.push(`${slot.date} ${slot.time}: ${e.message}`);\n    if (failures.length > 2) break; // Abort if >2 failures\n  }\n}\n\nconst booked = bookedIds.length;\n\n// Send patient notifications (email + SMS)\nif (booked > 0) {\n  // Build branded HTML email\n  let rows = '';\n  for (let i = 0; i < slots.length && i < booked; i++) {\n    const s = slots[i]; const dt = new Date(s.date+'T12:00:00');\n    const [h,m] = s.time.split(':').map(Number); const ap = h>=12?'PM':'AM'; const h12 = h>12?h-12:(h===0?12:h);\n    const bg = i%2===0?'#ffffff':'#f7f9fb';\n    rows += `<tr style=\"background:${bg};\"><td style=\"padding:12px 14px;border-bottom:1px solid #e8ecf0;font-size:14px;color:#41535f;font-weight:600;\">${i+1}</td><td style=\"padding:12px 14px;border-bottom:1px solid #e8ecf0;font-size:14px;color:#1a2b34;\">${dt.toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'})}</td><td style=\"padding:12px 14px;border-bottom:1px solid #e8ecf0;font-size:14px;color:#1a2b34;font-weight:500;\">${h12}:${String(m).padStart(2,'0')} ${ap}</td></tr>`;\n  }\n  const html = `<body style=\"margin:0;padding:0;background:#f2f4f6;font-family:'Inter',Arial,sans-serif;\"><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f2f4f6;\"><tr><td align=\"center\" style=\"padding:32px 16px;\"><table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:600px;width:100%;\"><tr><td style=\"background:#41535f;padding:28px 32px;border-radius:12px 12px 0 0;text-align:center;\"><img src=\"https://cdn.prod.website-files.com/68cb3774dbd0700a0d232cab/68e6e10072d1aad4355c7c6b_Logo.jpg\" alt=\"Lasting Language Therapy Services\" width=\"280\" style=\"display:block;margin:0 auto;border-radius:8px;\" /></td></tr><tr><td style=\"background:#ffffff;padding:36px 32px 24px;\"><h1 style=\"margin:0 0 8px;font-size:22px;font-weight:600;color:#1a2b34;letter-spacing:-0.3px;\">Your Appointment Schedule</h1><p style=\"margin:0 0 24px;font-size:15px;color:#5d6c7b;line-height:1.6;\">Hi ${fn}, your <strong style=\"color:#41535f;\">${typeLabel}</strong> sessions have been scheduled. Here are your upcoming appointments:</p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:0 0 28px;border-radius:8px;overflow:hidden;border:1px solid #e8ecf0;\"><tr style=\"background:#41535f;\"><th style=\"padding:10px 14px;text-align:left;font-size:12px;font-weight:600;color:#ffffff;text-transform:uppercase;letter-spacing:0.5px;\">#</th><th style=\"padding:10px 14px;text-align:left;font-size:12px;font-weight:600;color:#ffffff;text-transform:uppercase;letter-spacing:0.5px;\">Date</th><th style=\"padding:10px 14px;text-align:left;font-size:12px;font-weight:600;color:#ffffff;text-transform:uppercase;letter-spacing:0.5px;\">Time</th></tr>${rows}</table><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:0 0 24px;background:#f7f9fb;border-radius:8px;border:1px solid #e8ecf0;\"><tr><td style=\"padding:20px 20px 16px;\"><p style=\"margin:0 0 4px;font-size:13px;font-weight:600;color:#41535f;text-transform:uppercase;letter-spacing:0.5px;\">Location</p><p style=\"margin:0 0 12px;font-size:15px;color:#1a2b34;font-weight:500;line-height:1.5;\">6667 Vernon Woods Dr NE<br>Suite B-16<br>Sandy Springs, GA 30328</p><p style=\"margin:0;font-size:13px;color:#5d6c7b;line-height:1.6;\">We are located on the back side of the B building, along the fence line adjacent to the Toyota dealership. If you have difficulty walking, please park in the lower parking lot to avoid the stairs.</p></td></tr></table><p style=\"margin:0 0 8px;font-size:15px;color:#1a2b34;line-height:1.6;\">Need to reschedule? Give us a call:</p><a href=\"tel:4708514988\" style=\"display:inline-block;padding:12px 28px;background:#41535f;color:#ffffff;font-size:14px;font-weight:600;text-decoration:none;border-radius:6px;letter-spacing:0.3px;\">(470) 851-4988</a></td></tr><tr><td style=\"background:#f7f9fb;padding:24px 32px;border-radius:0 0 12px 12px;border-top:1px solid #e8ecf0;\"><p style=\"margin:0 0 4px;font-size:15px;font-weight:600;color:#1a2b34;\">Lasting Language Therapy Services</p><p style=\"margin:0 0 12px;font-size:13px;color:#5d6c7b;line-height:1.5;\">(470) 851-4988 &bull; lastinglanguagetherapy.com</p><p style=\"margin:0;font-size:12px;color:#98a5b1;line-height:1.5;\">Speech therapy that empowers every voice — from toddlers to adults.</p></td></tr></table></td></tr></table></body>`;\n\n  // Email\n  try {\n    await this.helpers.httpRequest({\n      method: 'POST', url: 'https://services.leadconnectorhq.com/conversations/messages',\n      headers: { 'Authorization': `Bearer ${GHL_KEY}`, 'Content-Type': 'application/json', 'Version': '2021-07-28' },\n      body: { type: 'Email', contactId, subject: `Your ${typeLabel} Schedule \u2014 Lasting Language`, html },\n    });\n  } catch(e) {}\n\n  // Patient SMS\n  const firstDate = new Date(slots[0].date+'T12:00:00').toLocaleDateString('en-US',{month:'long',day:'numeric'});\n  try {\n    await this.helpers.httpRequest({\n      method: 'POST', url: 'https://services.leadconnectorhq.com/conversations/messages',\n      headers: { 'Authorization': `Bearer ${GHL_KEY}`, 'Content-Type': 'application/json', 'Version': '2021-07-28' },\n      body: { type: 'SMS', contactId, message: `Hi ${fn}, your ${typeLabel.split('(')[0].trim()} sessions are booked at Lasting Language. ${booked} sessions starting ${firstDate}. Confirmation email on the way. Call (470) 851-4988 with questions.` },\n    });\n  } catch(e) {}\n}\n\n// Update state for undo window\nconst state = $getWorkflowStaticData('global');\nconst firstD = new Date(slots[0].date+'T12:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric'});\nconst lastD = new Date(slots[slots.length-1].date+'T12:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric'});\n\nstate[`session_${CHAT}`] = {\n  ...session,\n  status: 'undo_window',\n  bookedAppointmentIds: bookedIds,\n  undoExpiry: new Date(Date.now() + 5*60*1000).toISOString(),\n};\n\n// Send Amanda confirmation\nlet confirmMsg = `\u2705 <b>Done!</b> Booked ${booked} sessions for ${session.contactName} (${firstD} - ${lastD}).\\nPatient email and text queued.`;\nif (failures.length > 0) {\n  confirmMsg += `\\n\\n\u26a0\ufe0f ${failures.length} slot(s) failed:\\n${failures.join('\\n')}`;\n}\nconfirmMsg += '\\n\\nReply <b>UNDO</b> within 5 minutes to cancel all.';\n\nif (CHAT) await sendTelegram.call(this, CHAT, confirmMsg);\n\n// Schedule state cleanup after 5 min\nsetTimeout(() => {\n  const s = $getWorkflowStaticData('global');\n  if (s[`session_${CHAT}`]?.status === 'undo_window') {\n    delete s[`session_${CHAT}`];\n  }\n}, 5 * 60 * 1000);\n\nreturn [];"
      },
      "id": "3ce9d46a-7778-4c64-a588-399c2c8d4748",
      "name": "Book + Notify",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        860,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "// Handle cancellation\nconst data = $input.first().json;\nconst BOT = data.botToken;\nconst CHAT = data.chatId;\n\ntry {\n  await this.helpers.httpRequest({\n    method: 'POST', url: `https://api.telegram.org/bot${BOT}/sendMessage`,\n    body: { chat_id: CHAT, text: `Cancelled. No appointments booked for ${data.contactName || 'this patient'}.` },\n    headers: { 'Content-Type': 'application/json' },\n  });\n} catch(e) {}\n\nreturn [];"
      },
      "id": "ad581a29-744c-47b5-9553-20ad71a4c94e",
      "name": "Handle Cancel",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        860,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Handle undo: delete all booked appointments\nconst data = $input.first().json;\nconst GHL_KEY = 'pit-cd82bbf6-d31f-47cd-8177-662ca2d817e1';\nconst BOT = data.botToken;\nconst CHAT = data.chatId;\nconst session = data.session;\n\n// Check undo window\nif (session.undoExpiry && new Date() > new Date(session.undoExpiry)) {\n  try { await this.helpers.httpRequest({\n    method: 'POST', url: `https://api.telegram.org/bot${BOT}/sendMessage`,\n    body: { chat_id: CHAT, text: 'Undo window has expired. Please remove appointments manually in GHL.' },\n    headers: { 'Content-Type': 'application/json' },\n  }); } catch(e) {}\n  const state = $getWorkflowStaticData('global');\n  delete state[`session_${CHAT}`];\n  return [];\n}\n\n// Delete each appointment\nlet deleted = 0;\nfor (const apptId of (session.bookedAppointmentIds || [])) {\n  if (apptId === 'ok') continue; // No ID captured\n  try {\n    await this.helpers.httpRequest({\n      method: 'DELETE',\n      url: `https://services.leadconnectorhq.com/calendars/events/appointments/${apptId}`,\n      headers: { 'Authorization': `Bearer ${GHL_KEY}`, 'Version': '2021-04-15' },\n    });\n    deleted++;\n  } catch(e) {}\n}\n\nconst state = $getWorkflowStaticData('global');\ndelete state[`session_${CHAT}`];\n\nawait this.helpers.httpRequest({\n  method: 'POST', url: `https://api.telegram.org/bot${BOT}/sendMessage`,\n  body: { chat_id: CHAT, text: `\u21a9\ufe0f Undone. ${deleted} appointments cancelled for ${session.contactName}.` },\n  headers: { 'Content-Type': 'application/json' },\n});\n\nreturn [];"
      },
      "id": "ae7897fa-59b1-4f41-9ea2-da22c31881b6",
      "name": "Handle Undo",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        640,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Handle correction: update params and re-schedule\nconst data = $input.first().json;\nconst session = data.session;\n\n// Parse the correction with OpenAI\nconst resp = await this.helpers.httpRequest({\n  method: 'POST', url: 'https://api.openai.com/v1/chat/completions',\n  headers: {\n    'Authorization': 'Bearer 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',\n    'Content-Type': 'application/json',\n  },\n  body: {\n    model: 'gpt-4o-mini', max_tokens: 200, response_format: { type: 'json_object' },\n    messages: [\n      { role: 'system', content: `The user wants to modify a scheduling request. Current params: ${JSON.stringify(session.params)}. Parse their correction and return the UPDATED full params as JSON with fields: days (array), preference (am/pm/any), frequency, sessions, session_type. Only change what they mention, keep everything else the same.` },\n      { role: 'user', content: data.text }\n    ],\n  },\n});\n\nlet updated = session.params;\ntry { updated = JSON.parse(resp.choices[0].message.content); } catch(e) {}\n\nconst calendarIds = { adult: 'blAjNdl84YudbyBaiSvu', myo: 'RBSNb6Vn1pqiZ6Kmjf3z' };\n\n// Pass through to Schedule + Propose with updated params\nreturn [{ json: {\n  ...data,\n  action: 'run_scheduling',\n  contactId: session.contactId,\n  contactName: session.contactName,\n  contactEmail: session.contactEmail,\n  contactPhone: session.contactPhone,\n  days: updated.days || session.params.days,\n  preference: updated.preference || session.params.preference,\n  frequency: updated.frequency || session.params.frequency,\n  sessions: updated.sessions || session.params.sessions,\n  sessionType: updated.session_type || session.params.sessionType,\n  calendarId: calendarIds[updated.session_type || session.params.sessionType],\n  duration: (updated.session_type || session.params.sessionType) === 'myo' ? 30 : 45,\n  defaultedParams: [],\n} }];"
      },
      "id": "9ee9ad6f-812c-43b2-8fe8-52bd3f3c21ca",
      "name": "Handle Correction",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        640,
        400
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            { "outputKey": "confirm", "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict" }, "conditions": [{ "leftValue": "={{ $json.action }}", "rightValue": "confirm", "operator": { "type": "string", "operation": "equals" } }], "combinator": "and" } },
            { "outputKey": "cancel", "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict" }, "conditions": [{ "leftValue": "={{ $json.action }}", "rightValue": "cancel", "operator": { "type": "string", "operation": "equals" } }], "combinator": "and" } },
            { "outputKey": "correction", "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict" }, "conditions": [{ "leftValue": "={{ $json.action }}", "rightValue": "correction", "operator": { "type": "string", "operation": "equals" } }], "combinator": "and" } },
            { "outputKey": "undo", "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict" }, "conditions": [{ "leftValue": "={{ $json.action }}", "rightValue": "undo", "operator": { "type": "string", "operation": "equals" } }], "combinator": "and" } }
          ]
        },
        "options": { "fallbackOutput": "extra" }
      },
      "id": "928d8e85-fc22-4af1-b296-0108f5ece6fe",
      "name": "Action Router",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        640,
        200
      ]
    }
  ],
  "connections": {
    "Telegram Webhook": {
      "main": [
        [
          {
            "node": "Ack Telegram",
            "type": "main",
            "index": 0
          },
          {
            "node": "Route Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GHL Webhook": {
      "main": [
        [
          {
            "node": "Ack GHL",
            "type": "main",
            "index": 0
          },
          {
            "node": "Route GHL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Telegram": {
      "main": [
        [
          {
            "node": "Action Router",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route GHL": {
      "main": [
        [
          {
            "node": "Parse with OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Action Router": {
      "main": [
        [
          {
            "node": "Book + Notify",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Cancel",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Correction",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Undo",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Is New Message?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is New Message?": {
      "main": [
        [
          {
            "node": "Parse with OpenAI",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Parse with OpenAI": {
      "main": [
        [
          {
            "node": "Search Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Contact": {
      "main": [
        [
          {
            "node": "Schedule + Propose",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle Correction": {
      "main": [
        [
          {
            "node": "Schedule + Propose",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}