{"repo": "advika07/bookingagent", "files": {"index.js": {"940b980902f0e1d309d6b8a5d8241467d3252da0": {"t": 1785746703.4980354, "n": 69803, "h": "1a274482aa3d40a1", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e' // Hardcoded store_id\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name, storeName = null) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n let prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n `\n\n if (storeName) {\n prompt += `\n The message may reference a store. Ensure that any reference to a store uses the name \"${storeName}\" instead of any other store name like \"Glamour Salon\" unless explicitly intended.\n `\n }\n\n prompt += `\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n let rephrased = completion.choices[0].message.content.trim()\n rephrased = rephrased.replace(/\\[Name\\]/g, name)\n return rephrased\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay]\n \n if (!storeHours) {\n throw new Error(`Operating hours for ${apptDay} not found for store ${storeId}`)\n }\n\n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay]\n \n if (!storeHours) {\n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n continue\n }\n\n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - PROVIDE_PHONE_NUMBER (e.g., \"96460132\", \"6596460132\" when the state step is 'package_inquiry' and phoneNumberRequested is true)\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n - If the current state has step 'selectOption' and the message contains keywords like \"address\", \"hours\", \"services\", or \"packages\", classify the intent as STORE_INFO and set the info_type accordingly.\n - If the current state has step 'package_inquiry' and phoneNumberRequested is true, and the message is a sequence of digits (e.g., \"96460132\", \"6596460132\"), classify the intent as PROVIDE_PHONE_NUMBER and extract the phone_number.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests, \"packages\" for package-related requests)\n - phone_number (for PROVIDE_PHONE_NUMBER intent: the phone number provided, e.g., \"96460132\")\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\",\n \"phone_number\": \"PHONE_NUMBER or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (currentState.step === 'selectOption') {\n let infoType = null\n if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('hours') || lowerMessage.includes('opening') || lowerMessage.includes('operating')) infoType = 'hours'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n if (infoType) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: currentState.storeName || null,\n service_name: null,\n date: null,\n time: null,\n info_type: infoType,\n phone_number: null\n }\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (currentState.step === 'package_inquiry' && currentState.phoneNumberRequested && message.match(/^\\d+$/)) {\n return {\n intent: 'PROVIDE_PHONE_NUMBER',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: message\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType,\n phone_number: null\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null,\n phone_number: null\n }\n }\n }\n}\n\nasync function extractNameAndNumber(message) {\n try {\n console.log('Extracting name and number from message:', message)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and extract the name and phone number. The message may contain a name and a phone number in various formats, such as:\n - \"Name Number\" (e.g., \"John 96460132\")\n - \"Number Name\" (e.g., \"96460132 John\")\n - \"Name, Number\" (e.g., \"John, 96460132\")\n - \"My name is Name and my number is Number\" (e.g., \"My name is John and my number is 96460132\")\n - \"Number is my number and I'm Name\" (e.g., \"96460132 is my number and I'm John\")\n \n The name will typically be a string of alphabetic characters (e.g., \"John\", \"Advika\"), and the phone number will be a sequence of digits, possibly starting with a country code like +65 (e.g., \"96460132\", \"+6596460132\").\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"name\": \"EXTRACTED_NAME or null\",\n \"phoneNumber\": \"EXTRACTED_PHONE_NUMBER or null\"\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in extracting customer names and phone numbers from natural language with a focus on flexible format recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 100,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Extracted name and number:', result)\n\n // Normalize phone number by removing country code if present\n if (result.phoneNumber) {\n result.phoneNumber = result.phoneNumber.replace(/^\\+65/, '')\n }\n\n return result\n } catch (error) {\n console.error('DeepSeek name and number extraction error:', error.message)\n return { name: null, phoneNumber: null }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n if (!clientData && req.body.ProfileName) {\n name = req.body.ProfileName\n }\n\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n const { intent, details } = await classifyIntent(reply, currentState)\n\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else if (!conversationState.has(from)) {\n currentState = { intent }\n }\n\n conversationState.set(from, currentState)\n let state = conversationState.get(from) || {}\n console.log('Current conversation state after intent classification:', state)\n\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1' || details.info_type === 'address') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (reply === '2' || details.info_type === 'hours') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (reply === '3' || details.info_type === 'services') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (reply === '4' || details.info_type === 'packages') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n if (details.info_type === 'address') {\n const { store } = await fetchStoreInfo(state.storeId)\n responseMessage = `the address for ${state.storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (details.info_type === 'hours') {\n const { store } = await fetchStoreInfo(state.storeId)\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${state.storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (details.info_type === 'services') {\n const { services } = await fetchStoreInfo(state.storeId)\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${state.storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (details.info_type === 'packages') {\n const { packages } = await fetchStoreInfo(state.storeId)\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${state.storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n }\n } else {\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n if (servicesError) throw servicesError\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const { name: clientName, phoneNumber } = await extractNameAndNumber(reply)\n if (clientName && phoneNumber) {\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else if (intent === 'PROVIDE_PHONE_NUMBER') {\n const phoneNumber = details.phone_number\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n } else {\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n conversationState.set(from, { intent: 'GREETING' })\n } else if (intent === 'BOOK_APPOINTMENT') {\n state.step = 'book_appointment'\n state.intent = 'BOOK_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'VIEW_APPOINTMENTS'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString())\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CONFIRM_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CANCEL_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n }\n } else if (intent === 'STORE_INFO') {\n state.step = 'store_info'\n state.intent = 'STORE_INFO'\n if (details.store_name) state.storeName = details.store_name\n if (details.info_type) state.info_type = details.info_type\n conversationState.set(from, state)\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'PACKAGE_INQUIRY') {\n state.step = 'package_inquiry'\n state.intent = 'PACKAGE_INQUIRY'\n state.phoneNumberRequested = false\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `sorry ${name} I’m not sure what you meant you can book an appointment change an appointment get store info or check your package details just let me know how I can help`\n conversationState.delete(from)\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name, state.storeName)\n console.log('Sending response:', rephrasedMessage)\n\n const twiml = new twilio.twiml.MessagingResponse()\n twiml.message(rephrasedMessage)\n res.set('Content-Type', 'text/xml')\n res.send(twiml.toString())\n } catch (error) {\n console.error('Error processing webhook:', error.message)\n const twiml = new twilio.twiml.MessagingResponse()\n twiml.message(`sorry I ran into an issue please try again or contact support`)\n res.set('Content-Type', 'text/xml')\n res.send(twiml.toString())\n }\n})\n\napp.listen(3000, () => {\n console.log('Server running on port 3000')\n})\n"}, "e1b13e13db59c2d0b8726291cd6dc9beba463d76": {"t": 1785767088.3519132, "n": 67928, "h": "a27b06835a22e51e", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e' // Hardcoded store_id\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name, storeName = null) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n let prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n `\n\n if (storeName) {\n prompt += `\n The message may reference a store. Ensure that any reference to a store uses the name \"${storeName}\" instead of any other store name like \"Glamour Salon\" unless explicitly intended.\n `\n }\n\n prompt += `\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n let rephrased = completion.choices[0].message.content.trim()\n rephrased = rephrased.replace(/\\[Name\\]/g, name)\n return rephrased\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay]\n \n if (!storeHours) {\n throw new Error(`Operating hours for ${apptDay} not found for store ${storeId}`)\n }\n\n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay]\n \n if (!storeHours) {\n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n continue\n }\n\n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n - If the current state has step 'selectOption' and the message contains keywords like \"address\", \"hours\", \"services\", or \"packages\", classify the intent as STORE_INFO and set the info_type accordingly.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests, \"packages\" for package-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'selectOption') {\n let infoType = null\n if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('hours') || lowerMessage.includes('opening') || lowerMessage.includes('operating')) infoType = 'hours'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n if (infoType) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: currentState.storeName || null,\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\nasync function extractNameAndNumber(message) {\n try {\n console.log('Extracting name and number from message:', message)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and extract the name and phone number. The message may contain a name and a phone number in various formats, such as:\n - \"Name Number\" (e.g., \"John 96460132\")\n - \"Number Name\" (e.g., \"96460132 John\")\n - \"Name, Number\" (e.g., \"John, 96460132\")\n - \"My name is Name and my number is Number\" (e.g., \"My name is John and my number is 96460132\")\n - \"Number is my number and I'm Name\" (e.g., \"96460132 is my number and I'm John\")\n \n The name will typically be a string of alphabetic characters (e.g., \"John\", \"Advika\"), and the phone number will be a sequence of digits, possibly starting with a country code like +65 (e.g., \"96460132\", \"+6596460132\").\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"name\": \"EXTRACTED_NAME or null\",\n \"phoneNumber\": \"EXTRACTED_PHONE_NUMBER or null\"\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in extracting customer names and phone numbers from natural language with a focus on flexible format recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 100,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Extracted name and number:', result)\n\n // Normalize phone number by removing country code if present\n if (result.phoneNumber) {\n result.phoneNumber = result.phoneNumber.replace(/^\\+65/, '')\n }\n\n return result\n } catch (error) {\n console.error('DeepSeek name and number extraction error:', error.message)\n return { name: null, phoneNumber: null }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n if (!clientData && req.body.ProfileName) {\n name = req.body.ProfileName\n }\n\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n const { intent, details } = await classifyIntent(reply, currentState)\n\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else if (!conversationState.has(from)) {\n currentState = { intent }\n }\n\n conversationState.set(from, currentState)\n let state = conversationState.get(from) || {}\n console.log('Current conversation state after intent classification:', state)\n\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1' || details.info_type === 'address') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (reply === '2' || details.info_type === 'hours') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (reply === '3' || details.info_type === 'services') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (reply === '4' || details.info_type === 'packages') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n if (details.info_type === 'address') {\n const { store } = await fetchStoreInfo(state.storeId)\n responseMessage = `the address for ${state.storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (details.info_type === 'hours') {\n const { store } = await fetchStoreInfo(state.storeId)\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${state.storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (details.info_type === 'services') {\n const { services } = await fetchStoreInfo(state.storeId)\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${state.storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (details.info_type === 'packages') {\n const { packages } = await fetchStoreInfo(state.storeId)\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${state.storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n }\n } else {\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n if (servicesError) throw servicesError\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const { name: clientName, phoneNumber } = await extractNameAndNumber(reply)\n if (clientName && phoneNumber) {\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n conversationState.set(from, { intent: 'GREETING' })\n } else if (intent === 'BOOK_APPOINTMENT') {\n state.step = 'book_appointment'\n state.intent = 'BOOK_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'VIEW_APPOINTMENTS'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString())\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n state.step = 'change_appointment'\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n state.step = 'store_info'\n state.intent = 'STORE_INFO'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CONFIRM_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CANCEL_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n state.step = 'package_inquiry'\n state.intent = 'PACKAGE_INQUIRY'\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hi ${name} I’m here to help you can book an appointment change an appointment get store info or check your package details just let me know what you’d like`\n conversationState.set(from, { intent: 'UNKNOWN' })\n }\n }\n\n responseMessage = await rephraseWithDeepSeek(responseMessage, name, state.storeName)\n console.log('Sending response:', responseMessage)\n\n await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: req.body.From,\n body: responseMessage\n })\n\n res.status(200).send('Message processed')\n } catch (error) {\n console.error('Error processing message:', error.message)\n res.status(500).send('Error processing message')\n }\n})\n\nconst PORT = process.env.PORT || 3000\napp.listen(PORT, () => {\n console.log(`Server running on port ${PORT}`)\n})\n\nsetInterval(() => {\n sendReminders().catch(error => console.error('Error in scheduled reminders:', error.message))\n}, 24 * 60 * 60 * 1000)\n"}, "51be59fac6688046920b1661cd0e457f1fc8c35b": {"t": 1785767090.2478945, "n": 67872, "h": "f4e6cf84e74d5371", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name, storeName = null) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n let prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n `\n\n if (storeName) {\n prompt += `\n The message may reference a store. Ensure that any reference to a store uses the name \"${storeName}\" instead of any other store name like \"Glamour Salon\" unless explicitly intended.\n `\n }\n\n prompt += `\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n let rephrased = completion.choices[0].message.content.trim()\n rephrased = rephrased.replace(/\\[Name\\]/g, name)\n return rephrased\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay]\n \n if (!storeHours) {\n throw new Error(`Operating hours for ${apptDay} not found for store ${storeId}`)\n }\n\n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay]\n \n if (!storeHours) {\n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n continue\n }\n\n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n - If the current state has step 'selectOption' and the message contains keywords like \"address\", \"hours\", \"services\", or \"packages\", classify the intent as STORE_INFO and set the info_type accordingly.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests, \"packages\" for package-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'selectOption') {\n let infoType = null\n if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('hours') || lowerMessage.includes('opening') || lowerMessage.includes('operating')) infoType = 'hours'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n if (infoType) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: currentState.storeName || null,\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\nasync function extractNameAndNumber(message) {\n try {\n console.log('Extracting name and number from message:', message)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and extract the name and phone number. The message may contain a name and a phone number in various formats, such as:\n - \"Name Number\" (e.g., \"John 96460132\")\n - \"Number Name\" (e.g., \"96460132 John\")\n - \"Name, Number\" (e.g., \"John, 96460132\")\n - \"My name is Name and my number is Number\" (e.g., \"My name is John and my number is 96460132\")\n - \"Number is my number and I'm Name\" (e.g., \"96460132 is my number and I'm John\")\n \n The name will typically be a string of alphabetic characters (e.g., \"John\", \"Advika\"), and the phone number will be a sequence of digits, possibly starting with a country code like +65 (e.g., \"96460132\", \"+6596460132\").\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"name\": \"EXTRACTED_NAME or null\",\n \"phoneNumber\": \"EXTRACTED_PHONE_NUMBER or null\"\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in extracting customer names and phone numbers from natural language with a focus on flexible format recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 100,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Extracted name and number:', result)\n\n // Normalize phone number by removing country code if present\n if (result.phoneNumber) {\n result.phoneNumber = result.phoneNumber.replace(/^\\+65/, '')\n }\n\n return result\n } catch (error) {\n console.error('DeepSeek name and number extraction error:', error.message)\n return { name: null, phoneNumber: null }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n if (!clientData && req.body.ProfileName) {\n name = req.body.ProfileName\n }\n\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n const { intent, details } = await classifyIntent(reply, currentState)\n\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else if (!conversationState.has(from)) {\n currentState = { intent }\n }\n\n conversationState.set(from, currentState)\n let state = conversationState.get(from) || {}\n console.log('Current conversation state after intent classification:', state)\n\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1' || details.info_type === 'address') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (reply === '2' || details.info_type === 'hours') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (reply === '3' || details.info_type === 'services') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (reply === '4' || details.info_type === 'packages') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n if (details.info_type === 'address') {\n const { store } = await fetchStoreInfo(state.storeId)\n responseMessage = `the address for ${state.storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (details.info_type === 'hours') {\n const { store } = await fetchStoreInfo(state.storeId)\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${state.storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (details.info_type === 'services') {\n const { services } = await fetchStoreInfo(state.storeId)\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${state.storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (details.info_type === 'packages') {\n const { packages } = await fetchStoreInfo(state.storeId)\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${state.storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n }\n } else {\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n if (servicesError) throw servicesError\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const { name: clientName, phoneNumber } = await extractNameAndNumber(reply)\n if (clientName && phoneNumber) {\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n conversationState.set(from, { intent: 'GREETING' })\n } else if (intent === 'BOOK_APPOINTMENT') {\n state.step = 'book_appointment'\n state.intent = 'BOOK_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'VIEW_APPOINTMENTS'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString())\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n state.step = 'change_appointment'\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n state.step = 'store_info'\n state.intent = 'STORE_INFO'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CONFIRM_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CANCEL_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n state.step = 'package_inquiry'\n state.intent = 'PACKAGE_INQUIRY'\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hi ${name} I’m here to help you can book an appointment change an appointment get store info or check your package details just let me know what you’d like`\n conversationState.set(from, { intent: 'UNKNOWN' })\n }\n }\n\n responseMessage = await rephraseWithDeepSeek(responseMessage, name, state.storeName)\n console.log('Sending response:', responseMessage)\n\n await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: req.body.From,\n body: responseMessage\n })\n\n res.status(200).send('Message processed')\n } catch (error) {\n console.error('Error processing message:', error.message)\n res.status(500).send('Error processing message')\n }\n})\n\nconst PORT = process.env.PORT || 3000\napp.listen(PORT, () => {\n console.log(`Server running on port ${PORT}`)\n})\n\nsetInterval(() => {\n sendReminders().catch(error => console.error('Error in scheduled reminders:', error.message))\n}, 24 * 60 * 60 * 1000)\n"}, "a3ab78e7ef899d637f84045f8fa6c33f979ee568": {"t": 1785767094.8614457, "n": 65665, "h": "a56bc448f352a6c1", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name, storeName = null) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n let prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n `\n\n if (storeName) {\n prompt += `\n The message may reference a store. Ensure that any reference to a store uses the name \"${storeName}\" instead of any other store name like \"Glamour Salon\" unless explicitly intended.\n `\n }\n\n prompt += `\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n let rephrased = completion.choices[0].message.content.trim()\n rephrased = rephrased.replace(/\\[Name\\]/g, name)\n return rephrased\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay] // Fixed: Removed .toLowerCase() to match capitalized keys\n \n if (!storeHours) {\n throw new Error(`Operating hours for ${apptDay} not found for store ${storeId}`)\n }\n\n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay] // Fixed: Match capitalized keys\n \n if (!storeHours) {\n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n continue\n }\n\n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n - If the current state has step 'selectOption' and the message contains keywords like \"address\", \"hours\", \"services\", or \"packages\", classify the intent as STORE_INFO and set the info_type accordingly.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests, \"packages\" for package-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'selectOption') {\n let infoType = null\n if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('hours') || lowerMessage.includes('opening') || lowerMessage.includes('operating')) infoType = 'hours'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n if (infoType) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: currentState.storeName || null,\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n if (!clientData && req.body.ProfileName) {\n name = req.body.ProfileName\n }\n\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n const { intent, details } = await classifyIntent(reply, currentState)\n\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else if (!conversationState.has(from)) {\n currentState = { intent }\n }\n\n conversationState.set(from, currentState)\n let state = conversationState.get(from) || {}\n console.log('Current conversation state after intent classification:', state)\n\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1' || details.info_type === 'address') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (reply === '2' || details.info_type === 'hours') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (reply === '3' || details.info_type === 'services') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (reply === '4' || details.info_type === 'packages') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n if (details.info_type === 'address') {\n const { store } = await fetchStoreInfo(state.storeId)\n responseMessage = `the address for ${state.storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (details.info_type === 'hours') {\n const { store } = await fetchStoreInfo(state.storeId)\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${state.storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (details.info_type === 'services') {\n const { services } = await fetchStoreInfo(state.storeId)\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${state.storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (details.info_type === 'packages') {\n const { packages } = await fetchStoreInfo(state.storeId)\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${state.storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n }\n } else {\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n if (servicesError) throw servicesError\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id // Fixed: Ensure correct service_id from services table\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n conversationState.set(from, { intent: 'GREETING' })\n } else if (intent === 'BOOK_APPOINTMENT') {\n state.step = 'book_appointment'\n state.intent = 'BOOK_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'VIEW_APPOINTMENTS'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString())\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n state.step = 'change_appointment'\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n state.step = 'store_info'\n state.intent = 'STORE_INFO'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CONFIRM_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CANCEL_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n state.step = 'package_inquiry'\n state.intent = 'PACKAGE_INQUIRY'\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else if (intent === 'UNKNOWN') {\n responseMessage = `sorry ${name} I didn’t quite understand that can you try again or let me know how I can assist you`\n conversationState.delete(from)\n }\n }\n\n console.log('Sending response:', responseMessage)\n const twiml = new twilio.twiml.MessagingResponse()\n twiml.message(responseMessage)\n res.writeHead(200, { 'Content-Type': 'text/xml' })\n res.end(twiml.toString())\n } catch (error) {\n console.error('Error in webhook:', error.message)\n res.status(500).send('Internal Server Error')\n }\n})\n\nconst port = process.env.PORT || 3000\napp.listen(port, () => {\n console.log(`Server running on port ${port}`)\n})\n"}, "17a68c026f9d71b8dc0a1827f899b0157042c4a8": {"t": 1785767097.7034712, "n": 65860, "h": "9be339c93f58432d", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name, storeName = null) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n let prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n `\n\n // Add storeName to the prompt if provided to ensure correct store reference\n if (storeName) {\n prompt += `\n The message may reference a store. Ensure that any reference to a store uses the name \"${storeName}\" instead of any other store name like \"Glamour Salon\" unless explicitly intended.\n `\n }\n\n prompt += `\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n let rephrased = completion.choices[0].message.content.trim()\n // Ensure the name placeholder is correctly replaced\n rephrased = rephrased.replace(/\\[Name\\]/g, name)\n return rephrased\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n - If the current state has step 'selectOption' and the message contains keywords like \"address\", \"hours\", \"services\", or \"packages\", classify the intent as STORE_INFO and set the info_type accordingly.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests, \"packages\" for package-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n // Fallback logic with context\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'selectOption') {\n let infoType = null\n if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('hours') || lowerMessage.includes('opening') || lowerMessage.includes('operating')) infoType = 'hours'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n if (infoType) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: currentState.storeName || null,\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n // Use ProfileName from Twilio if available and clientData is not set\n if (!clientData && req.body.ProfileName) {\n name = req.body.ProfileName\n }\n\n // Get current state before intent classification\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n // Classify intent with current state context\n const { intent, details } = await classifyIntent(reply, currentState)\n\n // Reset state only if intent changes to a new top-level intent\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else if (!conversationState.has(from)) {\n currentState = { intent }\n }\n\n // Always store the current state in conversationState to ensure consistency\n conversationState.set(from, currentState)\n let state = conversationState.get(from) || {}\n console.log('Current conversation state after intent classification:', state)\n\n // Handle responses based on the current step in the conversation\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1' || details.info_type === 'address') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (reply === '2' || details.info_type === 'hours') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (reply === '3' || details.info_type === 'services') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (reply === '4' || details.info_type === 'packages') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n if (details.info_type === 'address') {\n const { store } = await fetchStoreInfo(state.storeId)\n responseMessage = `the address for ${state.storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (details.info_type === 'hours') {\n const { store } = await fetchStoreInfo(state.storeId)\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${state.storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (details.info_type === 'services') {\n const { services } = await fetchStoreInfo(state.storeId)\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${state.storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (details.info_type === 'packages') {\n const { packages } = await fetchStoreInfo(state.storeId)\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${state.storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n }\n } else {\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n } else {\n // Handle new intents when no specific step is set\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n conversationState.set(from, { intent: 'GREETING' })\n } else if (intent === 'BOOK_APPOINTMENT') {\n state.step = 'book_appointment'\n state.intent = 'BOOK_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'VIEW_APPOINTMENTS'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n state.step = 'change_appointment'\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n state.step = 'store_info'\n state.intent = 'STORE_INFO'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CONFIRM_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CANCEL_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n state.step = 'package_inquiry'\n state.intent = 'PACKAGE_INQUIRY'\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else if (intent === 'UNKNOWN') {\n responseMessage = `sorry ${name} I didn’t quite understand that can you try again or let me know how I can assist you`\n conversationState.delete(from)\n }\n }\n\n console.log('Sending response:', responseMessage)\n const twiml = new twilio.twiml.MessagingResponse()\n twiml.message(responseMessage)\n res.writeHead(200, { 'Content-Type': 'text/xml' })\n res.end(twiml.toString())\n } catch (error) {\n console.error('Error in webhook:', error.message)\n res.status(500).send('Internal Server Error')\n }\n})\n\nconst port = process.env.PORT || 3000\napp.listen(port, () => {\n console.log(`Server running on port ${port}`)\n})\n"}, "3525b50c6eb4144d033e288e80d3eea8cab57ef5": {"t": 1785767101.2885404, "n": 64744, "h": "94beccc8630e243d", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name, storeName = null) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n let prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n `\n\n // Add storeName to the prompt if provided to ensure correct store reference\n if (storeName) {\n prompt += `\n The message may reference a store. Ensure that any reference to a store uses the name \"${storeName}\" instead of any other store name like \"Glamour Salon\" unless explicitly intended.\n `\n }\n\n prompt += `\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n let rephrased = completion.choices[0].message.content.trim()\n // Ensure the name placeholder is correctly replaced\n rephrased = rephrased.replace(/\\[Name\\]/g, name)\n return rephrased\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n - If the current state has step 'selectOption' and the message contains keywords like \"address\", \"hours\", \"services\", or \"packages\", classify the intent as STORE_INFO and set the info_type accordingly.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests, \"packages\" for package-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n // Fallback logic with context\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'selectOption') {\n let infoType = null\n if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('hours') || lowerMessage.includes('opening') || lowerMessage.includes('operating')) infoType = 'hours'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n if (infoType) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: currentState.storeName || null,\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n // Use ProfileName from Twilio if available and clientData is not set\n if (!clientData && req.body.ProfileName) {\n name = req.body.ProfileName\n }\n\n // Get current state before intent classification\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n // Classify intent with current state context\n const { intent, details } = await classifyIntent(reply, currentState)\n\n // Reset state only if intent changes to a new top-level intent\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else if (!conversationState.has(from)) {\n currentState = { intent }\n }\n\n // Always store the current state in conversationState to ensure consistency\n conversationState.set(from, currentState)\n let state = conversationState.get(from) || {}\n console.log('Current conversation state after intent classification:', state)\n\n // Handle responses based on the current step in the conversation\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1' || details.info_type === 'address') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (reply === '2' || details.info_type === 'hours') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (reply === '3' || details.info_type === 'services') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (reply === '4' || details.info_type === 'packages') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n if (details.info_type === 'address') {\n const { store } = await fetchStoreInfo(state.storeId)\n responseMessage = `the address for ${state.storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (details.info_type === 'hours') {\n const { store } = await fetchStoreInfo(state.storeId)\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${state.storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (details.info_type === 'services') {\n const { services } = await fetchStoreInfo(state.storeId)\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${state.storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (details.info_type === 'packages') {\n const { packages } = await fetchStoreInfo(state.storeId)\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${state.storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n }\n } else {\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n } else {\n // Handle new intents when no specific step is set\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n conversationState.set(from, { intent: 'GREETING' })\n } else if (intent === 'BOOK_APPOINTMENT') {\n state.step = 'book_appointment'\n state.intent = 'BOOK_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'VIEW_APPOINTMENTS'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n state.step = 'change_appointment'\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n state.step = 'store_info'\n state.intent = 'STORE_INFO'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CONFIRM_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CANCEL_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()}\n"}, "efd8ccb8201a82206dceb3fa7d61e56535cc7f20": {"t": 1785767102.9392245, "n": 64200, "h": "9ad598c6eab9a4b4", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name, storeName = null) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n let prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n `\n\n // Add storeName to the prompt if provided to ensure correct store reference\n if (storeName) {\n prompt += `\n The message may reference a store. Ensure that any reference to a store uses the name \"${storeName}\" instead of any other store name like \"Glamour Salon\" unless explicitly intended.\n `\n }\n\n prompt += `\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n let rephrased = completion.choices[0].message.content.trim()\n // Ensure the name placeholder is correctly replaced\n rephrased = rephrased.replace(/\\[Name\\]/g, name)\n return rephrased\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n - If the current state has step 'selectOption' and the message contains keywords like \"address\", \"hours\", \"services\", or \"packages\", classify the intent as STORE_INFO and set the info_type accordingly.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests, \"packages\" for package-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n // Fallback logic with context\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'selectOption') {\n let infoType = null\n if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('hours') || lowerMessage.includes('opening') || lowerMessage.includes('operating')) infoType = 'hours'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n if (infoType) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: currentState.storeName || null,\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n // Use ProfileName from Twilio if available and clientData is not set\n if (!clientData && req.body.ProfileName) {\n name = req.body.ProfileName\n }\n\n // Get current state before intent classification\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n // Classify intent with current state context\n const { intent, details } = await classifyIntent(reply, currentState)\n\n // Reset state only if intent changes to a new top-level intent\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else if (!conversationState.has(from)) {\n currentState = { intent }\n }\n\n // Always store the current state in conversationState to ensure consistency\n conversationState.set(from, currentState)\n let state = conversationState.get(from) || {}\n console.log('Current conversation state after intent classification:', state)\n\n // Handle responses based on the current step in the conversation\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1' || details.info_type === 'address') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (reply === '2' || details.info_type === 'hours') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (reply === '3' || details.info_type === 'services') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (reply === '4' || details.info_type === 'packages') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n if (details.info_type === 'address') {\n const { store } = await fetchStoreInfo(state.storeId)\n responseMessage = `the address for ${state.storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (details.info_type === 'hours') {\n const { store } = await fetchStoreInfo(state.storeId)\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${state.storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (details.info_type === 'services') {\n const { services } = await fetchStoreInfo(state.storeId)\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${state.storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (details.info_type === 'packages') {\n const { packages } = await fetchStoreInfo(state.storeId)\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${state.storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n }\n } else {\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n } else {\n // Handle new intents when no specific step is set\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n conversationState.set(from, { intent: 'GREETING' })\n } else if (intent === 'BOOK_APPOINTMENT') {\n state.step = 'book_appointment'\n state.intent = 'BOOK_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'VIEW_APPOINTMENTS'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n state.step = 'change_appointment'\n state.intent = 'CHANGE_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n state.step = 'store_info'\n state.intent = 'STORE_INFO'\n conversationState.set(from, state)\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CONFIRM_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n conversationState.delete(from)\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.intent = 'CANCEL_APPOINTMENT'\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (appt\n"}, "eaec5bdcdbec7c721490b904f488c3692307c2d9": {"t": 1785767104.4419625, "n": 66870, "h": "b8d66fb51d24b99c", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name, storeName = null) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n let prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n `\n\n // Add storeName to the prompt if provided to ensure correct store reference\n if (storeName) {\n prompt += `\n The message may reference a store. Ensure that any reference to a store uses the name \"${storeName}\" instead of any other store name like \"Glamour Salon\" unless explicitly intended.\n `\n }\n\n prompt += `\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n let rephrased = completion.choices[0].message.content.trim()\n // Ensure the name placeholder is correctly replaced\n rephrased = rephrased.replace(/\\[Name\\]/g, name)\n return rephrased\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n - If the current state has step 'selectOption' and the message contains keywords like \"address\", \"hours\", \"services\", or \"packages\", classify the intent as STORE_INFO and set the info_type accordingly.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests, \"packages\" for package-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n // Fallback logic with context\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'selectOption') {\n let infoType = null\n if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('hours') || lowerMessage.includes('opening') || lowerMessage.includes('operating')) infoType = 'hours'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n if (infoType) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: currentState.storeName || null,\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n // Use ProfileName from Twilio if available and clientData is not set\n if (!clientData && req.body.ProfileName) {\n name = req.body.ProfileName\n }\n\n // Get current state before intent classification\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n // Classify intent with current state context\n const { intent, details } = await classifyIntent(reply, currentState)\n\n // Reset state only if intent changes to a new top-level intent\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else {\n currentState = { intent }\n }\n\n // Always store the current state in conversationState to ensure consistency\n conversationState.set(from, currentState)\n let state = conversationState.get(from) || {}\n\n if (conversationState.has(from)) {\n state = conversationState.get(from)\n console.log('Current conversation state:', state)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1' || details.info_type === 'address') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (reply === '2' || details.info_type === 'hours') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (reply === '3' || details.info_type === 'services') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (reply === '4' || details.info_type === 'packages') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n // Debug: Fetch all stores to see what's in the database\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n // Debug: Fetch all stores to see what's in the database\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n // If the user already specified an info_type (e.g., \"services\"), use it directly\n if (details.info_type === 'address') {\n const { store } = await fetchStoreInfo(state.storeId)\n responseMessage = `the address for ${state.storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (details.info_type === 'hours') {\n const { store } = await fetchStoreInfo(state.storeId)\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${state.storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (details.info_type === 'services') {\n const { services } = await fetchStoreInfo(state.storeId)\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${state.storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (details.info_type === 'packages') {\n const { packages } = await fetchStoreInfo(state.storeId)\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${state.storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment', intent: 'BOOK_APPOINTMENT' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'VIEW_APPOINTMENTS' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment', intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info', intent: 'STORE_INFO' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', intent: 'PACKAGE_INQUIRY', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name, state?.storeName || null)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\nconsole.log('Server setup complete, listening for requests...')\n"}, "870bdcbfe10ebc2b809ec7feee34b4d8f0f5e63c": {"t": 1785767106.0425482, "n": 66698, "h": "7d4b20790e89f9d1", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name, storeName = null) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n let prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n `\n\n // Add storeName to the prompt if provided to ensure correct store reference\n if (storeName) {\n prompt += `\n The message may reference a store. Ensure that any reference to a store uses the name \"${storeName}\" instead of any other store name like \"Glamour Salon\" unless explicitly intended.\n `\n }\n\n prompt += `\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n let rephrased = completion.choices[0].message.content.trim()\n // Ensure the name placeholder is correctly replaced\n rephrased = rephrased.replace(/\\[Name\\]/g, name)\n return rephrased\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n - If the current state has step 'selectOption' and the message contains keywords like \"address\", \"hours\", \"services\", or \"packages\", classify the intent as STORE_INFO and set the info_type accordingly.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests, \"packages\" for package-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n // Fallback logic with context\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'selectOption') {\n let infoType = null\n if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('hours') || lowerMessage.includes('opening') || lowerMessage.includes('operating')) infoType = 'hours'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n if (infoType) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: currentState.storeName || null,\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n else if (lowerMessage.includes('packages') || lowerMessage.includes('package')) infoType = 'packages'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n // Use ProfileName from Twilio if available and clientData is not set\n if (!clientData && req.body.ProfileName) {\n name = req.body.ProfileName\n }\n\n // Get current state before intent classification\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n // Classify intent with current state context\n const { intent, details } = await classifyIntent(reply, currentState)\n\n // Reset state only if intent changes to a new top-level intent\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else {\n currentState = { intent }\n }\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n console.log('Current conversation state:', state)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1' || details.info_type === 'address') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (reply === '2' || details.info_type === 'hours') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (reply === '3' || details.info_type === 'services') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (reply === '4' || details.info_type === 'packages') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n // Debug: Fetch all stores to see what's in the database\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n // Debug: Fetch all stores to see what's in the database\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n // If the user already specified an info_type (e.g., \"services\"), use it directly\n if (details.info_type === 'address') {\n const { store } = await fetchStoreInfo(state.storeId)\n responseMessage = `the address for ${state.storeName} is ${store.store_addressL1}`\n conversationState.delete(from)\n } else if (details.info_type === 'hours') {\n const { store } = await fetchStoreInfo(state.storeId)\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${state.storeName}\\n${formattedHours}`\n conversationState.delete(from)\n } else if (details.info_type === 'services') {\n const { services } = await fetchStoreInfo(state.storeId)\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${state.storeName}\\n${serviceList}`\n conversationState.delete(from)\n } else if (details.info_type === 'packages') {\n const { packages } = await fetchStoreInfo(state.storeId)\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${state.storeName}\\n${packageList}`\n conversationState.delete(from)\n } else {\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment', intent: 'BOOK_APPOINTMENT' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'VIEW_APPOINTMENTS' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment', intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info', intent: 'STORE_INFO' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', intent: 'PACKAGE_INQUIRY', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name, state?.storeName || null)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\nconsole.log('Server setup complete, listening for requests...')\n"}, "d9dd760ab188609b89351307dbed024d8d2d4e79": {"t": 1785767108.199512, "n": 62466, "h": "3069da43ed9256cb", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n return completion.choices[0].message.content.trim()\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n // Fallback logic with context\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n\n // Get current state before intent classification\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n // Classify intent with current state context\n const { intent, details } = await classifyIntent(reply, currentState)\n\n // Reset state only if intent changes to a new top-level intent\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else {\n currentState = { intent }\n }\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n console.log('Current conversation state:', state)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n } else if (reply === '2') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n conversationState.delete(from)\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n // Debug: Fetch all stores to see what's in the database\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n // Debug: Fetch all stores to see what's in the database\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment', intent: 'BOOK_APPOINTMENT' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'VIEW_APPOINTMENTS' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment', intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info', intent: 'STORE_INFO' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', intent: 'PACKAGE_INQUIRY', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\nconsole.log('Server setup complete, listening for requests...')\n"}, "d66388c73bf05d273b21456b1a565c7d893705fb": {"t": 1785767109.7649276, "n": 62395, "h": "ee7e67503338c203", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id\n store_id\n assignment_id\n client_id\n service_id\n appt_start\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name store_operating_hours store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_description price service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id package_name package_description package_limit package_type num_sessions num_credits package_price services_linked package_start package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}: ${error.message}`)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n return completion.choices[0].message.content.trim()\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message, currentState = {}) {\n try {\n console.log('Classifying intent for message:', message, 'with current state:', currentState)\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent, considering the current conversation state: ${JSON.stringify(currentState)}. The possible intents are:\n - GREETING (e.g., \"hello\", \"hi\", \"hey there\", \"good morning\")\n - BOOK_APPOINTMENT (e.g., \"i need to book an appointment\", \"i want a haircut\", \"i'd like to schedule a service\", \"can i book a manicure\", \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g., \"i want to change my booking\", \"can i reschedule\", \"move my appointment\", \"change my time slot\")\n - STORE_INFO (e.g., \"can i get more details about a salon\", \"what are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\", \"what do you offer\", \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g., \"can i see my upcoming appointments\", \"what are my bookings\", \"show my appointments\", \"what's my schedule\", \"do i have any appointments\", \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\", \"yes\", \"confirm\", \"i confirm\")\n - CANCEL_APPOINTMENT (e.g., \"NO\", \"no\", \"cancel\", \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g., \"can you tell me about my package\", \"is my package still active\", \"how many sessions are left\", \"package details\", \"what's my package status\")\n - REGISTER_CONFIRM (e.g., \"YES\", \"yes\", \"sure\", \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g., \"NO\", \"no\", \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n **Contextual Rules**:\n - If the current state has step 'book_appointment' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as BOOK_APPOINTMENT and extract the store_name.\n - If the current state has step 'store_info' and the message is a store name (e.g., \"Idle\", \"Glamour Salon\"), classify the intent as STORE_INFO and extract the store_name.\n - If the current state has step 'book_appointment_service' and the message contains a service name (e.g., \"haircut\", \"manicure\"), classify the intent as BOOK_APPOINTMENT and extract the service_name.\n - If the current state has step 'book_appointment_datetime' and the message contains a date or time (e.g., \"tomorrow at 2 PM\"), classify the intent as BOOK_APPOINTMENT and extract the date and time.\n\n Additionally extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\", \"Idle salon\")\n - service_name (e.g., \"haircut\", \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n\n // Fallback logic with context\n if (currentState.step === 'book_appointment' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'store_info' && (lowerMessage.includes('idle') || lowerMessage.includes('glamour'))) {\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : 'Glamour Salon',\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_service' && (lowerMessage.includes('haircut') || lowerMessage.includes('manicure') || lowerMessage.includes('cut') || lowerMessage.includes('nails'))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: lowerMessage.includes('haircut') || lowerMessage.includes('cut') ? 'haircut' : 'manicure',\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (currentState.step === 'book_appointment_datetime' && (lowerMessage.includes('tomorrow') || lowerMessage.includes('friday') || lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i))) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: lowerMessage.includes('tomorrow') ? 'tomorrow' : (lowerMessage.includes('friday') ? 'Friday' : null),\n time: lowerMessage.match(/\\d{1,2}(?::\\d{2})?\\s*(am|pm)/i)?.[0] || null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name global_client_id client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n\n // Get current state before intent classification\n let currentState = conversationState.get(from) || {}\n console.log('Current state before intent classification:', currentState)\n\n // Classify intent with current state context\n const { intent, details } = await classifyIntent(reply, currentState)\n\n // Reset state only if intent changes to a new top-level intent\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n if (currentState.intent !== intent) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n currentState = { intent }\n } else {\n console.log('No state reset needed; continuing with same intent:', intent)\n }\n } else {\n currentState = { intent }\n }\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n console.log('Current conversation state:', state)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n } else if (reply === '2') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n conversationState.delete(from)\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in book_appointment:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in book_appointment:', storeError?.message || 'No store found')\n console.log('Store not found in database in book_appointment:', details.store_name)\n // Debug: Fetch all stores to see what's in the database\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in book_appointment:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message || 'No store found')\n console.log('Store not found in database in store_info:', details.store_name)\n // Debug: Fetch all stores to see what's in the database\n const { data: allStores, error: allStoresError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n console.log('All stores in database:', allStores, 'Error:', allStoresError?.message)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id\n store_id\n package_id\n global_client_id\n client_id\n session_remaining\n credits_remaining\n purchase_date\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name\n package_description\n num_sessions\n num_credits\n package_price\n package_start\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment', intent: 'BOOK_APPOINTMENT' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'VIEW_APPOINTMENTS' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment', intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info', intent: 'STORE_INFO' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', intent: 'PACKAGE_INQUIRY', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\nconsole.log('Server setup complete, listening for requests...')\n"}, "9470ad974bcb17d060dc4e9e4475fa8c3a89f7af": {"t": 1785767111.9096363, "n": 58092, "h": "762dab5688b4e64e", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id\n store_id\n assignment_id\n client_id\n service_id\n appt_start\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name store_operating_hours store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_description price service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id package_name package_description package_limit package_type num_sessions num_credits package_price services_linked package_start package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n return completion.choices[0].message.content.trim()\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message) {\n try {\n console.log('Classifying intent for message:', message)\n const prompt = `\n You are a helpful assistant for Glamour Salon analyze the customer's message and classify their intent the possible intents are\n - GREETING (e.g. \"hello\" \"hi\" \"hey there\" \"good morning\")\n - BOOK_APPOINTMENT (e.g. \"i need to book an appointment\" \"i want a haircut\" \"i'd like to schedule a service\" \"can i book a manicure\" \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g. \"i want to change my booking\" \"can i reschedule\" \"move my appointment\" \"change my time slot\")\n - STORE_INFO (e.g. \"can i get more details about a salon\" \"what are your hours\" \"operating hours for Idle\" \"address of Glamour Salon\" \"services at Idle\" \"what do you offer\" \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g. \"can i see my upcoming appointments\" \"what are my bookings\" \"show my appointments\" \"what's my schedule\" \"do i have any appointments\" \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g. \"YES\" \"yes\" \"confirm\" \"i confirm\")\n - CANCEL_APPOINTMENT (e.g. \"NO\" \"no\" \"cancel\" \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g. \"can you tell me about my package\" \"is my package still active\" \"how many sessions are left\" \"package details\" \"what's my package status\")\n - REGISTER_CONFIRM (e.g. \"YES\" \"yes\" \"sure\" \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g. \"NO\" \"no\" \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally extract any relevant details such as\n - store_name (e.g. \"Idle\" \"Glamour Salon\" \"Idle salon\")\n - service_name (e.g. \"haircut\" \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g. \"26/05/2025\" \"tomorrow\" \"next Friday\" \"this Friday\")\n - time (e.g. \"14:00\" \"2 PM\" \"12:34 AM\" \"noon\" \"10 am\")\n - info_type (for STORE_INFO intent \"address\" for address-related requests \"hours\" for operating hours or schedule requests \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g. \"haircut maybe\" should be \"haircut\") or informal terms (e.g. \"cut\" should be interpreted as \"haircut\" \"nails\" as \"manicure\") if unsure prioritize the most likely service based on context\n\n Customer message: \"${message}\"\n\n Respond in JSON format\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name global_client_id client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n\n const { intent, details } = await classifyIntent(reply)\n\n // Reset state if intent changes to a new top-level intent\n console.log('Current state before reset:', conversationState.get(from))\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n }\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n console.log('Current conversation state:', state)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n } else if (reply === '2') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n conversationState.delete(from)\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error:', storeError?.message)\n console.log('Store not found in database:', details.store_name)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message)\n console.log('Store not found in database in store_info:', details.store_name)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id\n store_id\n package_id\n global_client_id\n client_id\n session_remaining\n credits_remaining\n purchase_date\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name\n package_description\n num_sessions\n num_credits\n package_price\n package_start\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'VIEW_APPOINTMENTS' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\nconsole.log('Server setup complete, listening for requests...')\n"}, "fc963e3cc7803f815e21d241aae11a6b5748ed0e": {"t": 1785767114.221286, "n": 6944, "h": "21015e0a660b341d", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id\n store_id\n assignment_id\n client_id\n service_id\n appt_start\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name store_operating_hours store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_description price service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id package_name package_description package_limit package_type num_sessions num_credits package_price services_linked package_start package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`,\n"}, "8354dbd36e6af995d4783ce52d55b7303e5f4780": {"t": 1785767116.681871, "n": 58092, "h": "762dab5688b4e64e", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id\n store_id\n assignment_id\n client_id\n service_id\n appt_start\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name store_operating_hours store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_description price service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id package_name package_description package_limit package_type num_sessions num_credits package_price services_linked package_start package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n return completion.choices[0].message.content.trim()\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message) {\n try {\n console.log('Classifying intent for message:', message)\n const prompt = `\n You are a helpful assistant for Glamour Salon analyze the customer's message and classify their intent the possible intents are\n - GREETING (e.g. \"hello\" \"hi\" \"hey there\" \"good morning\")\n - BOOK_APPOINTMENT (e.g. \"i need to book an appointment\" \"i want a haircut\" \"i'd like to schedule a service\" \"can i book a manicure\" \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g. \"i want to change my booking\" \"can i reschedule\" \"move my appointment\" \"change my time slot\")\n - STORE_INFO (e.g. \"can i get more details about a salon\" \"what are your hours\" \"operating hours for Idle\" \"address of Glamour Salon\" \"services at Idle\" \"what do you offer\" \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g. \"can i see my upcoming appointments\" \"what are my bookings\" \"show my appointments\" \"what's my schedule\" \"do i have any appointments\" \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g. \"YES\" \"yes\" \"confirm\" \"i confirm\")\n - CANCEL_APPOINTMENT (e.g. \"NO\" \"no\" \"cancel\" \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g. \"can you tell me about my package\" \"is my package still active\" \"how many sessions are left\" \"package details\" \"what's my package status\")\n - REGISTER_CONFIRM (e.g. \"YES\" \"yes\" \"sure\" \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g. \"NO\" \"no\" \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally extract any relevant details such as\n - store_name (e.g. \"Idle\" \"Glamour Salon\" \"Idle salon\")\n - service_name (e.g. \"haircut\" \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g. \"26/05/2025\" \"tomorrow\" \"next Friday\" \"this Friday\")\n - time (e.g. \"14:00\" \"2 PM\" \"12:34 AM\" \"noon\" \"10 am\")\n - info_type (for STORE_INFO intent \"address\" for address-related requests \"hours\" for operating hours or schedule requests \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g. \"haircut maybe\" should be \"haircut\") or informal terms (e.g. \"cut\" should be interpreted as \"haircut\" \"nails\" as \"manicure\") if unsure prioritize the most likely service based on context\n\n Customer message: \"${message}\"\n\n Respond in JSON format\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name global_client_id client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n\n const { intent, details } = await classifyIntent(reply)\n\n // Reset state if intent changes to a new top-level intent\n console.log('Current state before reset:', conversationState.get(from))\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n }\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n console.log('Current conversation state:', state)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n } else if (reply === '2') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n conversationState.delete(from)\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error:', storeError?.message)\n console.log('Store not found in database:', details.store_name)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message)\n console.log('Store not found in database in store_info:', details.store_name)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id\n store_id\n package_id\n global_client_id\n client_id\n session_remaining\n credits_remaining\n purchase_date\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name\n package_description\n num_sessions\n num_credits\n package_price\n package_start\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'VIEW_APPOINTMENTS' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\nconsole.log('Server setup complete, listening for requests...')\n"}, "a1ea017158a9da028b3d7d3fc08e44ce025a8fa8": {"t": 1785767118.3287416, "n": 46717, "h": "981e6ffa51d45f1c", "c": "require('dotenv').config();\nconst express = require('express');\nconst twilio = require('twilio');\nconst { createClient } = require('@supabase/supabase-js');\nconst { OpenAI } = require('openai');\nconst chrono = require('chrono-node'); // Kept as per your request\nconst app = express();\n\napp.use(express.urlencoded({ extended: false }));\napp.use(express.json());\n\n// Initialize Twilio client\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing. Check your .env file.');\n process.exit(1);\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID);\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN);\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER);\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);\n\n// Initialize Supabase client\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing. Check your .env file.');\n process.exit(1);\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY);\n\n// Initialize DeepSeek client (using OpenAI SDK)\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing. Check your .env file.');\n process.exit(1);\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n});\n\n// In-memory store to track conversation state\nconst conversationState = new Map();\n\n// Fetch appointments from Supabase\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `);\n if (error) throw error;\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single();\n if (clientError) throw clientError;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (globalError) throw globalError;\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph };\n }));\n return appointments;\n } catch (error) {\n console.error('Error fetching appointments:', error.message);\n throw error;\n }\n};\n\n// Fetch store information (updated to include packages)\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single();\n if (storeError) throw storeError;\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId);\n if (servicesError) throw servicesError;\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId);\n if (packagesError) throw packagesError;\n\n return { store, services, packages };\n } catch (error) {\n console.error('Error fetching store info:', error.message);\n throw error;\n }\n};\n\n// Create client record\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: phoneNumber.slice(3), // Remove +65\n })\n .select('global_client_id')\n .single();\n if (globalError) throw globalError;\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n })\n .select('client_id')\n .single();\n if (clientError) throw clientError;\n\n return client.client_id;\n } catch (error) {\n console.error('Error creating client record:', error.message);\n throw error;\n }\n};\n\n// Send appointment reminders (1 day before appt_start)\nconst sendReminders = async () => {\n try {\n const today = new Date();\n today.setHours(0, 0, 0, 0); // Reset to start of day\n const appointments = await fetchAppointments();\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start);\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24));\n if (diffDays === 1) { // 1 day before\n try {\n const name = appt.preferred_name || appt.client_name;\n const message = `Hi ${name}, this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist. Reply YES to confirm, NO to cancel, or CHANGE to reschedule.`;\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n });\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`);\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message);\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message);\n throw error;\n }\n};\n// Add this before the POST handler\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query);\n res.status(200).send('Webhook endpoint is active. Use POST for WhatsApp messages.');\n});\n// Controller for booking appointments\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body;\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00');\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000); // Assuming 1-hour duration for simplicity\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select();\n if (error) throw error;\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] });\n } catch (error) {\n res.status(400).json({ error: error.message });\n }\n});\n\n// Function to rephrase messages using DeepSeek\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `Hi ${name}! It seems I don’t have enough details to assist you yet. Please let me know what you'd like to do, and I’ll be happy to help!`;\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon. Rephrase the following message to sound warm, natural, and human-like, while addressing the customer by their name (${name}). Keep the tone polite and conversational, as if you're speaking directly to the customer. Do not change the core meaning of the message. Do not include any explanatory notes or brackets.\n\n Message: \"${message}\"\n\n Rephrased message:\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in warm and friendly customer communication.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n });\n\n return completion.choices[0].message.content.trim();\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message);\n return message || `Hi ${name}! I’m having trouble processing your request. Please try again or let me know what you need!`;\n }\n}\n\n// Function to parse natural language date and time\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date('2025-05-29T02:32:00+08:00'); // Updated to current date and time\n let dateTime;\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`;\n // Use chrono-node with English locale only to avoid Japanese locale issues\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Fallback to basic parsing if chrono fails\n const today = now.toISOString().split('T')[0];\n dateTime = new Date(`${today}T${timeStr}:00+08:00`);\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`);\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Handle relative dates\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1));\n else if (dateStr.toLowerCase().includes('today')) dateTime = now;\n else dateTime = new Date(dateStr);\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0); // Default to midnight if no time\n }\n } else if (timeStr) {\n dateTime = new Date(now);\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i);\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10);\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0;\n const period = timeParts[3]?.toLowerCase();\n if (period === 'pm' && hours < 12) hours += 12;\n else if (period === 'am' && hours === 12) hours = 0;\n dateTime.setHours(hours, minutes, 0, 0);\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n // Ensure timezone is +08:00\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480);\n return dateTime;\n }\n return null;\n}\n\n// Function to check if the time slot is available and within operating hours\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5; // Hardcoded max appointments per slot\n const { store } = await fetchStoreInfo(storeId);\n const operatingHours = store.store_operating_hours;\n \n // Get the day of the week for the appointment\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n const apptDay = daysOfWeek[apptStart.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n // Check if the store is closed\n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Parse store opening and closing times\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n const storeOpenTime = new Date(apptStart);\n storeOpenTime.setHours(openHour, openMinute, 0, 0);\n const storeCloseTime = new Date(apptStart);\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Check if the appointment is within operating hours\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Check for overlapping appointments\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString());\n \n if (error) throw error;\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n return { isAvailable: true };\n}\n\n// Function to suggest the next available time slot\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n let suggestedTime = new Date(requestedTime);\n let attempts = 0;\n const maxAttempts = 7; // Try up to 7 days ahead\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n // Start from opening time or requested time if later\n let nextTime = new Date(suggestedTime);\n nextTime.setHours(openHour, openMinute, 0, 0);\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0);\n }\n\n const closeTime = new Date(suggestedTime);\n closeTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Increment by 30 minutes until a slot is found or we pass closing time\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000);\n if (apptEnd <= closeTime) {\n // Check if this slot is available (mock check since we can't call async here)\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n };\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30);\n }\n }\n \n // Move to the next day\n suggestedTime.setDate(suggestedTime.getDate() + 1);\n suggestedTime.setHours(0, 0, 0, 0); // Reset to start of day\n attempts++;\n }\n\n // If no slot is found within 7 days, suggest contacting support\n return null;\n}\n\n// Function to classify intent and extract details using DeepSeek\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent. The possible intents are:\n - GREETING (e.g., \"Hello\", \"Hi\")\n - BOOK_APPOINTMENT (e.g., \"I need to book an appointment\", \"I want a haircut\", \"I'd like to schedule a service\")\n - CHANGE_APPOINTMENT (e.g., \"I want to change my booking\", \"Can I reschedule\")\n - STORE_INFO (e.g., \"Can I get more details about a salon\", \"What are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\")\n - CANCEL_APPOINTMENT (e.g., \"NO\")\n - PACKAGE_INQUIRY (e.g., \"Can you tell me about my package?\", \"Is my package still active?\", \"How many sessions are left?\", \"Package details\")\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally, extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\")\n - service_name (e.g., \"haircut\", \"manicure\", or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"Haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n });\n\n let responseContent = completion.choices[0].message.content.trim();\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '');\n }\n\n return JSON.parse(responseContent);\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message);\n // Fallback to keyword-based intent classification\n const lowerMessage = message.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services')) {\n let infoType = null;\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours';\n else if (lowerMessage.includes('address')) infoType = 'address';\n else if (lowerMessage.includes('services')) infoType = 'services';\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n };\n } else if (lowerMessage.toUpperCase() === 'YES') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.toUpperCase() === 'NO') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n}\n\n// Webhook to handle customer replies\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body); // Log the incoming request\n const reply = req.body.Body;\n const from = req.body.From.replace('whatsapp:', '');\n console.log(`Processing message from ${from}: ${reply}`);\n try {\n // Hardcode remove +65 from incoming number for client_ph query\n let fromWithoutPrefix = from;\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3); // Remove +65 to get 96460132\n }\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single();\n if (clientError || !clientData) {\n console.log(`No matching client data for ${fromWithoutPrefix}`);\n res.status(200).send('Customer not found.');\n return;\n }\n\n const { data: clientLink } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (!clientLink) {\n console.log(`No client link found for global_client_id ${clientData.global_client_id}`);\n res.status(200).send('No client link found.');\n return;\n }\n\n let responseMessage;\n const name = clientData.preferred_name || clientData.client_name;\n\n // Classify the customer's intent using DeepSeek\n const { intent, details } = await classifyIntent(reply);\n\n // Check if we're in a conversation state\n if (conversationState.has(from)) {\n const state = conversationState.get(from);\n if (state.step === 'selectOption') {\n // Handle store info sub-menu\n const { storeId, storeName } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n\n if (reply === '1') {\n // Store Address\n responseMessage = `Address for ${storeName}: ${store.store_addressL1}`;\n } else if (reply === '2') {\n // Operating Hours\n const hours = store.store_operating_hours;\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${info.close}`;\n })\n .join('\\n');\n responseMessage = `Operating Hours for ${storeName}:\\n${formattedHours}`;\n } else if (reply === '3') {\n // Services\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeName}:\\n${serviceList}`;\n } else if (reply === '4') {\n // Packages\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate} - ${endDate}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage = `Packages at ${storeName}:\\n${packageList}`;\n } else {\n responseMessage = `Please select an option:\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n conversationState.delete(from); // Reset state\n } else if (state.step === 'book_appointment') {\n // Handle booking flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'book_appointment_service';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That's wonderful—I've found your appointment slot at ${state.storeName}. What service would you love to book today? Maybe a fresh haircut, a relaxing manicure, or something else? Let me know, and I’ll get it all set up for you!`;\n }\n } else {\n responseMessage = `Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'book_appointment_service') {\n // Handle booking flow - Service selection\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId);\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase());\n if (!serviceMatch && details.service_name) {\n // Try to match informal terms or partial names\n const lowerServiceName = details.service_name.toLowerCase();\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n };\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName;\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService));\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name;\n state.serviceId = serviceMatch.service_id;\n state.serviceDuration = serviceMatch.service_duration;\n state.step = 'book_appointment_datetime';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That’s fantastic—your ${state.serviceName} is available at ${state.storeName}! 🎉 When would you like it? Feel free to say something like 'tomorrow at 2 PM' or 'Friday at noon'—I’ll handle the rest!`;\n } else {\n responseMessage = `Oops, I couldn’t find the service \"${details.service_name || reply}\" at ${state.storeName}. Did you mean something like 'haircut' or 'manicure'? Feel free to try again or let me know what you had in mind!`;\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n // Handle booking flow - Date and time selection\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000);\n\n // Check availability and operating hours\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration);\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `Sorry, ${name}, the store is closed on ${apptStart.toLocaleDateString()}.`;\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `Sorry, ${name}, the requested time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours.`;\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `Sorry, ${name}, the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked.`;\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` How about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead? Or let me know another time that works for you!`;\n } else {\n responseMessage += ` I couldn’t find an available slot soon. Could you try a different day, or let me know when you’re free?`;\n }\n } else {\n // Slot is available, proceed with booking\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select();\n if (error) throw error;\n\n responseMessage = `Great news, ${name}! Your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}. Can’t wait to see you!`;\n conversationState.delete(from); // Reset state\n }\n } else {\n responseMessage = `Hmm, I didn’t quite catch that date or time. Could you please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'? I’m here to help!`;\n }\n } else {\n responseMessage = `When would you like your ${state.serviceName} at ${state.storeName}? Feel free to say something like 'tomorrow at 2 PM' or 'this Friday at 10 am'—I’ll handle the rest!`;\n }\n }\n } else if (state.step === 'store_info') {\n // Handle store info flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'selectOption';\n conversationState.set(from, state);\n responseMessage = `I found ${state.storeName}! What would you like to know about it?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n } else {\n responseMessage = `Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'store_info_details') {\n // Handle store info flow - Details selection\n const { storeId, storeName } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n const lowerReply = reply.toLowerCase();\n if (!state.infoType) {\n if (details.info_type === 'address' || lowerReply.includes('address')) {\n responseMessage = `Address for ${storeName}:\\n${store.store_addressL1}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'hours' || lowerReply.includes('hours') || lowerReply.includes('schedule')) {\n const hours = store.store_operating_hours;\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${info.close}`;\n })\n .join('\\n');\n responseMessage = `Operating Hours for ${storeName}:\\n${formattedHours}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'services' || lowerReply.includes('services')) {\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeName}:\\n${serviceList}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'packages' || lowerReply.includes('packages')) {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate} - ${endDate}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage = `Packages at ${storeName}:\\n${packageList}`;\n conversationState.delete(from); // Reset state\n } else {\n responseMessage = `What would you like to know about ${storeName}?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n }\n } else if (state.step === 'change_appointment') {\n // Handle change appointment flow\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n conversationState.delete(from); // Reset state\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start)); // Preserve original duration\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `All set, ${name}! Your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}.`;\n conversationState.delete(from); // Reset state\n } else {\n responseMessage = `I didn’t quite get that date or time. Please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'.`;\n }\n } else {\n responseMessage = `When would you like to reschedule your ${apptData.service_id} appointment? Just let me know with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n // Ask for phone number\n state.phoneNumberRequested = true;\n conversationState.set(from, state);\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Process the provided phone number\n const phoneNumber = reply;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single();\n if (globalError || !globalClient) {\n responseMessage = `Sorry, ${name}, I couldn’t find a client with the phone number ${phoneNumber}. Please try again with your registered number or contact support.`;\n conversationState.delete(from); // Reset state\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id);\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `It looks like you don’t have any active packages, ${name}. Would you like to purchase one?`;\n conversationState.delete(from); // Reset state\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n // Fetch store name from store_management.stores\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single();\n if (storeError) throw storeError;\n\n // Fetch package details from store_management.packages\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single();\n if (packageInfoError) throw packageInfoError;\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString();\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString();\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString();\n\n return `Package: ${packageInfo.package_name} at ${store.store_name}\\n` +\n `Description: ${packageInfo.package_description}\\n` +\n `Price: $${packageInfo.package_price}\\n` +\n `Total Sessions: ${packageInfo.num_sessions}, Sessions Remaining: ${pkg.session_remaining}\\n` +\n `Total Credits: ${packageInfo.num_credits}, Credits Remaining: ${pkg.credits_remaining}\\n` +\n `Purchased On: ${purchaseDate}\\n` +\n `Valid From: ${packageStart} to ${packageEnd}\\n` +\n `Status: ${pkg.status}`;\n }));\n responseMessage = `Here are your package details, ${name}:\\n${packageDetails.join('\\n\\n')}\\nLet me know if you need more information!`;\n conversationState.delete(from); // Reset state\n }\n }\n }\n }\n } else {\n // Handle intents when not in a conversation state\n if (intent === 'GREETING') {\n responseMessage = `Hello ${name}! How can I help you today? You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nJust let me know what you'd like!`;\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n } else if (intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n } else {\n conversationState.set(from, { step: 'change_appointment' });\n responseMessage = `Let's reschedule your ${apptData.service_id} appointment, ${name}. When would you like it? Try something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' });\n responseMessage = `I'd be happy to help with that, ${name}! Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to confirm for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Thank you, ${name}! Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed.`;\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to cancel for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Sorry to hear that, ${name}. Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled.`;\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false });\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Fallback for unknown intents\n const lowerMessage = reply.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n } else {\n responseMessage = `I'm not sure how to help with that, ${name}. You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nWhat would you like to do?`;\n }\n }\n }\n\n // Rephrase the response using DeepSeek\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name);\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${clientData.client_ph}`,\n body: rephrasedMessage\n });\n console.log(`Reply from ${name} (${from}): ${reply}`);\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`);\n res.status(200).send('Reply processed.');\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message);\n res.status(500).send('Error processing reply.');\n }\n});\n\n// Start the server and send reminders\nconst port = process.env.PORT || 3001;\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`);\n try {\n await sendReminders();\n } catch (error) {\n console.error('Error during reminder sending:', error.message);\n }\n});\n"}, "bf551df0fa79c2faa441ab44392348020ef7366a": {"t": 1785767119.8655105, "n": 48093, "h": "cb615c5c4a59289e", "c": "require('dotenv').config();\nconst express = require('express');\nconst twilio = require('twilio');\nconst { createClient } = require('@supabase/supabase-js');\nconst { OpenAI } = require('openai');\nconst chrono = require('chrono-node'); // Kept as per your request\nconst app = express();\n\napp.use(express.urlencoded({ extended: false }));\napp.use(express.json());\n\n// Initialize Twilio client\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing. Check your .env file.');\n process.exit(1);\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID);\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN);\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER);\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);\n\n// Initialize Supabase client\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing. Check your .env file.');\n process.exit(1);\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY);\n\n// Initialize DeepSeek client (using OpenAI SDK)\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing. Check your .env file.');\n process.exit(1);\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n});\n\n// In-memory store to track conversation state\nconst conversationState = new Map();\n\n// Fetch appointments from Supabase\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `);\n if (error) throw error;\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single();\n if (clientError) throw clientError;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (globalError) throw globalError;\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph };\n }));\n return appointments;\n } catch (error) {\n console.error('Error fetching appointments:', error.message);\n throw error;\n }\n};\n\n// Fetch store information (updated to include packages)\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single();\n if (storeError) throw storeError;\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId);\n if (servicesError) throw servicesError;\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId);\n if (packagesError) throw packagesError;\n\n return { store, services, packages };\n } catch (error) {\n console.error('Error fetching store info:', error.message);\n throw error;\n }\n};\n\n// Create client record\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: phoneNumber.slice(3), // Remove +65\n })\n .select('global_client_id')\n .single();\n if (globalError) throw globalError;\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n })\n .select('client_id')\n .single();\n if (clientError) throw clientError;\n\n return client.client_id;\n } catch (error) {\n console.error('Error creating client record:', error.message);\n throw error;\n }\n};\n\n// Send appointment reminders (1 day before appt_start)\nconst sendReminders = async () => {\n try {\n const today = new Date();\n today.setHours(0, 0, 0, 0); // Reset to start of day\n const appointments = await fetchAppointments();\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start);\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24));\n if (diffDays === 1) { // 1 day before\n try {\n const name = appt.preferred_name || appt.client_name;\n const message = `Hi ${name}, this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist. Reply YES to confirm, NO to cancel, or CHANGE to reschedule.`;\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n });\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`);\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message);\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message);\n throw error;\n }\n};\n\n// Add this before the POST handler\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query);\n res.status(200).send('Webhook endpoint is active. Use POST for WhatsApp messages.');\n});\n\n// Controller for booking appointments\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body;\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00');\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000); // Assuming 1-hour duration for simplicity\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select();\n if (error) throw error;\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] });\n } catch (error) {\n res.status(400).json({ error: error.message });\n }\n});\n\n// Function to rephrase messages using DeepSeek\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `Hi ${name}! It seems I don’t have enough details to assist you yet. Please let me know what you'd like to do, and I’ll be happy to help!`;\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon. Rephrase the following message to sound warm, natural, and human-like, while addressing the customer by their name (${name}). Keep the tone polite and conversational, as if you're speaking directly to the customer. Do not change the core meaning of the message. Do not include any explanatory notes or brackets.\n\n Message: \"${message}\"\n\n Rephrased message:\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in warm and friendly customer communication.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n });\n\n return completion.choices[0].message.content.trim();\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message);\n return message || `Hi ${name}! I’m having trouble processing your request. Please try again or let me know what you need!`;\n }\n}\n\n// Function to parse natural language date and time\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date(); // Updated to use current date\n let dateTime;\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`;\n // Use chrono-node with English locale only to avoid Japanese locale issues\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Fallback to basic parsing if chrono fails\n const today = now.toISOString().split('T')[0];\n dateTime = new Date(`${today}T${timeStr}:00+08:00`);\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`);\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Handle relative dates\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1));\n else if (dateStr.toLowerCase().includes('today')) dateTime = now;\n else dateTime = new Date(dateStr);\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0); // Default to midnight if no time\n }\n } else if (timeStr) {\n dateTime = new Date(now);\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i);\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10);\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0;\n const period = timeParts[3]?.toLowerCase();\n if (period === 'pm' && hours < 12) hours += 12;\n else if (period === 'am' && hours === 12) hours = 0;\n dateTime.setHours(hours, minutes, 0, 0);\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n // Ensure timezone is +08:00\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480);\n return dateTime;\n }\n return null;\n}\n\n// Function to check if the time slot is available and within operating hours\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5; // Hardcoded max appointments per slot\n const { store } = await fetchStoreInfo(storeId);\n const operatingHours = store.store_operating_hours;\n \n // Get the day of the week for the appointment\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n const apptDay = daysOfWeek[apptStart.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n // Check if the store is closed\n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Parse store opening and closing times\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n const storeOpenTime = new Date(apptStart);\n storeOpenTime.setHours(openHour, openMinute, 0, 0);\n const storeCloseTime = new Date(apptStart);\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Check if the appointment is within operating hours\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Check for overlapping appointments\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString());\n \n if (error) throw error;\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n return { isAvailable: true };\n}\n\n// Function to suggest the next available time slot\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n let suggestedTime = new Date(requestedTime);\n let attempts = 0;\n const maxAttempts = 7; // Try up to 7 days ahead\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n // Start from opening time or requested time if later\n let nextTime = new Date(suggestedTime);\n nextTime.setHours(openHour, openMinute, 0, 0);\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0);\n }\n\n const closeTime = new Date(suggestedTime);\n closeTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Increment by 30 minutes until a slot is found or we pass closing time\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000);\n if (apptEnd <= closeTime) {\n // Check if this slot is available (mock check since we can't call async here)\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n };\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30);\n }\n }\n \n // Move to the next day\n suggestedTime.setDate(suggestedTime.getDate() + 1);\n suggestedTime.setHours(0, 0, 0, 0); // Reset to start of day\n attempts++;\n }\n\n // If no slot is found within 7 days, suggest contacting support\n return null;\n}\n\n// Function to classify intent and extract details using DeepSeek\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent. The possible intents are:\n - GREETING (e.g., \"Hello\", \"Hi\")\n - BOOK_APPOINTMENT (e.g., \"I need to book an appointment\", \"I want a haircut\", \"I'd like to schedule a service\")\n - CHANGE_APPOINTMENT (e.g., \"I want to change my booking\", \"Can I reschedule\")\n - STORE_INFO (e.g., \"Can I get more details about a salon\", \"What are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\")\n - CANCEL_APPOINTMENT (e.g., \"NO\")\n - PACKAGE_INQUIRY (e.g., \"Can you tell me about my package?\", \"Is my package still active?\", \"How many sessions are left?\", \"Package details\")\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally, extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\")\n - service_name (e.g., \"haircut\", \"manicure\", or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"Haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n });\n\n let responseContent = completion.choices[0].message.content.trim();\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '');\n }\n\n return JSON.parse(responseContent);\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message);\n // Fallback to keyword-based intent classification\n const lowerMessage = message.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services')) {\n let infoType = null;\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours';\n else if (lowerMessage.includes('address')) infoType = 'address';\n else if (lowerMessage.includes('services')) infoType = 'services';\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n };\n } else if (lowerMessage.toUpperCase() === 'YES') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.toUpperCase() === 'NO') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n}\n\n// Webhook to handle customer replies\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body); // Log the incoming request\n const reply = req.body.Body;\n const from = req.body.From.replace('whatsapp:', '');\n console.log(`Processing message from ${from}: ${reply}`);\n try {\n // Hardcode remove +65 from incoming number for client_ph query\n let fromWithoutPrefix = from;\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3); // Remove +65 to get 96460132\n }\n\n // Check if client exists\n let clientData = null;\n let clientLink = null;\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single();\n \n if (clientError || !existingClient) {\n console.log(`No matching client data for ${fromWithoutPrefix}, creating new client.`);\n // Create a new client record\n const clientId = await createClientRecord(from, 'Unknown'); // Default name as 'Unknown'\n // Fetch the newly created client data\n const { data: newClient, error: newClientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single();\n if (newClientError || !newClient) {\n console.error(`Failed to fetch newly created client for ${fromWithoutPrefix}: ${newClientError?.message}`);\n res.status(500).send('Error creating new client.');\n return;\n }\n clientData = newClient;\n\n const { data: newClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (linkError || !newClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`);\n res.status(500).send('Error linking new client.');\n return;\n }\n clientLink = newClientLink;\n } else {\n clientData = existingClient;\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`);\n res.status(500).send('Error fetching client link.');\n return;\n }\n clientLink = existingClientLink;\n }\n\n let responseMessage;\n const name = clientData.preferred_name || clientData.client_name;\n\n // Classify the customer's intent using DeepSeek\n const { intent, details } = await classifyIntent(reply);\n\n // Check if we're in a conversation state\n if (conversationState.has(from)) {\n const state = conversationState.get(from);\n if (state.step === 'selectOption') {\n // Handle store info sub-menu\n const { storeId, storeName } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n\n if (reply === '1') {\n // Store Address\n responseMessage = `Address for ${storeName}: ${store.store_addressL1}`;\n } else if (reply === '2') {\n // Operating Hours\n const hours = store.store_operating_hours;\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${info.close}`;\n })\n .join('\\n');\n responseMessage = `Operating Hours for ${storeName}:\\n${formattedHours}`;\n } else if (reply === '3') {\n // Services\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeName}:\\n${serviceList}`;\n } else if (reply === '4') {\n // Packages\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate} - ${endDate}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage = `Packages at ${storeName}:\\n${packageList}`;\n } else {\n responseMessage = `Please select an option:\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n conversationState.delete(from); // Reset state\n } else if (state.step === 'book_appointment') {\n // Handle booking flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'book_appointment_service';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That's wonderful—I've found your appointment slot at ${state.storeName}. What service would you love to book today? Maybe a fresh haircut, a relaxing manicure, or something else? Let me know, and I’ll get it all set up for you!`;\n }\n } else {\n responseMessage = `Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'book_appointment_service') {\n // Handle booking flow - Service selection\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId);\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase());\n if (!serviceMatch && details.service_name) {\n // Try to match informal terms or partial names\n const lowerServiceName = details.service_name.toLowerCase();\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n };\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName;\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService));\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name;\n state.serviceId = serviceMatch.service_id;\n state.serviceDuration = serviceMatch.service_duration;\n state.step = 'book_appointment_datetime';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That’s fantastic—your ${state.serviceName} is available at ${state.storeName}! 🎉 When would you like it? Feel free to say something like 'tomorrow at 2 PM' or 'Friday at noon'—I’ll handle the rest!`;\n } else {\n responseMessage = `Oops, I couldn’t find the service \"${details.service_name || reply}\" at ${state.storeName}. Did you mean something like 'haircut' or 'manicure'? Feel free to try again or let me know what you had in mind!`;\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n // Handle booking flow - Date and time selection\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000);\n\n // Check availability and operating hours\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration);\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `Sorry, ${name}, the store is closed on ${apptStart.toLocaleDateString()}.`;\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `Sorry, ${name}, the requested time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours.`;\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `Sorry, ${name}, the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked.`;\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` How about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead? Or let me know another time that works for you!`;\n } else {\n responseMessage += ` I couldn’t find an available slot soon. Could you try a different day, or let me know when you’re free?`;\n }\n } else {\n // Slot is available, proceed with booking\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select();\n if (error) throw error;\n\n responseMessage = `Great news, ${name}! Your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}. Can’t wait to see you!`;\n conversationState.delete(from); // Reset state\n }\n } else {\n responseMessage = `Hmm, I didn’t quite catch that date or time. Could you please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'? I’m here to help!`;\n }\n } else {\n responseMessage = `When would you like your ${state.serviceName} at ${state.storeName}? Feel free to say something like 'tomorrow at 2 PM' or 'this Friday at 10 am'—I’ll handle the rest!`;\n }\n }\n } else if (state.step === 'store_info') {\n // Handle store info flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'selectOption';\n conversationState.set(from, state);\n responseMessage = `I found ${state.storeName}! What would you like to know about it?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n } else {\n responseMessage = `Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'store_info_details') {\n // Handle store info flow - Details selection\n const { storeId, name } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n const lowerReply = reply.toLowerCase();\n if (!state.infoType) {\n if (details.info_type === 'address' || lowerReply.includes('address')) {\n responseMessage = `Address for ${storeId}:\\n${store.store_addressL1}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'hours || lowerMessage.includes('hours') || lowerMessage.includes('schedule')) {\n const hours = store.store_operating_hours;\n const formattedHours = hours.map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${hour}: ${info.close}`;\n }).join('\\n');\n responseMessage = `Operating Hours for ${storeId}:\\n${formattedHours}`;\n conversationState.delete(from);\n } else if (details.info_type || lowerReply.includes('services')) {\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeId}:\\n${serviceList}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'packages' || lowerPackage.includes('packages')) {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate.toISOString()} - ${endDate.toISOString()}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage.delete(`Packages at ${storeId}:\\n${packageList}`);\n } else {\n responseMessage = `What would you like to know about ${storeId}?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n }\n } else if (state.step === 'change_appointment') {\n // Handle change appointment flow\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n conversationState.delete(from); // Reset state\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start)); // Preserve original duration\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `All set, ${name}! Your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}.`;\n conversationState.delete(from); // Reset state\n } else {\n responseMessage = `I didn’t quite get that date or time. Please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'.`;\n }\n } else {\n responseMessage = `When would you like to reschedule your ${apptData.service_id} appointment? Just let me know with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n // Ask for phone number\n state.phoneNumberRequested = true;\n conversationState.set(from, state);\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Process the provided phone number\n const phoneNumber = reply;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single();\n if (globalError || !globalClient) {\n responseMessage = `Sorry, ${name}, I couldn’t find a client with the phone number ${phoneNumber}. Please try again with your registered number or contact support.`;\n conversationState.delete(from); // Reset state\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id);\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `It looks like you don’t have any active packages, ${name}. Would you like to purchase one?`;\n conversationState.delete(from); // Reset state\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n // Fetch store name from store_management.stores\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single();\n if (storeError) throw storeError;\n\n // Fetch package details from store_management.packages\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single();\n if (packageInfoError) throw packageInfoError;\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString();\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString();\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString();\n\n return `Package: ${packageInfo.package_name} at ${store.store_name}\\n` +\n `Description: ${packageInfo.package_description}\\n` +\n `rPrice: ${packageInfo.package_price}\\n` +\n `Total Sessions: ${packageInfo.num_sessions}, Sessions Remaining: ${pkg.session_remaining}\\n` +\n `Total Credits: ${packageInfo.num_credits}, Credits Remaining: ${pkg.credits_remaining}\\n` +\n `Purchased On: ${purchaseDate.toString()}\\n` +\n `Valid From: ${packageStart.toISOString()} to ${packageEnd.toISOString()}\\n` +\n `Status: ${pkg.status}`;\n }));\n responseMessage = `Here are your package details, ${name}:\\n${packageDetails.join('\\n\\n')}\\nLet me know if you need more information!`;\n conversationState.delete(from); // Reset state\n }\n }\n }\n }\n } else {\n // Handle intents when not in a conversation state\n if (intent === 'GREETING') {\n responseMessage = `Hello ${name}! How can I help you today? You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nJust let me know what you'd like!`;\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Id)?`;\n } else if (intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n } else {\n conversationState.set(from, { step: 'change_appointment' });\n responseMessage = `Let's reschedule your ${apptData.service_id} appointment, ${name}. When would you like it? Try something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' });\n responseMessage = `I'd be happy to help with that, ${name}! Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to confirm for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Thank you, ${name}! Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${apptData.appt_start.toLocaleTimeString()} is confirmed.`;\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to cancel for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Sorry to hear that, ${name}. Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled.`;\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false });\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Fallback for unknown intents\n const lowerMessage = reply.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n } else {\n responseMessage = `I'm not sure how to help with that, ${name}. You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nWhat would you like to do?`;\n }\n }\n }\n\n // Rephrase the response using DeepSeek\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name);\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${clientData.client_ph}`,\n body: rephrasedMessage\n });\n console.log(`Reply from ${name} (${from}): ${reply}`);\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`);\n res.status(200).send('Reply processed.');\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message);\n res.status(500).send('Error processing reply.');\n }\n});\n\n// Start the server and send reminders\nconst port = process.env.PORT || 3001;\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`);\n try {\n await sendReminders();\n } catch (error) {\n console.error('Error during reminder sending:', error.message);\n }\n});\n"}, "3e08d790ddb74e3d7e7994b1fe5f8cf4d77c4a05": {"t": 1785767121.353552, "n": 58092, "h": "762dab5688b4e64e", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nconsole.log('Starting application setup...')\n\n// Verify environment variables and initialize clients\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Twilio credentials loaded successfully')\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Twilio client initialized')\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Supabase credentials loaded successfully')\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\nconsole.log('Supabase client initialized')\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconsole.log('DeepSeek API key loaded successfully')\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\nconsole.log('OpenAI client initialized')\n\nconst conversationState = new Map()\nconsole.log('Conversation state map initialized')\n\nconst fetchAppointments = async () => {\n try {\n console.log('Fetching appointments...')\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id\n store_id\n assignment_id\n client_id\n service_id\n appt_start\n appt_end\n `)\n if (error) throw error\n console.log('Appointments fetched:', data.length)\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n console.log('Fetching store info for storeId:', storeId)\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name store_operating_hours store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n console.log('Store fetched:', store)\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_description price service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n console.log('Services fetched:', services.length)\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id package_name package_description package_limit package_type num_sessions num_credits package_price services_linked package_start package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n console.log('Packages fetched:', packages.length)\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n console.log('Creating client record for phone:', phoneNumber)\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n console.log('Linking client to store:', globalClientId, storeId)\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n console.log('Sending reminders...')\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n console.log('Received book-appointment request:', req.body)\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name) {\n try {\n console.log('Rephrasing message:', message)\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n return completion.choices[0].message.content.trim()\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message) {\n try {\n console.log('Classifying intent for message:', message)\n const prompt = `\n You are a helpful assistant for Glamour Salon analyze the customer's message and classify their intent the possible intents are\n - GREETING (e.g. \"hello\" \"hi\" \"hey there\" \"good morning\")\n - BOOK_APPOINTMENT (e.g. \"i need to book an appointment\" \"i want a haircut\" \"i'd like to schedule a service\" \"can i book a manicure\" \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g. \"i want to change my booking\" \"can i reschedule\" \"move my appointment\" \"change my time slot\")\n - STORE_INFO (e.g. \"can i get more details about a salon\" \"what are your hours\" \"operating hours for Idle\" \"address of Glamour Salon\" \"services at Idle\" \"what do you offer\" \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g. \"can i see my upcoming appointments\" \"what are my bookings\" \"show my appointments\" \"what's my schedule\" \"do i have any appointments\" \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g. \"YES\" \"yes\" \"confirm\" \"i confirm\")\n - CANCEL_APPOINTMENT (e.g. \"NO\" \"no\" \"cancel\" \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g. \"can you tell me about my package\" \"is my package still active\" \"how many sessions are left\" \"package details\" \"what's my package status\")\n - REGISTER_CONFIRM (e.g. \"YES\" \"yes\" \"sure\" \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g. \"NO\" \"no\" \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally extract any relevant details such as\n - store_name (e.g. \"Idle\" \"Glamour Salon\" \"Idle salon\")\n - service_name (e.g. \"haircut\" \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g. \"26/05/2025\" \"tomorrow\" \"next Friday\" \"this Friday\")\n - time (e.g. \"14:00\" \"2 PM\" \"12:34 AM\" \"noon\" \"10 am\")\n - info_type (for STORE_INFO intent \"address\" for address-related requests \"hours\" for operating hours or schedule requests \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g. \"haircut maybe\" should be \"haircut\") or informal terms (e.g. \"cut\" should be interpreted as \"haircut\" \"nails\" as \"manicure\") if unsure prioritize the most likely service based on context\n\n Customer message: \"${message}\"\n\n Respond in JSON format\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n const result = JSON.parse(responseContent)\n console.log('Intent classification result:', result)\n return result\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have') || lowerMessage.includes('i want to see upcoming appointments')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name global_client_id client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n\n const { intent, details } = await classifyIntent(reply)\n\n // Reset state if intent changes to a new top-level intent\n console.log('Current state before reset:', conversationState.get(from))\n console.log('Detected intent:', intent)\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n console.log('Resetting conversation state for new intent:', intent)\n conversationState.delete(from)\n }\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n console.log('Current conversation state:', state)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n } else if (reply === '2') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n conversationState.delete(from)\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error:', storeError?.message)\n console.log('Store not found in database:', details.store_name)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n console.log('Searching for store in store_info:', details.store_name)\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name.replace(' salon', '').trim()}%`)\n .single()\n if (storeError || !storeRecord) {\n console.error('Store search error in store_info:', storeError?.message)\n console.log('Store not found in database in store_info:', details.store_name)\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n console.log('Store found in store_info:', storeRecord)\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id\n store_id\n package_id\n global_client_id\n client_id\n session_remaining\n credits_remaining\n purchase_date\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name\n package_description\n num_sessions\n num_credits\n package_price\n package_start\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'VIEW_APPOINTMENTS' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\nconsole.log('Server setup complete, listening for requests...')\n"}, "e5f9b45167a0fdd4b5510e529132b1cfae48667a": {"t": 1785767123.025308, "n": 55924, "h": "1a0b88dcc87c5a68", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\n\nconst conversationState = new Map()\n\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id\n store_id\n assignment_id\n client_id\n service_id\n appt_start\n appt_end\n `)\n if (error) throw error\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name store_operating_hours store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_description price service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id package_name package_description package_limit package_type num_sessions num_credits package_price services_linked package_start package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n return completion.choices[0].message.content.trim()\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon analyze the customer's message and classify their intent the possible intents are\n - GREETING (e.g. \"hello\" \"hi\" \"hey there\" \"good morning\")\n - BOOK_APPOINTMENT (e.g. \"i need to book an appointment\" \"i want a haircut\" \"i'd like to schedule a service\" \"can i book a manicure\" \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g. \"i want to change my booking\" \"can i reschedule\" \"move my appointment\" \"change my time slot\")\n - STORE_INFO (e.g. \"can i get more details about a salon\" \"what are your hours\" \"operating hours for Idle\" \"address of Glamour Salon\" \"services at Idle\" \"what do you offer\" \"can u get store details for me\")\n - VIEW_APPOINTMENTS (e.g. \"can i see my upcoming appointments\" \"what are my bookings\" \"show my appointments\" \"what's my schedule\" \"do i have any appointments\" \"i want to see upcoming appointments\")\n - CONFIRM_APPOINTMENT (e.g. \"YES\" \"yes\" \"confirm\" \"i confirm\")\n - CANCEL_APPOINTMENT (e.g. \"NO\" \"no\" \"cancel\" \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g. \"can you tell me about my package\" \"is my package still active\" \"how many sessions are left\" \"package details\" \"what's my package status\")\n - REGISTER_CONFIRM (e.g. \"YES\" \"yes\" \"sure\" \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g. \"NO\" \"no\" \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally extract any relevant details such as\n - store_name (e.g. \"Idle\" \"Glamour Salon\" \"Idle salon\")\n - service_name (e.g. \"haircut\" \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g. \"26/05/2025\" \"tomorrow\" \"next Friday\" \"this Friday\")\n - time (e.g. \"14:00\" \"2 PM\" \"12:34 AM\" \"noon\" \"10 am\")\n - info_type (for STORE_INFO intent \"address\" for address-related requests \"hours\" for operating hours or schedule requests \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g. \"haircut maybe\" should be \"haircut\") or informal terms (e.g. \"cut\" should be interpreted as \"haircut\" \"nails\" as \"manicure\") if unsure prioritize the most likely service based on context\n\n Customer message: \"${message}\"\n\n Respond in JSON format\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n return JSON.parse(responseContent)\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer') || lowerMessage.includes('store details')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name global_client_id client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n\n const { intent, details } = await classifyIntent(reply)\n\n // Reset state if intent changes to a new top-level intent\n if (conversationState.has(from) && ['GREETING', 'BOOK_APPOINTMENT', 'VIEW_APPOINTMENTS', 'CHANGE_APPOINTMENT', 'STORE_INFO', 'PACKAGE_INQUIRY'].includes(intent)) {\n conversationState.delete(from)\n }\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n } else if (reply === '2') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n conversationState.delete(from)\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', details.store_name.replace(' salon', '').trim())\n .single()\n if (storeError || !storeRecord) {\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n if (state.intent === 'VIEW_APPOINTMENTS') {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n } else if (state.intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n state.step = 'change_appointment'\n state.clientId = clientId\n conversationState.set(from, state)\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else if (state.intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n conversationState.delete(from)\n }\n } else if (state.intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n conversationState.delete(from)\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n conversationState.delete(from)\n }\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', details.store_name.replace(' salon', '').trim())\n .single()\n if (storeError || !storeRecord) {\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'change_appointment') {\n const clientId = state.clientId || clientLink.client_id\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientId)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id\n store_id\n package_id\n global_client_id\n client_id\n session_remaining\n credits_remaining\n purchase_date\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name\n package_description\n num_sessions\n num_credits\n package_price\n package_start\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'VIEW_APPOINTMENTS' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientLink.client_id)\n .gt('appt_start', new Date().toISOString()) // Only future appointments\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how can I assist you today you can book an appointment change an appointment get store info or check your package details just let me know what you'd like`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\n"}, "42d25c8b3ac532f8f476cd71d6954a625437dfa4": {"t": 1785767125.3915958, "n": 52603, "h": "de832305d126c20c", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\n\nconst conversationState = new Map()\n\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id\n store_id\n assignment_id\n client_id\n service_id\n appt_start\n appt_end\n `)\n if (error) throw error\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name store_operating_hours store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_description price service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id package_name package_description package_limit package_type num_sessions num_credits package_price services_linked package_start package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n return completion.choices[0].message.content.trim()\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon analyze the customer's message and classify their intent the possible intents are\n - GREETING (e.g. \"hello\" \"hi\" \"hey there\" \"good morning\")\n - BOOK_APPOINTMENT (e.g. \"i need to book an appointment\" \"i want a haircut\" \"i'd like to schedule a service\" \"can i book a manicure\" \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g. \"i want to change my booking\" \"can i reschedule\" \"move my appointment\" \"change my time slot\")\n - STORE_INFO (e.g. \"can i get more details about a salon\" \"what are your hours\" \"operating hours for Idle\" \"address of Glamour Salon\" \"services at Idle\" \"what do you offer\")\n - VIEW_APPOINTMENTS (e.g. \"can i see my upcoming appointments\" \"what are my bookings\" \"show my appointments\" \"what's my schedule\" \"do i have any appointments\")\n - CONFIRM_APPOINTMENT (e.g. \"YES\" \"yes\" \"confirm\" \"i confirm\")\n - CANCEL_APPOINTMENT (e.g. \"NO\" \"no\" \"cancel\" \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g. \"can you tell me about my package\" \"is my package still active\" \"how many sessions are left\" \"package details\" \"what's my package status\")\n - REGISTER_CONFIRM (e.g. \"YES\" \"yes\" \"sure\" \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g. \"NO\" \"no\" \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally extract any relevant details such as\n - store_name (e.g. \"Idle\" \"Glamour Salon\")\n - service_name (e.g. \"haircut\" \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g. \"26/05/2025\" \"tomorrow\" \"next Friday\" \"this Friday\")\n - time (e.g. \"14:00\" \"2 PM\" \"12:34 AM\" \"noon\" \"10 am\")\n - info_type (for STORE_INFO intent \"address\" for address-related requests \"hours\" for operating hours or schedule requests \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g. \"haircut maybe\" should be \"haircut\") or informal terms (e.g. \"cut\" should be interpreted as \"haircut\" \"nails\" as \"manicure\") if unsure prioritize the most likely service based on context\n\n Customer message: \"${message}\"\n\n Respond in JSON format\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n return JSON.parse(responseContent)\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name global_client_id client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n\n const { intent, details } = await classifyIntent(reply)\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n } else if (reply === '2') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n conversationState.delete(from)\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name}%`)\n .single()\n if (storeError || !storeRecord) {\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .ilike('store_name', `%${details.store_name}%`)\n .single()\n if (storeError || !storeRecord) {\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'view_appointments') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId } = await createClientRecord(from, clientName)\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'change_appointment') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id\n store_id\n package_id\n global_client_id\n client_id\n session_remaining\n credits_remaining\n purchase_date\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name\n package_description\n num_sessions\n num_credits\n package_price\n package_start\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how may I help you`\n } else if (intent === 'BOOK_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'book_appointment', from, intent: 'BOOK_APPOINTMENT' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else {\n conversationState.set(from, { step: 'book_appointment' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n }\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'view_appointments', from })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientLink.client_id)\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how may I help you`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\n"}, "7ffbcd5acd4d21b4f707cebf20300bcb57e18002": {"t": 1785767126.8072014, "n": 52517, "h": "6a9a12ec5284dbd4", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\n\nconst conversationState = new Map()\n\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id\n store_id\n assignment_id\n client_id\n service_id\n appt_start\n appt_end\n `)\n if (error) throw error\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name store_operating_hours store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_description price service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id package_name package_description package_limit package_type num_sessions num_credits package_price services_linked package_start package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst linkClientToStore = async (globalClientId, storeId) => {\n try {\n const { error } = await supabase\n .schema('clients')\n .from('clients')\n .update({ store_id: storeId })\n .eq('global_client_id', globalClientId)\n if (error) throw error\n } catch (error) {\n console.error('Error linking client to store:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n return completion.choices[0].message.content.trim()\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon analyze the customer's message and classify their intent the possible intents are\n - GREETING (e.g. \"hello\" \"hi\" \"hey there\" \"good morning\")\n - BOOK_APPOINTMENT (e.g. \"i need to book an appointment\" \"i want a haircut\" \"i'd like to schedule a service\" \"can i book a manicure\" \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g. \"i want to change my booking\" \"can i reschedule\" \"move my appointment\" \"change my time slot\")\n - STORE_INFO (e.g. \"can i get more details about a salon\" \"what are your hours\" \"operating hours for Idle\" \"address of Glamour Salon\" \"services at Idle\" \"what do you offer\")\n - VIEW_APPOINTMENTS (e.g. \"can i see my upcoming appointments\" \"what are my bookings\" \"show my appointments\" \"what's my schedule\" \"do i have any appointments\")\n - CONFIRM_APPOINTMENT (e.g. \"YES\" \"yes\" \"confirm\" \"i confirm\")\n - CANCEL_APPOINTMENT (e.g. \"NO\" \"no\" \"cancel\" \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g. \"can you tell me about my package\" \"is my package still active\" \"how many sessions are left\" \"package details\" \"what's my package status\")\n - REGISTER_CONFIRM (e.g. \"YES\" \"yes\" \"sure\" \"okay\" for confirming registration)\n - REGISTER_DECLINE (e.g. \"NO\" \"no\" \"not now\" for declining registration)\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally extract any relevant details such as\n - store_name (e.g. \"Idle\" \"Glamour Salon\")\n - service_name (e.g. \"haircut\" \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g. \"26/05/2025\" \"tomorrow\" \"next Friday\" \"this Friday\")\n - time (e.g. \"14:00\" \"2 PM\" \"12:34 AM\" \"noon\" \"10 am\")\n - info_type (for STORE_INFO intent \"address\" for address-related requests \"hours\" for operating hours or schedule requests \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g. \"haircut maybe\" should be \"haircut\") or informal terms (e.g. \"cut\" should be interpreted as \"haircut\" \"nails\" as \"manicure\") if unsure prioritize the most likely service based on context\n\n Customer message: \"${message}\"\n\n Respond in JSON format\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n return JSON.parse(responseContent)\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'sure' || lowerMessage.toLowerCase() === 'okay') {\n return {\n intent: 'REGISTER_CONFIRM',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'not now') {\n return {\n intent: 'REGISTER_DECLINE',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name global_client_id client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n\n const { intent, details } = await classifyIntent(reply)\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n } else if (reply === '2') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n conversationState.delete(from)\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .eq('store_name', details.store_name)\n .single()\n if (!storeRecord) {\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n state.step = 'ask_to_register'\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId, globalClientId } = await createClientRecord(from, clientName)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n state.globalClientId = globalClientId\n state.step = 'ask_to_register'\n conversationState.set(from, state)\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'ask_to_register') {\n if (intent === 'REGISTER_CONFIRM') {\n state.step = 'collect_registration_details'\n conversationState.set(from, state)\n responseMessage = `great ${name} let’s get your details saved please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n } else if (intent === 'REGISTER_DECLINE') {\n responseMessage = `no worries ${name} your booking is still confirmed see you soon`\n conversationState.delete(from)\n } else {\n responseMessage = `would you like to save your details for easier bookings next time just say YES or NO`\n }\n } else if (state.step === 'collect_registration_details') {\n if (reply.toLowerCase() === 'skip') {\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `all done ${name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n const detailsMatch = reply.match(/([a-zA-Z\\s]+)(?:,\\s*(male|female|other))?/i)\n if (detailsMatch) {\n const preferredName = detailsMatch[1].trim()\n const gender = detailsMatch[2] ? detailsMatch[2].toLowerCase() : null\n const { error } = await supabase\n .schema('clients')\n .from('global_clients')\n .update({\n preferred_name: preferredName !== name ? preferredName : null,\n gender: gender\n })\n .eq('global_client_id', state.globalClientId)\n if (error) throw error\n\n if (state.globalClientId && state.appointmentDetails.storeId) {\n await linkClientToStore(state.globalClientId, state.appointmentDetails.storeId)\n }\n responseMessage = `thanks ${preferredName || name} your details are saved see you at your appointment`\n conversationState.delete(from)\n } else {\n responseMessage = `please share your preferred name if different from ${name} and your gender if you’d like for example Preferred Name Gender like Jane female or just say skip to keep it as is`\n }\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .eq('store_name', details.store_name)\n .single()\n if (!storeRecord) {\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'view_appointments') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId } = await createClientRecord(from, clientName)\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'change_appointment') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id\n store_id\n package_id\n global_client_id\n client_id\n session_remaining\n credits_remaining\n purchase_date\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name\n package_description\n num_sessions\n num_credits\n package_price\n package_start\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} how may I help you`\n } else if (intent === 'BOOK_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'book_appointment', from, intent: 'BOOK_APPOINTMENT' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n } else {\n conversationState.set(from, { step: 'book_appointment' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n }\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'view_appointments', from })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientLink.client_id)\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n responseMessage = `hello ${name} how may I help you`\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\n"}, "393bb98290b2a8fa7a2f28f5271f39682c7b42cf": {"t": 1785767128.5749319, "n": 49015, "h": "b1736e609487e2ae", "c": "require('dotenv').config()\nconst express = require('express')\nconst twilio = require('twilio')\nconst { createClient } = require('@supabase/supabase-js')\nconst { OpenAI } = require('openai')\nconst chrono = require('chrono-node')\nconst app = express()\n\napp.use(express.urlencoded({ extended: false }))\napp.use(express.json())\n\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing check your .env file')\n process.exit(1)\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID)\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN)\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER)\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)\n\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing check your .env file')\n process.exit(1)\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY)\n\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing check your .env file')\n process.exit(1)\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n})\n\nconst conversationState = new Map()\n\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id\n store_id\n assignment_id\n client_id\n service_id\n appt_start\n appt_end\n `)\n if (error) throw error\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single()\n if (clientError) throw clientError\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (globalError) throw globalError\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph }\n }))\n return appointments\n } catch (error) {\n console.error('Error fetching appointments:', error.message)\n throw error\n }\n}\n\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name store_operating_hours store_addressL1')\n .eq('store_id', storeId)\n .single()\n if (storeError) throw storeError\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_description price service_duration')\n .eq('store_id', storeId)\n if (servicesError) throw servicesError\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id package_name package_description package_limit package_type num_sessions num_credits package_price services_linked package_start package_end')\n .eq('store_id', storeId)\n if (packagesError) throw packagesError\n\n return { store, services, packages }\n } catch (error) {\n console.error('Error fetching store info:', error.message)\n throw error\n }\n}\n\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10),\n })\n .select('global_client_id')\n .single()\n if (globalError) throw globalError\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single()\n if (clientError) throw clientError\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id }\n } catch (error) {\n console.error('Error creating client record:', error.message)\n throw error\n }\n}\n\nconst sendReminders = async () => {\n try {\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n const appointments = await fetchAppointments()\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start)\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24))\n if (diffDays === 1) {\n try {\n const name = appt.preferred_name || appt.client_name\n const message = `hi ${name} this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist reply YES to confirm NO to cancel or CHANGE to reschedule`\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n })\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`)\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message)\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message)\n throw error\n }\n}\n\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query)\n res.status(200).send('Webhook endpoint is active use POST for WhatsApp messages')\n})\n\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00')\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select()\n if (error) throw error\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] })\n } catch (error) {\n res.status(400).json({ error: error.message })\n }\n})\n\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `hi ${name} I don't have enough details yet please let me know what you'd like to do and I'll help you`\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon rephrase the following message to sound warm natural and human-like while addressing the customer by their name (${name}) keep the tone polite and conversational as if you're speaking directly to the customer do not change the core meaning of the message do not include any explanatory notes brackets emojis or unnecessary punctuation\n\n Message: \"${message}\"\n\n Rephrased message:\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in warm and friendly customer communication' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n })\n\n return completion.choices[0].message.content.trim()\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message)\n return message || `hi ${name} I'm having trouble with your request please try again or let me know what you need`\n }\n}\n\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date()\n let dateTime\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0]\n dateTime = new Date(`${today}T${timeStr}:00+08:00`)\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`)\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' })\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1))\n else if (dateStr.toLowerCase().includes('today')) dateTime = now\n else dateTime = new Date(dateStr)\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0)\n }\n } else if (timeStr) {\n dateTime = new Date(now)\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i)\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10)\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0\n const period = timeParts[3]?.toLowerCase()\n if (period === 'pm' && hours < 12) hours += 12\n else if (period === 'am' && hours === 12) hours = 0\n dateTime.setHours(hours, minutes, 0, 0)\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480)\n return dateTime\n }\n return null\n}\n\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5\n const { store } = await fetchStoreInfo(storeId)\n const operatingHours = store.store_operating_hours\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n const apptDay = daysOfWeek[apptStart.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n const storeOpenTime = new Date(apptStart)\n storeOpenTime.setHours(openHour, openMinute, 0, 0)\n const storeCloseTime = new Date(apptStart)\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0)\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString())\n \n if (error) throw error\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n }\n }\n\n return { isAvailable: true }\n}\n\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']\n let suggestedTime = new Date(requestedTime)\n let attempts = 0\n const maxAttempts = 7\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()]\n const storeHours = operatingHours[apptDay.toLowerCase()]\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number)\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number)\n \n let nextTime = new Date(suggestedTime)\n nextTime.setHours(openHour, openMinute, 0, 0)\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0)\n }\n\n const closeTime = new Date(suggestedTime)\n closeTime.setHours(closeHour, closeMinute, 0, 0)\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000)\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n }\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30)\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1)\n suggestedTime.setHours(0, 0, 0, 0)\n attempts++\n }\n\n return null\n}\n\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon analyze the customer's message and classify their intent the possible intents are\n - GREETING (e.g. \"hello\" \"hi\" \"hey there\" \"good morning\")\n - BOOK_APPOINTMENT (e.g. \"i need to book an appointment\" \"i want a haircut\" \"i'd like to schedule a service\" \"can i book a manicure\" \"book me for a trim\")\n - CHANGE_APPOINTMENT (e.g. \"i want to change my booking\" \"can i reschedule\" \"move my appointment\" \"change my time slot\")\n - STORE_INFO (e.g. \"can i get more details about a salon\" \"what are your hours\" \"operating hours for Idle\" \"address of Glamour Salon\" \"services at Idle\" \"what do you offer\")\n - VIEW_APPOINTMENTS (e.g. \"can i see my upcoming appointments\" \"what are my bookings\" \"show my appointments\" \"what's my schedule\" \"do i have any appointments\")\n - CONFIRM_APPOINTMENT (e.g. \"YES\" \"yes\" \"confirm\" \"i confirm\")\n - CANCEL_APPOINTMENT (e.g. \"NO\" \"no\" \"cancel\" \"i want to cancel\")\n - PACKAGE_INQUIRY (e.g. \"can you tell me about my package\" \"is my package still active\" \"how many sessions are left\" \"package details\" \"what's my package status\")\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally extract any relevant details such as\n - store_name (e.g. \"Idle\" \"Glamour Salon\")\n - service_name (e.g. \"haircut\" \"manicure\" or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g. \"26/05/2025\" \"tomorrow\" \"next Friday\" \"this Friday\")\n - time (e.g. \"14:00\" \"2 PM\" \"12:34 AM\" \"noon\" \"10 am\")\n - info_type (for STORE_INFO intent \"address\" for address-related requests \"hours\" for operating hours or schedule requests \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g. \"haircut maybe\" should be \"haircut\") or informal terms (e.g. \"cut\" should be interpreted as \"haircut\" \"nails\" as \"manicure\") if unsure prioritize the most likely service based on context\n\n Customer message: \"${message}\"\n\n Respond in JSON format\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n })\n\n let responseContent = completion.choices[0].message.content.trim()\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '')\n }\n\n return JSON.parse(responseContent)\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message)\n const lowerMessage = message.toLowerCase()\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify') || lowerMessage.includes('move my appointment')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services') || lowerMessage.includes('what do you offer')) {\n let infoType = null\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours'\n else if (lowerMessage.includes('address')) infoType = 'address'\n else if (lowerMessage.includes('services') || lowerMessage.includes('offer')) infoType = 'services'\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n }\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings') || lowerMessage.includes('my schedule') || lowerMessage.includes('what appointments do i have')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'YES' || lowerMessage.toLowerCase() === 'yes' || lowerMessage.toLowerCase() === 'confirm' || lowerMessage.toLowerCase() === 'i confirm') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.toUpperCase() === 'NO' || lowerMessage.toLowerCase() === 'no' || lowerMessage.toLowerCase() === 'cancel' || lowerMessage.toLowerCase() === 'i want to cancel') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey') || lowerMessage.includes('good morning') || lowerMessage.includes('good afternoon')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active') || lowerMessage.includes('package status')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n }\n }\n}\n\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body)\n const reply = req.body.Body\n const from = req.body.From.replace('whatsapp:', '')\n console.log(`Processing message from ${from}: ${reply}`)\n try {\n let fromWithoutPrefix = from\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3)\n }\n\n let clientData = null\n let clientLink = null\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name preferred_name global_client_id client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single()\n \n if (!clientError && existingClient) {\n clientData = existingClient\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single()\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`)\n res.status(500).send('Error fetching client link')\n return\n }\n clientLink = existingClientLink\n }\n\n let responseMessage\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there'\n\n const { intent, details } = await classifyIntent(reply)\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from)\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state\n const { store, services, packages } = await fetchStoreInfo(storeId)\n\n if (reply === '1') {\n responseMessage = `the address for ${storeName} is ${store.store_addressL1}`\n } else if (reply === '2') {\n const hours = store.store_operating_hours\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day} closed`\n return `${day} ${info.open} to ${info.close}`\n })\n .join('\\n')\n responseMessage = `here are the operating hours for ${storeName}\\n${formattedHours}`\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name} $${s.price} ${s.service_duration} mins`).join('\\n')\n responseMessage = `these are the services at ${storeName}\\n${serviceList}`\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString()\n const endDate = new Date(p.package_end).toLocaleDateString()\n return `${p.package_name} $${p.package_price} ${p.num_sessions} sessions ${p.num_credits} credits valid ${startDate} to ${endDate} ${p.package_description}`\n }).join('\\n')\n responseMessage = `here are the packages at ${storeName}\\n${packageList}`\n } else {\n responseMessage = `please pick an option\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n conversationState.delete(from)\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .eq('store_name', details.store_name)\n .single()\n if (!storeRecord) {\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'book_appointment_service'\n conversationState.set(from, state)\n responseMessage = `great ${name} I found ${state.storeName} what service would you like to book today maybe a haircut or a manicure`\n }\n } else {\n responseMessage = `which store would you like to book at maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id service_name service_duration')\n .eq('store_id', state.storeId)\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase())\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase()\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n }\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService))\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name\n state.serviceId = serviceMatch.service_id\n state.serviceDuration = serviceMatch.service_duration\n state.step = 'book_appointment_datetime'\n conversationState.set(from, state)\n responseMessage = `nice choice ${name} your ${state.serviceName} is available at ${state.storeName} when would you like to come in just say something like tomorrow at 2 PM or Friday at noon`\n } else {\n responseMessage = `sorry ${name} I couldn’t find the service ${details.service_name || reply} at ${state.storeName} did you mean something like haircut or manicure try again or let me know what you meant`\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000)\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration)\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `sorry ${name} the store is closed on ${apptStart.toLocaleDateString()}`\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `sorry ${name} the time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours`\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `sorry ${name} the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked`\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` how about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead or let me know another time that works for you`\n } else {\n responseMessage += ` I couldn’t find an available slot soon can you try a different day or let me know when you’re free`\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details'\n state.from = from\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n }\n conversationState.set(from, state)\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n responseMessage = `all set ${name} your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} looking forward to seeing you`\n conversationState.delete(from)\n }\n }\n } else {\n responseMessage = `sorry ${name} I didn’t catch that date or time can you try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like your ${state.serviceName} at ${state.storeName} just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId } = await createClientRecord(from, clientName)\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select()\n if (error) throw error\n\n name = clientName\n responseMessage = `wonderful ${name} your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} see you soon`\n conversationState.delete(from)\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id store_name')\n .eq('store_name', details.store_name)\n .single()\n if (!storeRecord) {\n responseMessage = `sorry ${name} I couldn't find a store named ${details.store_name} can you try again`\n } else {\n state.storeName = storeRecord.store_name\n state.storeId = storeRecord.store_id\n state.step = 'selectOption'\n conversationState.set(from, state)\n responseMessage = `I found ${state.storeName} what would you like to know about it\\n1 store address\\n2 operating hours\\n3 services\\n4 packages`\n }\n } else {\n responseMessage = `which store would you like more details about maybe Idle or Glamour Salon`\n }\n }\n } else if (state.step === 'view_appointments') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/)\n if (nameMatch) {\n const clientName = nameMatch[1].trim()\n const phoneNumber = nameMatch[2].trim()\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `the phone number you gave ${phoneNumber} doesn’t match the one you’re messaging from ${fromWithoutPrefix} please use the same number or let me know if this is on purpose`\n } else {\n const { clientId } = await createClientRecord(from, clientName)\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientId)\n if (apptError) throw apptError\n\n name = clientName\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n conversationState.delete(from)\n }\n } else {\n responseMessage = `sorry ${name} I didn’t get that please give your name and phone number like this Name Number for example John 96460132`\n }\n } else if (state.step === 'change_appointment') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start appt_end service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n conversationState.delete(from)\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time)\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start))\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `all set ${name} your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n conversationState.delete(from)\n } else {\n responseMessage = `sorry ${name} I didn’t get that date or time please try again with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n } else {\n responseMessage = `when would you like to reschedule your ${apptData.service_id} appointment just let me know with something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true\n conversationState.set(from, state)\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const phoneNumber = reply\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single()\n if (globalError || !globalClient) {\n responseMessage = `sorry ${name} I couldn’t find a client with the phone number ${phoneNumber} please try again with your registered number or contact support`\n conversationState.delete(from)\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id\n store_id\n package_id\n global_client_id\n client_id\n session_remaining\n credits_remaining\n purchase_date\n status\n `)\n .eq('global_client_id', globalClient.global_client_id)\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `looks like you don’t have any active packages ${name} would you like to purchase one`\n conversationState.delete(from)\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single()\n if (storeError) throw storeError\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name\n package_description\n num_sessions\n num_credits\n package_price\n package_start\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single()\n if (packageInfoError) throw packageInfoError\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString()\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString()\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString()\n\n return `package ${packageInfo.package_name} at ${store.store_name}\\n` +\n `description ${packageInfo.package_description}\\n` +\n `price $${packageInfo.package_price}\\n` +\n `total sessions ${packageInfo.num_sessions} sessions remaining ${pkg.session_remaining}\\n` +\n `total credits ${packageInfo.num_credits} credits remaining ${pkg.credits_remaining}\\n` +\n `purchased on ${purchaseDate}\\n` +\n `valid from ${packageStart} to ${packageEnd}\\n` +\n `status ${pkg.status}`\n }))\n responseMessage = `here are your package details ${name}\\n${packageDetails.join('\\n\\n')} let me know if you need more info`\n conversationState.delete(from)\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `hello ${name} welcome to Idle salon how may I help you`\n } else if (intent === 'BOOK_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'BOOK_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n conversationState.set(from, { step: 'book_appointment' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n }\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'view_appointments', from })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id store_id')\n .eq('client_id', clientLink.client_id)\n if (apptError) throw apptError\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `hi ${name} looks like you don’t have any upcoming appointments would you like to book one now`\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single()\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`\n }))\n responseMessage = `hi ${name} here are your upcoming appointments\\n${apptDetails.join('\\n')}`\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to change would you like to book a new one instead`\n } else {\n conversationState.set(from, { step: 'change_appointment' })\n responseMessage = `let’s reschedule your ${apptData.service_id} appointment ${name} when would you like it just say something like tomorrow at 2 PM or this Friday at 10 am`\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' })\n responseMessage = `I’d be happy to help with that ${name} which store would you like more details about maybe Idle or Glamour Salon`\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to confirm would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id)\n responseMessage = `thanks ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed`\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id appt_start service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single()\n if (apptError || !apptData) {\n responseMessage = `sorry ${name} I couldn’t find an appointment to cancel would you like to book a new one`\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id)\n responseMessage = `sorry to hear that ${name} your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled`\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false })\n responseMessage = `I’d love to help with your package details ${name} can you please give me your registered phone number like 96460132 so I can look it up`\n } else {\n const lowerMessage = reply.toLowerCase()\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service') || lowerMessage.includes('make a booking')) {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'BOOK_APPOINTMENT' })\n responseMessage = `okay ${name} can I get your details please your phone number and name`\n } else {\n conversationState.set(from, { step: 'book_appointment' })\n responseMessage = `I’d be happy to book an appointment for you ${name} which store would you like to book at maybe Idle or Glamour Salon`\n }\n } else {\n responseMessage = `hello ${name} welcome to Idle salon how may I help you`\n }\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name)\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n })\n console.log(`Reply from ${name} (${from}): ${reply}`)\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`)\n res.status(200).send('Reply processed')\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message)\n res.status(500).send('Error processing reply')\n }\n})\n\nconst port = process.env.PORT || 3001\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`)\n try {\n await sendReminders()\n } catch (error) {\n console.error('Error during reminder sending:', error.message)\n }\n})\n"}, "103160941824e2da0a52c0f000de8214f120fd8a": {"t": 1785767130.7598293, "n": 50723, "h": "fefd8ecb3ce42061", "c": "require('dotenv').config();\nconst express = require('express');\nconst twilio = require('twilio');\nconst { createClient } = require('@supabase/supabase-js');\nconst { OpenAI } = require('openai');\nconst chrono = require('chrono-node');\nconst app = express();\n\napp.use(express.urlencoded({ extended: false }));\napp.use(express.json());\n\n// Initialize Twilio client\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing. Check your .env file.');\n process.exit(1);\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID);\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN);\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER);\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);\n\n// Initialize Supabase client\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing. Check your .env file.');\n process.exit(1);\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY);\n\n// Initialize DeepSeek client (using OpenAI SDK)\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing. Check your .env file.');\n process.exit(1);\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n});\n\n// In-memory store to track conversation state\nconst conversationState = new Map();\n\n// Fetch appointments from Supabase\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `);\n if (error) throw error;\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single();\n if (clientError) throw clientError;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (globalError) throw globalError;\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph };\n }));\n return appointments;\n } catch (error) {\n console.error('Error fetching appointments:', error.message);\n throw error;\n }\n};\n\n// Fetch store information (updated to include packages)\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single();\n if (storeError) throw storeError;\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId);\n if (servicesError) throw servicesError;\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId);\n if (packagesError) throw packagesError;\n\n return { store, services, packages };\n } catch (error) {\n console.error('Error fetching store info:', error.message);\n throw error;\n }\n};\n\n// Create client record\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n // Insert into global_clients\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: parseInt(phoneNumber.slice(3), 10), // Remove +65 and convert to int\n })\n .select('global_client_id')\n .single();\n if (globalError) throw globalError;\n\n // Insert into clients\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n store_id: null\n })\n .select('client_id')\n .single();\n if (clientError) throw clientError;\n\n return { clientId: client.client_id, globalClientId: globalClient.global_client_id };\n } catch (error) {\n console.error('Error creating client record:', error.message);\n throw error;\n }\n};\n\n// Send appointment reminders (1 day before appt_start)\nconst sendReminders = async () => {\n try {\n const today = new Date();\n today.setHours(0, 0, 0, 0); // Reset to start of day\n const appointments = await fetchAppointments();\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start);\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24));\n if (diffDays === 1) { // 1 day before\n try {\n const name = appt.preferred_name || appt.client_name;\n const message = `Hi ${name}, this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist. Reply YES to confirm, NO to cancel, or CHANGE to reschedule.`;\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n });\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`);\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message);\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message);\n throw error;\n }\n};\n\n// Add this before the POST handler\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query);\n res.status(200).send('Webhook endpoint is active. Use POST for WhatsApp messages.');\n});\n\n// Controller for booking appointments\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body;\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00');\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000); // Assuming 1-hour duration for simplicity\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select();\n if (error) throw error;\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] });\n } catch (error) {\n res.status(400).json({ error: error.message });\n }\n});\n\n// Function to rephrase messages using DeepSeek\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `Hi ${name}! It seems I don’t have enough details to assist you yet. Please let me know what you'd like to do, and I’ll be happy to help!`;\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon. Rephrase the following message to sound warm, natural, and human-like, while addressing the customer by their name (${name}). Keep the tone polite and conversational, as if you're speaking directly to the customer. Do not change the core meaning of the message. Do not include any explanatory notes or brackets.\n\n Message: \"${message}\"\n\n Rephrased message:\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in warm and friendly customer communication.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n });\n\n return completion.choices[0].message.content.trim();\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message);\n return message || `Hi ${name}! I’m having trouble processing your request. Please try again or let me know what you need!`;\n }\n}\n\n// Function to parse natural language date and time\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date();\n let dateTime;\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`;\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n const today = now.toISOString().split('T')[0];\n dateTime = new Date(`${today}T${timeStr}:00+08:00`);\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`);\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1));\n else if (dateStr.toLowerCase().includes('today')) dateTime = now;\n else dateTime = new Date(dateStr);\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0);\n }\n } else if (timeStr) {\n dateTime = new Date(now);\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i);\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10);\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0;\n const period = timeParts[3]?.toLowerCase();\n if (period === 'pm' && hours < 12) hours += 12;\n else if (period === 'am' && hours === 12) hours = 0;\n dateTime.setHours(hours, minutes, 0, 0);\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480);\n return dateTime;\n }\n return null;\n}\n\n// Function to check if the time slot is available and within operating hours\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5;\n const { store } = await fetchStoreInfo(storeId);\n const operatingHours = store.store_operating_hours;\n \n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n const apptDay = daysOfWeek[apptStart.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n const storeOpenTime = new Date(apptStart);\n storeOpenTime.setHours(openHour, openMinute, 0, 0);\n const storeCloseTime = new Date(apptStart);\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0);\n\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString());\n \n if (error) throw error;\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n return { isAvailable: true };\n}\n\n// Function to suggest the next available time slot\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n let suggestedTime = new Date(requestedTime);\n let attempts = 0;\n const maxAttempts = 7;\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n let nextTime = new Date(suggestedTime);\n nextTime.setHours(openHour, openMinute, 0, 0);\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0);\n }\n\n const closeTime = new Date(suggestedTime);\n closeTime.setHours(closeHour, closeMinute, 0, 0);\n\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000);\n if (apptEnd <= closeTime) {\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n };\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30);\n }\n }\n \n suggestedTime.setDate(suggestedTime.getDate() + 1);\n suggestedTime.setHours(0, 0, 0, 0);\n attempts++;\n }\n\n return null;\n}\n\n// Function to classify intent and extract details using DeepSeek\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent. The possible intents are:\n - GREETING (e.g., \"Hello\", \"Hi\")\n - BOOK_APPOINTMENT (e.g., \"I need to book an appointment\", \"I want a haircut\", \"I'd like to schedule a service\")\n - CHANGE_APPOINTMENT (e.g., \"I want to change my booking\", \"Can I reschedule\")\n - STORE_INFO (e.g., \"Can I get more details about a salon\", \"What are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\")\n - VIEW_APPOINTMENTS (e.g., \"Can I see my upcoming appointments?\", \"What are my bookings?\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\")\n - CANCEL_APPOINTMENT (e.g., \"NO\")\n - PACKAGE_INQUIRY (e.g., \"Can you tell me about my package?\", \"Is my package still active?\", \"How many sessions are left?\", \"Package details\")\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally, extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\")\n - service_name (e.g., \"haircut\", \"manicure\", or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"Haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n });\n\n let responseContent = completion.choices[0].message.content.trim();\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '');\n }\n\n return JSON.parse(responseContent);\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message);\n const lowerMessage = message.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services')) {\n let infoType = null;\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours';\n else if (lowerMessage.includes('address')) infoType = 'address';\n else if (lowerMessage.includes('services')) infoType = 'services';\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n };\n } else if (lowerMessage.includes('upcoming') || lowerMessage.includes('appointments') || lowerMessage.includes('bookings')) {\n return {\n intent: 'VIEW_APPOINTMENTS',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.toUpperCase() === 'YES') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.toUpperCase() === 'NO') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n}\n\n// Webhook to handle customer replies\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body);\n const reply = req.body.Body;\n const from = req.body.From.replace('whatsapp:', '');\n console.log(`Processing message from ${from}: ${reply}`);\n try {\n let fromWithoutPrefix = from;\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3);\n }\n\n let clientData = null;\n let clientLink = null;\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single();\n \n if (!clientError && existingClient) {\n clientData = existingClient;\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`);\n res.status(500).send('Error fetching client link.');\n return;\n }\n clientLink = existingClientLink;\n }\n\n let responseMessage;\n let name = clientData ? (clientData.preferred_name || clientData.client_name || 'there') : 'there';\n\n const { intent, details } = await classifyIntent(reply);\n\n if (conversationState.has(from)) {\n const state = conversationState.get(from);\n if (state.step === 'selectOption') {\n const { storeId, storeName } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n\n if (reply === '1') {\n responseMessage = `Address for ${storeName}: ${store.store_addressL1}`;\n } else if (reply === '2') {\n const hours = store.store_operating_hours;\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${info.close}`;\n })\n .join('\\n');\n responseMessage = `Operating Hours for ${storeName}:\\n${formattedHours}`;\n } else if (reply === '3') {\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeName}:\\n${serviceList}`;\n } else if (reply === '4') {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate} - ${endDate}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage = `Packages at ${storeName}:\\n${packageList}`;\n } else {\n responseMessage = `Please select an option:\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n conversationState.delete(from);\n } else if (state.step === 'book_appointment') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'book_appointment_service';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That's wonderful—I've found your appointment slot at ${state.storeName}. What service would you love to book today? Maybe a fresh haircut, a relaxing manicure, or something else? Let me know, and I’ll get it all set up for you!`;\n }\n } else {\n responseMessage = `Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'book_appointment_service') {\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId);\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase());\n if (!serviceMatch && details.service_name) {\n const lowerServiceName = details.service_name.toLowerCase();\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n };\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName;\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService));\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name;\n state.serviceId = serviceMatch.service_id;\n state.serviceDuration = serviceMatch.service_duration;\n state.step = 'book_appointment_datetime';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That’s fantastic—your ${state.serviceName} is available at ${state.storeName}! 🎉 When would you like it? Feel free to say something like 'tomorrow at 2 PM' or 'Friday at noon'—I’ll handle the rest!`;\n } else {\n responseMessage = `Oops, I couldn’t find the service \"${details.service_name || reply}\" at ${state.storeName}. Did you mean something like 'haircut' or 'manicure'? Feel free to try again or let me know what you had in mind!`;\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000);\n\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration);\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `Sorry, ${name}, the store is closed on ${apptStart.toLocaleDateString()}.`;\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `Sorry, ${name}, the requested time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours.`;\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `Sorry, ${name}, the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked.`;\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` How about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead? Or let me know another time that works for you!`;\n } else {\n responseMessage += ` I couldn’t find an available slot soon. Could you try a different day, or let me know when you’re free?`;\n }\n } else {\n if (!clientData) {\n state.step = 'collect_client_details';\n state.from = from;\n state.appointmentDetails = {\n storeId: state.storeId,\n storeName: state.storeName,\n serviceId: state.serviceId,\n serviceName: state.serviceName,\n apptStart: apptStart.toISOString(),\n apptEnd: apptEnd.toISOString()\n };\n conversationState.set(from, state);\n responseMessage = `To confirm your booking, I’ll need a few details. Could you please share your name and phone number (e.g., John, 96460132)?`;\n } else {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select();\n if (error) throw error;\n\n responseMessage = `Great news, ${name}! Your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}. Can’t wait to see you!`;\n conversationState.delete(from);\n }\n }\n } else {\n responseMessage = `Hmm, I didn’t quite catch that date or time. Could you please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'? I’m here to help!`;\n }\n } else {\n responseMessage = `When would you like your ${state.serviceName} at ${state.storeName}? Feel free to say something like 'tomorrow at 2 PM' or 'this Friday at 10 am'—I’ll handle the rest!`;\n }\n }\n } else if (state.step === 'collect_client_details') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/);\n if (nameMatch) {\n const clientName = nameMatch[1].trim();\n const phoneNumber = nameMatch[2].trim();\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `The phone number you provided (${phoneNumber}) doesn’t match the number you’re messaging from (${fromWithoutPrefix}). Please use the same number or let me know if this is intentional.`;\n } else {\n const { clientId } = await createClientRecord(from, clientName);\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.appointmentDetails.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientId,\n service_id: state.appointmentDetails.serviceId,\n appt_start: state.appointmentDetails.apptStart,\n appt_end: state.appointmentDetails.apptEnd,\n prepayment_amt: 50\n })\n .select();\n if (error) throw error;\n\n name = clientName;\n responseMessage = `Great news, ${name}! Your ${state.appointmentDetails.serviceName} at ${state.appointmentDetails.storeName} is booked for ${new Date(state.appointmentDetails.apptStart).toLocaleDateString()} at ${new Date(state.appointmentDetails.apptStart).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}. Can’t wait to see you!`;\n conversationState.delete(from);\n }\n } else {\n responseMessage = `I didn’t quite get that. Please provide your name and phone number in the format: Name, Number (e.g., John, 96460132).`;\n }\n } else if (state.step === 'store_info') {\n if (!state.storeName) {\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'selectOption';\n conversationState.set(from, state);\n responseMessage = `I found ${state.storeName}! What would you like to know about it?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n } else {\n responseMessage = `Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'view_appointments') {\n const nameMatch = reply.match(/([a-zA-Z\\s]+),\\s*(\\d+)/);\n if (nameMatch) {\n const clientName = nameMatch[1].trim();\n const phoneNumber = nameMatch[2].trim();\n if (phoneNumber !== fromWithoutPrefix) {\n responseMessage = `The phone number you provided (${phoneNumber}) doesn’t match the number you’re messaging from (${fromWithoutPrefix}). Please use the same number or let me know if this is intentional.`;\n } else {\n const { clientId } = await createClientRecord(from, clientName);\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientId);\n if (apptError) throw apptError;\n\n name = clientName;\n if (!appointments || appointments.length === 0) {\n responseMessage = `Hi ${name}, it looks like you don’t have any upcoming appointments. Would you like to book one now?`;\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single();\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`;\n }));\n responseMessage = `Hi ${name}, here are your upcoming appointments:\\n${apptDetails.join('\\n')}`;\n }\n conversationState.delete(from);\n }\n } else {\n responseMessage = `I didn’t quite get that. Please provide your name and phone number in the format: Name, Number (e.g., John, 96460132).`;\n }\n } else if (state.step === 'change_appointment') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n conversationState.delete(from);\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start));\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `All set, ${name}! Your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}.`;\n conversationState.delete(from);\n } else {\n responseMessage = `I didn’t quite get that date or time. Please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'.`;\n }\n } else {\n responseMessage = `When would you like to reschedule your ${apptData.service_id} appointment? Just let me know with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n state.phoneNumberRequested = true;\n conversationState.set(from, state);\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n const phoneNumber = reply;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single();\n if (globalError || !globalClient) {\n responseMessage = `Sorry, ${name}, I couldn’t find a client with the phone number ${phoneNumber}. Please try again with your registered number or contact support.`;\n conversationState.delete(from);\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id);\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `It looks like you don’t have any active packages, ${name}. Would you like to purchase one?`;\n conversationState.delete(from);\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single();\n if (storeError) throw storeError;\n\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single();\n if (packageInfoError) throw packageInfoError;\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString();\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString();\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString();\n\n return `Package: ${packageInfo.package_name} at ${store.store_name}\\n` +\n `Description: ${packageInfo.package_description}\\n` +\n `Price: $${packageInfo.package_price}\\n` +\n `Total Sessions: ${packageInfo.num_sessions}, Sessions Remaining: ${pkg.session_remaining}\\n` +\n `Total Credits: ${packageInfo.num_credits}, Credits Remaining: ${pkg.credits_remaining}\\n` +\n `Purchased On: ${purchaseDate}\\n` +\n `Valid From: ${packageStart} to ${packageEnd}\\n` +\n `Status: ${pkg.status}`;\n }));\n responseMessage = `Here are your package details, ${name}:\\n${packageDetails.join('\\n\\n')}\\nLet me know if you need more information!`;\n conversationState.delete(from);\n }\n }\n }\n }\n } else {\n if (intent === 'GREETING') {\n responseMessage = `Hi ${name}, how can I help you today? You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- See your upcoming appointments (e.g., \"Can I see my upcoming appointments?\")\\nWhat would you like to do?`;\n } else if (intent === 'BOOK_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'BOOK_APPOINTMENT' });\n responseMessage = `To book an appointment, I’ll need a few details. Could you please share your name and phone number (e.g., John, 96460132)?`;\n } else {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n }\n } else if (intent === 'VIEW_APPOINTMENTS') {\n if (!clientData) {\n conversationState.set(from, { step: 'view_appointments', from });\n responseMessage = `To view your appointments, I’ll need a few details. Could you please share your name and phone number (e.g., John, 96460132)?`;\n } else {\n const { data: appointments, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id, store_id')\n .eq('client_id', clientLink.client_id);\n if (apptError) throw apptError;\n\n if (!appointments || appointments.length === 0) {\n responseMessage = `Hi ${name}, it looks like you don’t have any upcoming appointments. Would you like to book one now?`;\n } else {\n const apptDetails = await Promise.all(appointments.map(async (appt) => {\n const { data: store } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', appt.store_id)\n .single();\n return `${appt.service_id} at ${store.store_name} on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}`;\n }));\n responseMessage = `Hi ${name}, here are your upcoming appointments:\\n${apptDetails.join('\\n')}`;\n }\n }\n } else if (intent === 'CHANGE_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CHANGE_APPOINTMENT' });\n responseMessage = `To change your appointment, I’ll need a few details. Could you please share your name and phone number (e.g., John, 96460132)?`;\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n } else {\n conversationState.set(from, { step: 'change_appointment' });\n responseMessage = `Let's reschedule your ${apptData.service_id} appointment, ${name}. When would you like it? Try something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' });\n responseMessage = `I'd be happy to help with that, ${name}! Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CONFIRM_APPOINTMENT' });\n responseMessage = `To confirm your appointment, I’ll need a few details. Could you please share your name and phone number (e.g., John, 96460132)?`;\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to confirm for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Thank you, ${name}! Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed.`;\n }\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'CANCEL_APPOINTMENT' });\n responseMessage = `To cancel your appointment, I’ll need a few details. Could you please share your name and phone number (e.g., John, 96460132)?`;\n } else {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to cancel for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Sorry to hear that, ${name}. Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled.`;\n }\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false });\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n const lowerMessage = reply.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n if (!clientData) {\n conversationState.set(from, { step: 'collect_client_details', from, intent: 'BOOK_APPOINTMENT' });\n responseMessage = `To book an appointment, I’ll need a few details. Could you please share your name and phone number (e.g., John, 96460132)?`;\n } else {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n }\n } else {\n responseMessage = `Hi ${name}, how can I help you today? You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- See your upcoming appointments (e.g., \"Can I see my upcoming appointments?\")\\nWhat would you like to do?`;\n }\n }\n }\n\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name);\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:${from}`,\n body: rephrasedMessage\n });\n console.log(`Reply from ${name} (${from}): ${reply}`);\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`);\n res.status(200).send('Reply processed.');\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message);\n res.status(500).send('Error processing reply.');\n }\n});\n\n// Start the server and send reminders\nconst port = process.env.PORT || 3001;\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`);\n try {\n await sendReminders();\n } catch (error) {\n console.error('Error during reminder sending:', error.message);\n }\n});\n"}, "8089199e3dc5b4577d358d7512d7c68f77f40460": {"t": 1785767132.8990152, "n": 48159, "h": "1c525e289d1696d7", "c": "require('dotenv').config();\nconst express = require('express');\nconst twilio = require('twilio');\nconst { createClient } = require('@supabase/supabase-js');\nconst { OpenAI } = require('openai');\nconst chrono = require('chrono-node'); // Kept as per your request\nconst app = express();\n\napp.use(express.urlencoded({ extended: false }));\napp.use(express.json());\n\n// Initialize Twilio client\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing. Check your .env file.');\n process.exit(1);\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID);\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN);\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER);\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);\n\n// Initialize Supabase client\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing. Check your .env file.');\n process.exit(1);\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY);\n\n// Initialize DeepSeek client (using OpenAI SDK)\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing. Check your .env file.');\n process.exit(1);\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n});\n\n// In-memory store to track conversation state\nconst conversationState = new Map();\n\n// Fetch appointments from Supabase\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `);\n if (error) throw error;\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single();\n if (clientError) throw clientError;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (globalError) throw globalError;\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph };\n }));\n return appointments;\n } catch (error) {\n console.error('Error fetching appointments:', error.message);\n throw error;\n }\n};\n\n// Fetch store information (updated to include packages)\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single();\n if (storeError) throw storeError;\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId);\n if (servicesError) throw servicesError;\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId);\n if (packagesError) throw packagesError;\n\n return { store, services, packages };\n } catch (error) {\n console.error('Error fetching store info:', error.message);\n throw error;\n }\n};\n\n// Create client record\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: phoneNumber.slice(3), // Remove +65\n })\n .select('global_client_id')\n .single();\n if (globalError) throw globalError;\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n })\n .select('client_id')\n .single();\n if (clientError) throw clientError;\n\n return client.client_id;\n } catch (error) {\n console.error('Error creating client record:', error.message);\n throw error;\n }\n};\n\n// Send appointment reminders (1 day before appt_start)\nconst sendReminders = async () => {\n try {\n const today = new Date();\n today.setHours(0, 0, 0, 0); // Reset to start of day\n const appointments = await fetchAppointments();\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start);\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24));\n if (diffDays === 1) { // 1 day before\n try {\n const name = appt.preferred_name || appt.client_name;\n const message = `Hi ${name}, this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist. Reply YES to confirm, NO to cancel, or CHANGE to reschedule.`;\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n });\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`);\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message);\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message);\n throw error;\n }\n};\n\n// Add this before the POST handler\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query);\n res.status(200).send('Webhook endpoint is active. Use POST for WhatsApp messages.');\n});\n\n// Controller for booking appointments\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body;\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00');\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000); // Assuming 1-hour duration for simplicity\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select();\n if (error) throw error;\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] });\n } catch (error) {\n res.status(400).json({ error: error.message });\n }\n});\n\n// Function to rephrase messages using DeepSeek\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `Hi ${name}! It seems I don’t have enough details to assist you yet. Please let me know what you'd like to do, and I’ll be happy to help!`;\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon. Rephrase the following message to sound warm, natural, and human-like, while addressing the customer by their name (${name}). Keep the tone polite and conversational, as if you're speaking directly to the customer. Do not change the core meaning of the message. Do not include any explanatory notes or brackets.\n\n Message: \"${message}\"\n\n Rephrased message:\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in warm and friendly customer communication.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n });\n\n return completion.choices[0].message.content.trim();\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message);\n return message || `Hi ${name}! I’m having trouble processing your request. Please try again or let me know what you need!`;\n }\n}\n\n// Function to parse natural language date and time\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date(); // Updated to use current date\n let dateTime;\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`;\n // Use chrono-node with English locale only to avoid Japanese locale issues\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Fallback to basic parsing if chrono fails\n const today = now.toISOString().split('T')[0];\n dateTime = new Date(`${today}T${timeStr}:00+08:00`);\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`);\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Handle relative dates\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1));\n else if (dateStr.toLowerCase().includes('today')) dateTime = now;\n else dateTime = new Date(dateStr);\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0); // Default to midnight if no time\n }\n } else if (timeStr) {\n dateTime = new Date(now);\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i);\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10);\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0;\n const period = timeParts[3]?.toLowerCase();\n if (period === 'pm' && hours < 12) hours += 12;\n else if (period === 'am' && hours === 12) hours = 0;\n dateTime.setHours(hours, minutes, 0, 0);\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n // Ensure timezone is +08:00\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480);\n return dateTime;\n }\n return null;\n}\n\n// Function to check if the time slot is available and within operating hours\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5; // Hardcoded max appointments per slot\n const { store } = await fetchStoreInfo(storeId);\n const operatingHours = store.store_operating_hours;\n \n // Get the day of the week for the appointment\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n const apptDay = daysOfWeek[apptStart.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n // Check if the store is closed\n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Parse store opening and closing times\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n const storeOpenTime = new Date(apptStart);\n storeOpenTime.setHours(openHour, openMinute, 0, 0);\n const storeCloseTime = new Date(apptStart);\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Check if the appointment is within operating hours\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Check for overlapping appointments\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString());\n \n if (error) throw error;\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n return { isAvailable: true };\n}\n\n// Function to suggest the next available time slot\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n let suggestedTime = new Date(requestedTime);\n let attempts = 0;\n const maxAttempts = 7; // Try up to 7 days ahead\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n // Start from opening time or requested time if later\n let nextTime = new Date(suggestedTime);\n nextTime.setHours(openHour, openMinute, 0, 0);\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0);\n }\n\n const closeTime = new Date(suggestedTime);\n closeTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Increment by 30 minutes until a slot is found or we pass closing time\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000);\n if (apptEnd <= closeTime) {\n // Check if this slot is available (mock check since we can't call async here)\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n };\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30);\n }\n }\n \n // Move to the next day\n suggestedTime.setDate(suggestedTime.getDate() + 1);\n suggestedTime.setHours(0, 0, 0, 0); // Reset to start of day\n attempts++;\n }\n\n // If no slot is found within 7 days, suggest contacting support\n return null;\n}\n\n// Function to classify intent and extract details using DeepSeek\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent. The possible intents are:\n - GREETING (e.g., \"Hello\", \"Hi\")\n - BOOK_APPOINTMENT (e.g., \"I need to book an appointment\", \"I want a haircut\", \"I'd like to schedule a service\")\n - CHANGE_APPOINTMENT (e.g., \"I want to change my booking\", \"Can I reschedule\")\n - STORE_INFO (e.g., \"Can I get more details about a salon\", \"What are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\")\n - CANCEL_APPOINTMENT (e.g., \"NO\")\n - PACKAGE_INQUIRY (e.g., \"Can you tell me about my package?\", \"Is my package still active?\", \"How many sessions are left?\", \"Package details\")\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally, extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\")\n - service_name (e.g., \"haircut\", \"manicure\", or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"Haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n });\n\n let responseContent = completion.choices[0].message.content.trim();\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '');\n }\n\n return JSON.parse(responseContent);\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message);\n // Fallback to keyword-based intent classification\n const lowerMessage = message.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services')) {\n let infoType = null;\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours';\n else if (lowerMessage.includes('address')) infoType = 'address';\n else if (lowerMessage.includes('services')) infoType = 'services';\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n };\n } else if (lowerMessage.toUpperCase() === 'YES') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.toUpperCase() === 'NO') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n}\n\n// Webhook to handle customer replies\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body); // Log the incoming request\n const reply = req.body.Body;\n const from = req.body.From.replace('whatsapp:', '');\n console.log(`Processing message from ${from}: ${reply}`);\n try {\n // Hardcode remove +65 from incoming number for client_ph query\n let fromWithoutPrefix = from;\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3); // Remove +65 to get 96460132\n }\n\n // Check if client exists\n let clientData = null;\n let clientLink = null;\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single();\n \n if (clientError || !existingClient) {\n console.log(`No matching client data for ${fromWithoutPrefix}, creating new client.`);\n // Create a new client record\n const clientId = await createClientRecord(from, 'Unknown'); // Default name as 'Unknown'\n // Fetch the newly created client data\n const { data: newClient, error: newClientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single();\n if (newClientError || !newClient) {\n console.error(`Failed to fetch newly created client for ${fromWithoutPrefix}: ${newClientError?.message}`);\n res.status(500).send('Error creating new client.');\n return;\n }\n clientData = newClient;\n\n const { data: newClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (linkError || !newClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`);\n res.status(500).send('Error linking new client.');\n return;\n }\n clientLink = newClientLink;\n } else {\n clientData = existingClient;\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`);\n res.status(500).send('Error fetching client link.');\n return;\n }\n clientLink = existingClientLink;\n }\n\n let responseMessage;\n const name = clientData.preferred_name || clientData.client_name;\n\n // Classify the customer's intent using DeepSeek\n const { intent, details } = await classifyIntent(reply);\n\n // Check if we're in a conversation state\n if (conversationState.has(from)) {\n const state = conversationState.get(from);\n if (state.step === 'selectOption') {\n // Handle store info sub-menu\n const { storeId, storeName } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n\n if (reply === '1') {\n // Store Address\n responseMessage = `Address for ${storeName}: ${store.store_addressL1}`;\n } else if (reply === '2') {\n // Operating Hours\n const hours = store.store_operating_hours;\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${info.close}`;\n })\n .join('\\n');\n responseMessage = `Operating Hours for ${storeName}:\\n${formattedHours}`;\n } else if (reply === '3') {\n // Services\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeName}:\\n${serviceList}`;\n } else if (reply === '4') {\n // Packages\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate} - ${endDate}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage = `Packages at ${storeName}:\\n${packageList}`;\n } else {\n responseMessage = `Please select an option:\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n conversationState.delete(from); // Reset state\n } else if (state.step === 'book_appointment') {\n // Handle booking flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'book_appointment_service';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That's wonderful—I've found your appointment slot at ${state.storeName}. What service would you love to book today? Maybe a fresh haircut, a relaxing manicure, or something else? Let me know, and I’ll get it all set up for you!`;\n }\n } else {\n responseMessage = `Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'book_appointment_service') {\n // Handle booking flow - Service selection\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId);\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase());\n if (!serviceMatch && details.service_name) {\n // Try to match informal terms or partial names\n const lowerServiceName = details.service_name.toLowerCase();\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n };\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName;\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService));\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name;\n state.serviceId = serviceMatch.service_id;\n state.serviceDuration = serviceMatch.service_duration;\n state.step = 'book_appointment_datetime';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That’s fantastic—your ${state.serviceName} is available at ${state.storeName}! 🎉 When would you like it? Feel free to say something like 'tomorrow at 2 PM' or 'Friday at noon'—I’ll handle the rest!`;\n } else {\n responseMessage = `Oops, I couldn’t find the service \"${details.service_name || reply}\" at ${state.storeName}. Did you mean something like 'haircut' or 'manicure'? Feel free to try again or let me know what you had in mind!`;\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n // Handle booking flow - Date and time selection\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000);\n\n // Check availability and operating hours\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration);\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `Sorry, ${name}, the store is closed on ${apptStart.toLocaleDateString()}.`;\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `Sorry, ${name}, the requested time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours.`;\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `Sorry, ${name}, the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked.`;\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` How about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead? Or let me know another time that works for you!`;\n } else {\n responseMessage += ` I couldn’t find an available slot soon. Could you try a different day, or let me know when you’re free?`;\n }\n } else {\n // Slot is available, proceed with booking\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select();\n if (error) throw error;\n\n responseMessage = `Great news, ${name}! Your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}. Can’t wait to see you!`;\n conversationState.delete(from); // Reset state\n }\n } else {\n responseMessage = `Hmm, I didn’t quite catch that date or time. Could you please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'? I’m here to help!`;\n }\n } else {\n responseMessage = `When would you like your ${state.serviceName} at ${state.storeName}? Feel free to say something like 'tomorrow at 2 PM' or 'this Friday at 10 am'—I’ll handle the rest!`;\n }\n }\n } else if (state.step === 'store_info') {\n // Handle store info flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'selectOption';\n conversationState.set(from, state);\n responseMessage = `I found ${state.storeName}! What would you like to know about it?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n } else {\n responseMessage = `Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'store_info_details') {\n // Handle store info flow - Details selection\n const { storeId, storeName } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n const lowerReply = reply.toLowerCase();\n if (!state.infoType) {\n if (details.info_type === 'address' || lowerReply.includes('address')) {\n responseMessage = `Address for ${storeName}:\\n${store.store_addressL1}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'hours' || lowerReply.includes('hours') || lowerReply.includes('schedule')) {\n const hours = store.store_operating_hours;\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${info.close}`;\n })\n .join('\\n');\n responseMessage = `Operating Hours for ${storeName}:\\n${formattedHours}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'services' || lowerReply.includes('services')) {\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeName}:\\n${serviceList}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'packages' || lowerReply.includes('packages')) {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate} - ${endDate}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage = `Packages at ${storeName}:\\n${packageList}`;\n conversationState.delete(from); // Reset state\n } else {\n responseMessage = `What would you like to know about ${storeName}?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n }\n } else if (state.step === 'change_appointment') {\n // Handle change appointment flow\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n conversationState.delete(from); // Reset state\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start)); // Preserve original duration\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `All set, ${name}! Your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}.`;\n conversationState.delete(from); // Reset state\n } else {\n responseMessage = `I didn’t quite get that date or time. Please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'.`;\n }\n } else {\n responseMessage = `When would you like to reschedule your ${apptData.service_id} appointment? Just let me know with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n // Ask for phone number\n state.phoneNumberRequested = true;\n conversationState.set(from, state);\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Process the provided phone number\n const phoneNumber = reply;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single();\n if (globalError || !globalClient) {\n responseMessage = `Sorry, ${name}, I couldn’t find a client with the phone number ${phoneNumber}. Please try again with your registered number or contact support.`;\n conversationState.delete(from); // Reset state\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id);\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `It looks like you don’t have any active packages, ${name}. Would you like to purchase one?`;\n conversationState.delete(from); // Reset state\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n // Fetch store name from store_management.stores\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single();\n if (storeError) throw storeError;\n\n // Fetch package details from store_management.packages\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single();\n if (packageInfoError) throw packageInfoError;\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString();\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString();\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString();\n\n return `Package: ${packageInfo.package_name} at ${store.store_name}\\n` +\n `Description: ${packageInfo.package_description}\\n` +\n `Price: $${packageInfo.package_price}\\n` +\n `Total Sessions: ${packageInfo.num_sessions}, Sessions Remaining: ${pkg.session_remaining}\\n` +\n `Total Credits: ${packageInfo.num_credits}, Credits Remaining: ${pkg.credits_remaining}\\n` +\n `Purchased On: ${purchaseDate}\\n` +\n `Valid From: ${packageStart} to ${packageEnd}\\n` +\n `Status: ${pkg.status}`;\n }));\n responseMessage = `Here are your package details, ${name}:\\n${packageDetails.join('\\n\\n')}\\nLet me know if you need more information!`;\n conversationState.delete(from); // Reset state\n }\n }\n }\n }\n } else {\n // Handle intents when not in a conversation state\n if (intent === 'GREETING') {\n responseMessage = `Hello ${name}! How can I help you today? You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nJust let me know what you'd like!`;\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n } else if (intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n } else {\n conversationState.set(from, { step: 'change_appointment' });\n responseMessage = `Let's reschedule your ${apptData.service_id} appointment, ${name}. When would you like it? Try something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' });\n responseMessage = `I'd be happy to help with that, ${name}! Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to confirm for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Thank you, ${name}! Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed.`;\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to cancel for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Sorry to hear that, ${name}. Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled.`;\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false });\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Fallback for unknown intents\n const lowerMessage = reply.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n } else {\n responseMessage = `I'm not sure how to help with that, ${name}. You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nWhat would you like to do?`;\n }\n }\n }\n\n // Rephrase the response using DeepSeek\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name);\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${clientData.client_ph}`,\n body: rephrasedMessage\n });\n console.log(`Reply from ${name} (${from}): ${reply}`);\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`);\n res.status(200).send('Reply processed.');\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message);\n res.status(500).send('Error processing reply.');\n }\n});\n\n// Start the server and send reminders\nconst port = process.env.PORT || 3001;\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`);\n try {\n await sendReminders();\n } catch (error) {\n console.error('Error during reminder sending:', error.message);\n }\n});\n"}, "0435d98d9941b1a91591a281d560abf3f521e348": {"t": 1785767136.0894597, "n": 48093, "h": "cb615c5c4a59289e", "c": "require('dotenv').config();\nconst express = require('express');\nconst twilio = require('twilio');\nconst { createClient } = require('@supabase/supabase-js');\nconst { OpenAI } = require('openai');\nconst chrono = require('chrono-node'); // Kept as per your request\nconst app = express();\n\napp.use(express.urlencoded({ extended: false }));\napp.use(express.json());\n\n// Initialize Twilio client\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing. Check your .env file.');\n process.exit(1);\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID);\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN);\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER);\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);\n\n// Initialize Supabase client\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing. Check your .env file.');\n process.exit(1);\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY);\n\n// Initialize DeepSeek client (using OpenAI SDK)\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing. Check your .env file.');\n process.exit(1);\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n});\n\n// In-memory store to track conversation state\nconst conversationState = new Map();\n\n// Fetch appointments from Supabase\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `);\n if (error) throw error;\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single();\n if (clientError) throw clientError;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (globalError) throw globalError;\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph };\n }));\n return appointments;\n } catch (error) {\n console.error('Error fetching appointments:', error.message);\n throw error;\n }\n};\n\n// Fetch store information (updated to include packages)\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single();\n if (storeError) throw storeError;\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId);\n if (servicesError) throw servicesError;\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId);\n if (packagesError) throw packagesError;\n\n return { store, services, packages };\n } catch (error) {\n console.error('Error fetching store info:', error.message);\n throw error;\n }\n};\n\n// Create client record\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: phoneNumber.slice(3), // Remove +65\n })\n .select('global_client_id')\n .single();\n if (globalError) throw globalError;\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n })\n .select('client_id')\n .single();\n if (clientError) throw clientError;\n\n return client.client_id;\n } catch (error) {\n console.error('Error creating client record:', error.message);\n throw error;\n }\n};\n\n// Send appointment reminders (1 day before appt_start)\nconst sendReminders = async () => {\n try {\n const today = new Date();\n today.setHours(0, 0, 0, 0); // Reset to start of day\n const appointments = await fetchAppointments();\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start);\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24));\n if (diffDays === 1) { // 1 day before\n try {\n const name = appt.preferred_name || appt.client_name;\n const message = `Hi ${name}, this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist. Reply YES to confirm, NO to cancel, or CHANGE to reschedule.`;\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n });\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`);\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message);\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message);\n throw error;\n }\n};\n\n// Add this before the POST handler\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query);\n res.status(200).send('Webhook endpoint is active. Use POST for WhatsApp messages.');\n});\n\n// Controller for booking appointments\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body;\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00');\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000); // Assuming 1-hour duration for simplicity\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select();\n if (error) throw error;\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] });\n } catch (error) {\n res.status(400).json({ error: error.message });\n }\n});\n\n// Function to rephrase messages using DeepSeek\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `Hi ${name}! It seems I don’t have enough details to assist you yet. Please let me know what you'd like to do, and I’ll be happy to help!`;\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon. Rephrase the following message to sound warm, natural, and human-like, while addressing the customer by their name (${name}). Keep the tone polite and conversational, as if you're speaking directly to the customer. Do not change the core meaning of the message. Do not include any explanatory notes or brackets.\n\n Message: \"${message}\"\n\n Rephrased message:\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in warm and friendly customer communication.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n });\n\n return completion.choices[0].message.content.trim();\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message);\n return message || `Hi ${name}! I’m having trouble processing your request. Please try again or let me know what you need!`;\n }\n}\n\n// Function to parse natural language date and time\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date(); // Updated to use current date\n let dateTime;\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`;\n // Use chrono-node with English locale only to avoid Japanese locale issues\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Fallback to basic parsing if chrono fails\n const today = now.toISOString().split('T')[0];\n dateTime = new Date(`${today}T${timeStr}:00+08:00`);\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`);\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Handle relative dates\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1));\n else if (dateStr.toLowerCase().includes('today')) dateTime = now;\n else dateTime = new Date(dateStr);\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0); // Default to midnight if no time\n }\n } else if (timeStr) {\n dateTime = new Date(now);\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i);\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10);\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0;\n const period = timeParts[3]?.toLowerCase();\n if (period === 'pm' && hours < 12) hours += 12;\n else if (period === 'am' && hours === 12) hours = 0;\n dateTime.setHours(hours, minutes, 0, 0);\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n // Ensure timezone is +08:00\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480);\n return dateTime;\n }\n return null;\n}\n\n// Function to check if the time slot is available and within operating hours\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5; // Hardcoded max appointments per slot\n const { store } = await fetchStoreInfo(storeId);\n const operatingHours = store.store_operating_hours;\n \n // Get the day of the week for the appointment\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n const apptDay = daysOfWeek[apptStart.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n // Check if the store is closed\n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Parse store opening and closing times\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n const storeOpenTime = new Date(apptStart);\n storeOpenTime.setHours(openHour, openMinute, 0, 0);\n const storeCloseTime = new Date(apptStart);\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Check if the appointment is within operating hours\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Check for overlapping appointments\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString());\n \n if (error) throw error;\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n return { isAvailable: true };\n}\n\n// Function to suggest the next available time slot\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n let suggestedTime = new Date(requestedTime);\n let attempts = 0;\n const maxAttempts = 7; // Try up to 7 days ahead\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n // Start from opening time or requested time if later\n let nextTime = new Date(suggestedTime);\n nextTime.setHours(openHour, openMinute, 0, 0);\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0);\n }\n\n const closeTime = new Date(suggestedTime);\n closeTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Increment by 30 minutes until a slot is found or we pass closing time\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000);\n if (apptEnd <= closeTime) {\n // Check if this slot is available (mock check since we can't call async here)\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n };\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30);\n }\n }\n \n // Move to the next day\n suggestedTime.setDate(suggestedTime.getDate() + 1);\n suggestedTime.setHours(0, 0, 0, 0); // Reset to start of day\n attempts++;\n }\n\n // If no slot is found within 7 days, suggest contacting support\n return null;\n}\n\n// Function to classify intent and extract details using DeepSeek\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent. The possible intents are:\n - GREETING (e.g., \"Hello\", \"Hi\")\n - BOOK_APPOINTMENT (e.g., \"I need to book an appointment\", \"I want a haircut\", \"I'd like to schedule a service\")\n - CHANGE_APPOINTMENT (e.g., \"I want to change my booking\", \"Can I reschedule\")\n - STORE_INFO (e.g., \"Can I get more details about a salon\", \"What are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\")\n - CANCEL_APPOINTMENT (e.g., \"NO\")\n - PACKAGE_INQUIRY (e.g., \"Can you tell me about my package?\", \"Is my package still active?\", \"How many sessions are left?\", \"Package details\")\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally, extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\")\n - service_name (e.g., \"haircut\", \"manicure\", or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"Haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n });\n\n let responseContent = completion.choices[0].message.content.trim();\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '');\n }\n\n return JSON.parse(responseContent);\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message);\n // Fallback to keyword-based intent classification\n const lowerMessage = message.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services')) {\n let infoType = null;\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours';\n else if (lowerMessage.includes('address')) infoType = 'address';\n else if (lowerMessage.includes('services')) infoType = 'services';\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n };\n } else if (lowerMessage.toUpperCase() === 'YES') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.toUpperCase() === 'NO') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n}\n\n// Webhook to handle customer replies\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body); // Log the incoming request\n const reply = req.body.Body;\n const from = req.body.From.replace('whatsapp:', '');\n console.log(`Processing message from ${from}: ${reply}`);\n try {\n // Hardcode remove +65 from incoming number for client_ph query\n let fromWithoutPrefix = from;\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3); // Remove +65 to get 96460132\n }\n\n // Check if client exists\n let clientData = null;\n let clientLink = null;\n const { data: existingClient, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single();\n \n if (clientError || !existingClient) {\n console.log(`No matching client data for ${fromWithoutPrefix}, creating new client.`);\n // Create a new client record\n const clientId = await createClientRecord(from, 'Unknown'); // Default name as 'Unknown'\n // Fetch the newly created client data\n const { data: newClient, error: newClientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single();\n if (newClientError || !newClient) {\n console.error(`Failed to fetch newly created client for ${fromWithoutPrefix}: ${newClientError?.message}`);\n res.status(500).send('Error creating new client.');\n return;\n }\n clientData = newClient;\n\n const { data: newClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (linkError || !newClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`);\n res.status(500).send('Error linking new client.');\n return;\n }\n clientLink = newClientLink;\n } else {\n clientData = existingClient;\n const { data: existingClientLink, error: linkError } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (linkError || !existingClientLink) {\n console.error(`No client link found for global_client_id ${clientData.global_client_id}: ${linkError?.message}`);\n res.status(500).send('Error fetching client link.');\n return;\n }\n clientLink = existingClientLink;\n }\n\n let responseMessage;\n const name = clientData.preferred_name || clientData.client_name;\n\n // Classify the customer's intent using DeepSeek\n const { intent, details } = await classifyIntent(reply);\n\n // Check if we're in a conversation state\n if (conversationState.has(from)) {\n const state = conversationState.get(from);\n if (state.step === 'selectOption') {\n // Handle store info sub-menu\n const { storeId, storeName } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n\n if (reply === '1') {\n // Store Address\n responseMessage = `Address for ${storeName}: ${store.store_addressL1}`;\n } else if (reply === '2') {\n // Operating Hours\n const hours = store.store_operating_hours;\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${info.close}`;\n })\n .join('\\n');\n responseMessage = `Operating Hours for ${storeName}:\\n${formattedHours}`;\n } else if (reply === '3') {\n // Services\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeName}:\\n${serviceList}`;\n } else if (reply === '4') {\n // Packages\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate} - ${endDate}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage = `Packages at ${storeName}:\\n${packageList}`;\n } else {\n responseMessage = `Please select an option:\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n conversationState.delete(from); // Reset state\n } else if (state.step === 'book_appointment') {\n // Handle booking flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'book_appointment_service';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That's wonderful—I've found your appointment slot at ${state.storeName}. What service would you love to book today? Maybe a fresh haircut, a relaxing manicure, or something else? Let me know, and I’ll get it all set up for you!`;\n }\n } else {\n responseMessage = `Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'book_appointment_service') {\n // Handle booking flow - Service selection\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId);\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase());\n if (!serviceMatch && details.service_name) {\n // Try to match informal terms or partial names\n const lowerServiceName = details.service_name.toLowerCase();\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n };\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName;\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService));\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name;\n state.serviceId = serviceMatch.service_id;\n state.serviceDuration = serviceMatch.service_duration;\n state.step = 'book_appointment_datetime';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That’s fantastic—your ${state.serviceName} is available at ${state.storeName}! 🎉 When would you like it? Feel free to say something like 'tomorrow at 2 PM' or 'Friday at noon'—I’ll handle the rest!`;\n } else {\n responseMessage = `Oops, I couldn’t find the service \"${details.service_name || reply}\" at ${state.storeName}. Did you mean something like 'haircut' or 'manicure'? Feel free to try again or let me know what you had in mind!`;\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n // Handle booking flow - Date and time selection\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000);\n\n // Check availability and operating hours\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration);\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `Sorry, ${name}, the store is closed on ${apptStart.toLocaleDateString()}.`;\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `Sorry, ${name}, the requested time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours.`;\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `Sorry, ${name}, the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked.`;\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` How about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead? Or let me know another time that works for you!`;\n } else {\n responseMessage += ` I couldn’t find an available slot soon. Could you try a different day, or let me know when you’re free?`;\n }\n } else {\n // Slot is available, proceed with booking\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select();\n if (error) throw error;\n\n responseMessage = `Great news, ${name}! Your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}. Can’t wait to see you!`;\n conversationState.delete(from); // Reset state\n }\n } else {\n responseMessage = `Hmm, I didn’t quite catch that date or time. Could you please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'? I’m here to help!`;\n }\n } else {\n responseMessage = `When would you like your ${state.serviceName} at ${state.storeName}? Feel free to say something like 'tomorrow at 2 PM' or 'this Friday at 10 am'—I’ll handle the rest!`;\n }\n }\n } else if (state.step === 'store_info') {\n // Handle store info flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'selectOption';\n conversationState.set(from, state);\n responseMessage = `I found ${state.storeName}! What would you like to know about it?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n } else {\n responseMessage = `Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'store_info_details') {\n // Handle store info flow - Details selection\n const { storeId, name } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n const lowerReply = reply.toLowerCase();\n if (!state.infoType) {\n if (details.info_type === 'address' || lowerReply.includes('address')) {\n responseMessage = `Address for ${storeId}:\\n${store.store_addressL1}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'hours || lowerMessage.includes('hours') || lowerMessage.includes('schedule')) {\n const hours = store.store_operating_hours;\n const formattedHours = hours.map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${hour}: ${info.close}`;\n }).join('\\n');\n responseMessage = `Operating Hours for ${storeId}:\\n${formattedHours}`;\n conversationState.delete(from);\n } else if (details.info_type || lowerReply.includes('services')) {\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeId}:\\n${serviceList}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'packages' || lowerPackage.includes('packages')) {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate.toISOString()} - ${endDate.toISOString()}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage.delete(`Packages at ${storeId}:\\n${packageList}`);\n } else {\n responseMessage = `What would you like to know about ${storeId}?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n }\n } else if (state.step === 'change_appointment') {\n // Handle change appointment flow\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n conversationState.delete(from); // Reset state\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start)); // Preserve original duration\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `All set, ${name}! Your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}.`;\n conversationState.delete(from); // Reset state\n } else {\n responseMessage = `I didn’t quite get that date or time. Please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'.`;\n }\n } else {\n responseMessage = `When would you like to reschedule your ${apptData.service_id} appointment? Just let me know with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n // Ask for phone number\n state.phoneNumberRequested = true;\n conversationState.set(from, state);\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Process the provided phone number\n const phoneNumber = reply;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single();\n if (globalError || !globalClient) {\n responseMessage = `Sorry, ${name}, I couldn’t find a client with the phone number ${phoneNumber}. Please try again with your registered number or contact support.`;\n conversationState.delete(from); // Reset state\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id);\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `It looks like you don’t have any active packages, ${name}. Would you like to purchase one?`;\n conversationState.delete(from); // Reset state\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n // Fetch store name from store_management.stores\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single();\n if (storeError) throw storeError;\n\n // Fetch package details from store_management.packages\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single();\n if (packageInfoError) throw packageInfoError;\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString();\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString();\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString();\n\n return `Package: ${packageInfo.package_name} at ${store.store_name}\\n` +\n `Description: ${packageInfo.package_description}\\n` +\n `rPrice: ${packageInfo.package_price}\\n` +\n `Total Sessions: ${packageInfo.num_sessions}, Sessions Remaining: ${pkg.session_remaining}\\n` +\n `Total Credits: ${packageInfo.num_credits}, Credits Remaining: ${pkg.credits_remaining}\\n` +\n `Purchased On: ${purchaseDate.toString()}\\n` +\n `Valid From: ${packageStart.toISOString()} to ${packageEnd.toISOString()}\\n` +\n `Status: ${pkg.status}`;\n }));\n responseMessage = `Here are your package details, ${name}:\\n${packageDetails.join('\\n\\n')}\\nLet me know if you need more information!`;\n conversationState.delete(from); // Reset state\n }\n }\n }\n }\n } else {\n // Handle intents when not in a conversation state\n if (intent === 'GREETING') {\n responseMessage = `Hello ${name}! How can I help you today? You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nJust let me know what you'd like!`;\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Id)?`;\n } else if (intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n } else {\n conversationState.set(from, { step: 'change_appointment' });\n responseMessage = `Let's reschedule your ${apptData.service_id} appointment, ${name}. When would you like it? Try something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' });\n responseMessage = `I'd be happy to help with that, ${name}! Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to confirm for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Thank you, ${name}! Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${apptData.appt_start.toLocaleTimeString()} is confirmed.`;\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to cancel for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Sorry to hear that, ${name}. Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled.`;\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false });\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Fallback for unknown intents\n const lowerMessage = reply.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n } else {\n responseMessage = `I'm not sure how to help with that, ${name}. You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nWhat would you like to do?`;\n }\n }\n }\n\n // Rephrase the response using DeepSeek\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name);\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${clientData.client_ph}`,\n body: rephrasedMessage\n });\n console.log(`Reply from ${name} (${from}): ${reply}`);\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`);\n res.status(200).send('Reply processed.');\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message);\n res.status(500).send('Error processing reply.');\n }\n});\n\n// Start the server and send reminders\nconst port = process.env.PORT || 3001;\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`);\n try {\n await sendReminders();\n } catch (error) {\n console.error('Error during reminder sending:', error.message);\n }\n});\n"}, "1b4138d5db0de37783690ec42c54c689253056a4": {"t": 1785767137.5518389, "n": 46717, "h": "7e76afee9ba9d45c", "c": "require('dotenv').config();\nconst express = require('express');\nconst twilio = require('twilio');\nconst { createClient } = require('@supabase/supabase-js');\nconst { OpenAI } = require('openai');\nconst chrono = require('chrono-node'); // Kept as per your request\nconst app = express();\n\napp.use(express.urlencoded({ extended: false }));\napp.use(express.json());\n\n// Initialize Twilio client\nif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN) {\n console.error('Twilio credentials missing. Check your .env file.');\n process.exit(1);\n}\nconsole.log('Loaded TWILIO_ACCOUNT_SID:', process.env.TWILIO_ACCOUNT_SID);\nconsole.log('Loaded TWILIO_AUTH_TOKEN:', process.env.TWILIO_AUTH_TOKEN);\nconsole.log('Loaded TWILIO_WHATSAPP_NUMBER:', process.env.TWILIO_WHATSAPP_NUMBER);\nconst twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);\n\n// Initialize Supabase client\nif (!process.env.SUPABASE_URL || !process.env.SUPABASE_API_KEY) {\n console.error('Supabase credentials missing. Check your .env file.');\n process.exit(1);\n}\nconst supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_API_KEY);\n\n// Initialize DeepSeek client (using OpenAI SDK)\nif (!process.env.DEEPSEEK_API_KEY) {\n console.error('DeepSeek API key missing. Check your .env file.');\n process.exit(1);\n}\nconst openai = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: 'https://api.deepseek.com/v1',\n});\n\n// In-memory store to track conversation state\nconst conversationState = new Map();\n\n// Fetch appointments from Supabase\nconst fetchAppointments = async () => {\n try {\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select(`\n appt_id,\n store_id,\n assignment_id,\n client_id,\n service_id,\n appt_start,\n appt_end\n `);\n if (error) throw error;\n const appointments = await Promise.all(data.map(async (appt) => {\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .select('global_client_id')\n .eq('client_id', appt.client_id)\n .single();\n if (clientError) throw clientError;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, client_ph')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (globalError) throw globalError;\n return { ...appt, ...globalClient, phone_number: globalClient.client_ph };\n }));\n return appointments;\n } catch (error) {\n console.error('Error fetching appointments:', error.message);\n throw error;\n }\n};\n\n// Fetch store information (updated to include packages)\nconst fetchStoreInfo = async (storeId) => {\n try {\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name, store_operating_hours, store_addressL1')\n .eq('store_id', storeId)\n .single();\n if (storeError) throw storeError;\n\n const { data: services, error: servicesError } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_description, price, service_duration')\n .eq('store_id', storeId);\n if (servicesError) throw servicesError;\n\n const { data: packages, error: packagesError } = await supabase\n .schema('store_management')\n .from('packages')\n .select('package_id, package_name, package_description, package_limit, package_type, num_sessions, num_credits, package_price, services_linked, package_start, package_end')\n .eq('store_id', storeId);\n if (packagesError) throw packagesError;\n\n return { store, services, packages };\n } catch (error) {\n console.error('Error fetching store info:', error.message);\n throw error;\n }\n};\n\n// Create client record\nconst createClientRecord = async (phoneNumber, name) => {\n try {\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .insert({\n client_name: name,\n client_ph: phoneNumber.slice(3), // Remove +65\n })\n .select('global_client_id')\n .single();\n if (globalError) throw globalError;\n\n const { data: client, error: clientError } = await supabase\n .schema('clients')\n .from('clients')\n .insert({\n global_client_id: globalClient.global_client_id,\n })\n .select('client_id')\n .single();\n if (clientError) throw clientError;\n\n return client.client_id;\n } catch (error) {\n console.error('Error creating client record:', error.message);\n throw error;\n }\n};\n\n// Send appointment reminders (1 day before appt_start)\nconst sendReminders = async () => {\n try {\n const today = new Date();\n today.setHours(0, 0, 0, 0); // Reset to start of day\n const appointments = await fetchAppointments();\n for (const appt of appointments) {\n const apptStart = new Date(appt.appt_start);\n const diffDays = Math.ceil((apptStart - today) / (1000 * 60 * 60 * 24));\n if (diffDays === 1) { // 1 day before\n try {\n const name = appt.preferred_name || appt.client_name;\n const message = `Hi ${name}, this is a reminder for your ${appt.service_id} appointment at Glamour Salon on ${new Date(appt.appt_start).toLocaleDateString()} at ${new Date(appt.appt_start).toLocaleTimeString()} with your stylist. Reply YES to confirm, NO to cancel, or CHANGE to reschedule.`;\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${appt.phone_number}`,\n body: message\n });\n console.log(`Reminder sent to ${name} at ${appt.phone_number}: SID ${result.sid}`);\n } catch (error) {\n console.error(`Failed to send reminder to ${appt.client_name}:`, error.message);\n }\n }\n }\n } catch (error) {\n console.error('Error in sendReminders:', error.message);\n throw error;\n }\n};\n// Add this before the POST handler\napp.get('/twilio-webhook', (req, res) => {\n console.log('Received GET request on /twilio-webhook:', req.query);\n res.status(200).send('Webhook endpoint is active. Use POST for WhatsApp messages.');\n});\n\n// Controller for booking appointments\napp.post('/book-appointment', async (req, res) => {\n const { client_id, appointment_date, time, service_id, stylist, store_id } = req.body;\n try {\n const apptStart = new Date(appointment_date + 'T' + time + '+08:00');\n const apptEnd = new Date(apptStart.getTime() + 60 * 60 * 1000); // Assuming 1-hour duration for simplicity\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id,\n assignment_id: null,\n client_id,\n service_id,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString()\n })\n .select();\n if (error) throw error;\n res.status(201).json({ message: 'Appointment booked successfully', appointment: data[0] });\n } catch (error) {\n res.status(400).json({ error: error.message });\n }\n});\n\n// Function to rephrase messages using DeepSeek\nasync function rephraseWithDeepSeek(message, name) {\n try {\n if (!message) {\n return `Hi ${name}! It seems I don’t have enough details to assist you yet. Please let me know what you'd like to do, and I’ll be happy to help!`;\n }\n\n const prompt = `\n You are a friendly and professional assistant for Glamour Salon. Rephrase the following message to sound warm, natural, and human-like, while addressing the customer by their name (${name}). Keep the tone polite and conversational, as if you're speaking directly to the customer. Do not change the core meaning of the message. Do not include any explanatory notes or brackets.\n\n Message: \"${message}\"\n\n Rephrased message:\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in warm and friendly customer communication.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 150,\n });\n\n return completion.choices[0].message.content.trim();\n } catch (error) {\n console.error('DeepSeek rephrasing error:', error.message);\n return message || `Hi ${name}! I’m having trouble processing your request. Please try again or let me know what you need!`;\n }\n}\n\n// Function to parse natural language date and time\nfunction parseDateTime(dateStr, timeStr) {\n const now = new Date('2025-05-29T02:32:00+08:00'); // Updated to current date and time\n let dateTime;\n\n if (dateStr && timeStr) {\n const fullStr = `${dateStr} ${timeStr}`;\n // Use chrono-node with English locale only to avoid Japanese locale issues\n dateTime = chrono.parseDate(fullStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Fallback to basic parsing if chrono fails\n const today = now.toISOString().split('T')[0];\n dateTime = new Date(`${today}T${timeStr}:00+08:00`);\n if (isNaN(dateTime.getTime()) && dateStr) {\n dateTime = new Date(`${dateStr}T00:00:00+08:00`);\n }\n }\n } else if (dateStr) {\n dateTime = chrono.parseDate(dateStr, now, { forwardDate: true, timezone: 'Asia/Singapore', locale: 'en' });\n if (!dateTime || isNaN(dateTime.getTime())) {\n // Handle relative dates\n if (dateStr.toLowerCase().includes('tomorrow')) dateTime = new Date(now.setDate(now.getDate() + 1));\n else if (dateStr.toLowerCase().includes('today')) dateTime = now;\n else dateTime = new Date(dateStr);\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n dateTime.setHours(0, 0, 0, 0); // Default to midnight if no time\n }\n } else if (timeStr) {\n dateTime = new Date(now);\n const timeParts = timeStr.toLowerCase().match(/(\\d{1,2})(?::(\\d{2}))?\\s*(am|pm)?/i);\n if (timeParts) {\n let hours = parseInt(timeParts[1], 10);\n const minutes = timeParts[2] ? parseInt(timeParts[2], 10) : 0;\n const period = timeParts[3]?.toLowerCase();\n if (period === 'pm' && hours < 12) hours += 12;\n else if (period === 'am' && hours === 12) hours = 0;\n dateTime.setHours(hours, minutes, 0, 0);\n }\n }\n if (dateTime && !isNaN(dateTime.getTime())) {\n // Ensure timezone is +08:00\n dateTime.setMinutes(dateTime.getMinutes() + dateTime.getTimezoneOffset() + 480);\n return dateTime;\n }\n return null;\n}\n\n// Function to check if the time slot is available and within operating hours\nasync function checkAvailabilityAndHours(storeId, apptStart, apptEnd, serviceDuration) {\n const MAX_APPOINTMENTS_PER_SLOT = 5; // Hardcoded max appointments per slot\n const { store } = await fetchStoreInfo(storeId);\n const operatingHours = store.store_operating_hours;\n \n // Get the day of the week for the appointment\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n const apptDay = daysOfWeek[apptStart.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n // Check if the store is closed\n if (storeHours.isClosed) {\n return {\n isAvailable: false,\n reason: 'closed',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Parse store opening and closing times\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n const storeOpenTime = new Date(apptStart);\n storeOpenTime.setHours(openHour, openMinute, 0, 0);\n const storeCloseTime = new Date(apptStart);\n storeCloseTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Check if the appointment is within operating hours\n if (apptStart < storeOpenTime || apptEnd > storeCloseTime) {\n return {\n isAvailable: false,\n reason: 'outside_hours',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n // Check for overlapping appointments\n const { data: existingAppointments, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_start, appt_end')\n .eq('store_id', storeId)\n .lte('appt_start', apptEnd.toISOString())\n .gte('appt_end', apptStart.toISOString());\n \n if (error) throw error;\n\n if (existingAppointments.length >= MAX_APPOINTMENTS_PER_SLOT) {\n return {\n isAvailable: false,\n reason: 'fully_booked',\n suggestedTime: suggestNextAvailableTime(storeId, apptStart, serviceDuration, operatingHours)\n };\n }\n\n return { isAvailable: true };\n}\n\n// Function to suggest the next available time slot\nfunction suggestNextAvailableTime(storeId, requestedTime, serviceDuration, operatingHours) {\n const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n let suggestedTime = new Date(requestedTime);\n let attempts = 0;\n const maxAttempts = 7; // Try up to 7 days ahead\n\n while (attempts < maxAttempts) {\n const apptDay = daysOfWeek[suggestedTime.getDay()];\n const storeHours = operatingHours[apptDay.toLowerCase()];\n \n if (!storeHours.isClosed) {\n const [openHour, openMinute] = storeHours.open.split(':').map(Number);\n const [closeHour, closeMinute] = storeHours.close.split(':').map(Number);\n \n // Start from opening time or requested time if later\n let nextTime = new Date(suggestedTime);\n nextTime.setHours(openHour, openMinute, 0, 0);\n if (nextTime < suggestedTime) {\n nextTime.setHours(suggestedTime.getHours(), Math.ceil(suggestedTime.getMinutes() / 30) * 30, 0, 0);\n }\n\n const closeTime = new Date(suggestedTime);\n closeTime.setHours(closeHour, closeMinute, 0, 0);\n\n // Increment by 30 minutes until a slot is found or we pass closing time\n while (nextTime < closeTime) {\n const apptEnd = new Date(nextTime.getTime() + serviceDuration * 60 * 1000);\n if (apptEnd <= closeTime) {\n // Check if this slot is available (mock check since we can't call async here)\n return {\n date: nextTime.toLocaleDateString(),\n time: nextTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n };\n }\n nextTime.setMinutes(nextTime.getMinutes() + 30);\n }\n }\n \n // Move to the next day\n suggestedTime.setDate(suggestedTime.getDate() + 1);\n suggestedTime.setHours(0, 0, 0, 0); // Reset to start of day\n attempts++;\n }\n\n // If no slot is found within 7 days, suggest contacting support\n return null;\n}\n\n// Function to classify intent and extract details using DeepSeek\nasync function classifyIntent(message) {\n try {\n const prompt = `\n You are a helpful assistant for Glamour Salon. Analyze the customer's message and classify their intent. The possible intents are:\n - GREETING (e.g., \"Hello\", \"Hi\")\n - BOOK_APPOINTMENT (e.g., \"I need to book an appointment\", \"I want a haircut\", \"I'd like to schedule a service\")\n - CHANGE_APPOINTMENT (e.g., \"I want to change my booking\", \"Can I reschedule\")\n - STORE_INFO (e.g., \"Can I get more details about a salon\", \"What are your hours\", \"operating hours for Idle\", \"address of Glamour Salon\", \"services at Idle\")\n - CONFIRM_APPOINTMENT (e.g., \"YES\")\n - CANCEL_APPOINTMENT (e.g., \"NO\")\n - PACKAGE_INQUIRY (e.g., \"Can you tell me about my package?\", \"Is my package still active?\", \"How many sessions are left?\", \"Package details\")\n - UNKNOWN (if the intent doesn't match any of the above)\n\n Additionally, extract any relevant details such as:\n - store_name (e.g., \"Idle\", \"Glamour Salon\")\n - service_name (e.g., \"haircut\", \"manicure\", or informal terms like \"cut\" or \"nails\" that can be mapped to services)\n - date (e.g., \"26/05/2025\", \"tomorrow\", \"next Friday\", \"this Friday\")\n - time (e.g., \"14:00\", \"2 PM\", \"12:34 AM\", \"noon\", \"10 am\")\n - info_type (for STORE_INFO intent: \"address\" for address-related requests, \"hours\" for operating hours or schedule requests, \"services\" for service-related requests)\n\n Focus on identifying the primary service name even if the message includes extra words (e.g., \"Haircut maybe\" should be \"haircut\") or informal terms (e.g., \"cut\" should be interpreted as \"haircut\", \"nails\" as \"manicure\"). If unsure, prioritize the most likely service based on context.\n\n Customer message: \"${message}\"\n\n Respond in JSON format:\n {\n \"intent\": \"INTENT_NAME\",\n \"details\": {\n \"store_name\": \"STORE_NAME or null\",\n \"service_name\": \"SERVICE_NAME or null\",\n \"date\": \"DATE or null\",\n \"time\": \"TIME or null\",\n \"info_type\": \"INFO_TYPE or null\"\n }\n }\n `;\n\n const completion = await openai.chat.completions.create({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a helpful assistant for Glamour Salon, specializing in understanding customer intents from natural language with a focus on flexible service name and date/time recognition.' },\n { role: 'user', content: prompt }\n ],\n temperature: 0.5,\n max_tokens: 200,\n });\n\n let responseContent = completion.choices[0].message.content.trim();\n if (responseContent.startsWith('```json')) {\n responseContent = responseContent.replace(/^```json\\n/, '').replace(/\\n```$/, '');\n }\n\n return JSON.parse(responseContent);\n } catch (error) {\n console.error('DeepSeek intent classification error:', error.message);\n // Fallback to keyword-based intent classification\n const lowerMessage = message.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n return {\n intent: 'BOOK_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('change') || lowerMessage.includes('reschedule') || lowerMessage.includes('modify')) {\n return {\n intent: 'CHANGE_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('details') || lowerMessage.includes('info') || lowerMessage.includes('hours') || lowerMessage.includes('address') || lowerMessage.includes('services')) {\n let infoType = null;\n if (lowerMessage.includes('hours') || lowerMessage.includes('schedule')) infoType = 'hours';\n else if (lowerMessage.includes('address')) infoType = 'address';\n else if (lowerMessage.includes('services')) infoType = 'services';\n\n return {\n intent: 'STORE_INFO',\n details: {\n store_name: lowerMessage.includes('idle') ? 'Idle' : (lowerMessage.includes('glamour') ? 'Glamour Salon' : null),\n service_name: null,\n date: null,\n time: null,\n info_type: infoType\n }\n };\n } else if (lowerMessage.toUpperCase() === 'YES') {\n return {\n intent: 'CONFIRM_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.toUpperCase() === 'NO') {\n return {\n intent: 'CANCEL_APPOINTMENT',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) {\n return {\n intent: 'GREETING',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n } else if (lowerMessage.includes('package') || lowerMessage.includes('session') || lowerMessage.includes('credit') || lowerMessage.includes('active')) {\n return {\n intent: 'PACKAGE_INQUIRY',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n\n return {\n intent: 'UNKNOWN',\n details: {\n store_name: null,\n service_name: null,\n date: null,\n time: null,\n info_type: null\n }\n };\n }\n}\n\n// Webhook to handle customer replies\napp.post('/twilio-webhook', async (req, res) => {\n console.log('Webhook received:', req.body); // Log the incoming request\n const reply = req.body.Body;\n const from = req.body.From.replace('whatsapp:', '');\n console.log(`Processing message from ${from}: ${reply}`);\n try {\n // Hardcode remove +65 from incoming number for client_ph query\n let fromWithoutPrefix = from;\n if (from.startsWith('+65')) {\n fromWithoutPrefix = from.slice(3); // Remove +65 to get 96460132\n }\n const { data: clientData, error: clientError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('client_name, preferred_name, global_client_id, client_ph')\n .eq('client_ph', fromWithoutPrefix)\n .single();\n if (clientError || !clientData) {\n console.log(`No matching client data for ${fromWithoutPrefix}`);\n res.status(200).send('Customer not found.');\n return;\n }\n\n const { data: clientLink } = await supabase\n .schema('clients')\n .from('clients')\n .select('client_id')\n .eq('global_client_id', clientData.global_client_id)\n .single();\n if (!clientLink) {\n console.log(`No client link found for global_client_id ${clientData.global_client_id}`);\n res.status(200).send('No client link found.');\n return;\n }\n\n let responseMessage;\n const name = clientData.preferred_name || clientData.client_name;\n\n // Classify the customer's intent using DeepSeek\n const { intent, details } = await classifyIntent(reply);\n\n // Check if we're in a conversation state\n if (conversationState.has(from)) {\n const state = conversationState.get(from);\n if (state.step === 'selectOption') {\n // Handle store info sub-menu\n const { storeId, storeName } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n\n if (reply === '1') {\n // Store Address\n responseMessage = `Address for ${storeName}: ${store.store_addressL1}`;\n } else if (reply === '2') {\n // Operating Hours\n const hours = store.store_operating_hours;\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${info.close}`;\n })\n .join('\\n');\n responseMessage = `Operating Hours for ${storeName}:\\n${formattedHours}`;\n } else if (reply === '3') {\n // Services\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeName}:\\n${serviceList}`;\n } else if (reply === '4') {\n // Packages\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate} - ${endDate}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage = `Packages at ${storeName}:\\n${packageList}`;\n } else {\n responseMessage = `Please select an option:\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n conversationState.delete(from); // Reset state\n } else if (state.step === 'book_appointment') {\n // Handle booking flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'book_appointment_service';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That's wonderful—I've found your appointment slot at ${state.storeName}. What service would you love to book today? Maybe a fresh haircut, a relaxing manicure, or something else? Let me know, and I’ll get it all set up for you!`;\n }\n } else {\n responseMessage = `Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'book_appointment_service') {\n // Handle booking flow - Service selection\n if (!state.serviceName) {\n const { data: services } = await supabase\n .schema('store_management')\n .from('services')\n .select('service_id, service_name, service_duration')\n .eq('store_id', state.storeId);\n let serviceMatch = services.find(s => s.service_name.toLowerCase() === details.service_name?.toLowerCase());\n if (!serviceMatch && details.service_name) {\n // Try to match informal terms or partial names\n const lowerServiceName = details.service_name.toLowerCase();\n const serviceSynonyms = {\n 'cut': 'haircut',\n 'nails': 'manicure',\n 'trim': 'haircut'\n };\n const normalizedService = serviceSynonyms[lowerServiceName] || lowerServiceName;\n serviceMatch = services.find(s => s.service_name.toLowerCase().includes(normalizedService));\n }\n if (serviceMatch) {\n state.serviceName = serviceMatch.service_name;\n state.serviceId = serviceMatch.service_id;\n state.serviceDuration = serviceMatch.service_duration;\n state.step = 'book_appointment_datetime';\n conversationState.set(from, state);\n responseMessage = `Hi ${name}! That’s fantastic—your ${state.serviceName} is available at ${state.storeName}! 🎉 When would you like it? Feel free to say something like 'tomorrow at 2 PM' or 'Friday at noon'—I’ll handle the rest!`;\n } else {\n responseMessage = `Oops, I couldn’t find the service \"${details.service_name || reply}\" at ${state.storeName}. Did you mean something like 'haircut' or 'manicure'? Feel free to try again or let me know what you had in mind!`;\n }\n }\n } else if (state.step === 'book_appointment_datetime') {\n // Handle booking flow - Date and time selection\n if (!state.date || !state.time) {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + state.serviceDuration * 60 * 1000);\n\n // Check availability and operating hours\n const availability = await checkAvailabilityAndHours(state.storeId, apptStart, apptEnd, state.serviceDuration);\n if (!availability.isAvailable) {\n if (availability.reason === 'closed') {\n responseMessage = `Sorry, ${name}, the store is closed on ${apptStart.toLocaleDateString()}.`;\n } else if (availability.reason === 'outside_hours') {\n responseMessage = `Sorry, ${name}, the requested time on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is outside our operating hours.`;\n } else if (availability.reason === 'fully_booked') {\n responseMessage = `Sorry, ${name}, the time slot on ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })} is fully booked.`;\n }\n \n if (availability.suggestedTime) {\n responseMessage += ` How about ${availability.suggestedTime.date} at ${availability.suggestedTime.time} instead? Or let me know another time that works for you!`;\n } else {\n responseMessage += ` I couldn’t find an available slot soon. Could you try a different day, or let me know when you’re free?`;\n }\n } else {\n // Slot is available, proceed with booking\n const { data, error } = await supabase\n .schema('appointments')\n .from('appointments')\n .insert({\n store_id: state.storeId,\n assignment_id: 'b32e113a-0bdc-46ee-95ab-f7260abda24e',\n client_id: clientLink.client_id,\n service_id: state.serviceId,\n appt_start: apptStart.toISOString(),\n appt_end: apptEnd.toISOString(),\n prepayment_amt: 50\n })\n .select();\n if (error) throw error;\n\n responseMessage = `Great news, ${name}! Your ${state.serviceName} at ${state.storeName} is booked for ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}. Can’t wait to see you!`;\n conversationState.delete(from); // Reset state\n }\n } else {\n responseMessage = `Hmm, I didn’t quite catch that date or time. Could you please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'? I’m here to help!`;\n }\n } else {\n responseMessage = `When would you like your ${state.serviceName} at ${state.storeName}? Feel free to say something like 'tomorrow at 2 PM' or 'this Friday at 10 am'—I’ll handle the rest!`;\n }\n }\n } else if (state.step === 'store_info') {\n // Handle store info flow - Store selection\n if (!state.storeName) {\n // Waiting for store name\n if (details.store_name) {\n const { data: storeRecord } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_id, store_name')\n .eq('store_name', details.store_name)\n .single();\n if (!storeRecord) {\n responseMessage = `Sorry, I couldn't find a store named \"${details.store_name}\". Could you please try again?`;\n } else {\n state.storeName = storeRecord.store_name;\n state.storeId = storeRecord.store_id;\n state.step = 'selectOption';\n conversationState.set(from, state);\n responseMessage = `I found ${state.storeName}! What would you like to know about it?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n } else {\n responseMessage = `Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n }\n }\n } else if (state.step === 'store_info_details') {\n // Handle store info flow - Details selection\n const { storeId, storeName } = state;\n const { store, services, packages } = await fetchStoreInfo(storeId);\n const lowerReply = reply.toLowerCase();\n if (!state.infoType) {\n if (details.info_type === 'address' || lowerReply.includes('address')) {\n responseMessage = `Address for ${storeName}:\\n${store.store_addressL1}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'hours' || lowerReply.includes('hours') || lowerReply.includes('schedule')) {\n const hours = store.store_operating_hours;\n const formattedHours = Object.entries(hours)\n .map(([day, info]) => {\n if (info.isClosed) return `${day}: Closed`;\n return `${day}: ${info.open} - ${info.close}`;\n })\n .join('\\n');\n responseMessage = `Operating Hours for ${storeName}:\\n${formattedHours}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'services' || lowerReply.includes('services')) {\n const serviceList = services.map(s => `${s.service_name}: $${s.price} (${s.service_duration} mins)`).join('\\n');\n responseMessage = `Services at ${storeName}:\\n${serviceList}`;\n conversationState.delete(from); // Reset state\n } else if (details.info_type === 'packages' || lowerReply.includes('packages')) {\n const packageList = packages.map(p => {\n const startDate = new Date(p.package_start).toLocaleDateString();\n const endDate = new Date(p.package_end).toLocaleDateString();\n return `${p.package_name}: $${p.package_price} (${p.num_sessions} sessions, ${p.num_credits} credits, valid ${startDate} - ${endDate}) - ${p.package_description}`;\n }).join('\\n');\n responseMessage = `Packages at ${storeName}:\\n${packageList}`;\n conversationState.delete(from); // Reset state\n } else {\n responseMessage = `What would you like to know about ${storeName}?\\n1. Store Address\\n2. Operating Hours\\n3. Services\\n4. Packages`;\n }\n }\n } else if (state.step === 'change_appointment') {\n // Handle change appointment flow\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, appt_end, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n conversationState.delete(from); // Reset state\n } else {\n if (details.date || details.time) {\n const dateTime = parseDateTime(details.date, details.time);\n if (dateTime && !isNaN(dateTime.getTime())) {\n const apptStart = dateTime;\n const apptEnd = new Date(apptStart.getTime() + (apptData.appt_end - apptData.appt_start)); // Preserve original duration\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ appt_start: apptStart.toISOString(), appt_end: apptEnd.toISOString() })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `All set, ${name}! Your ${apptData.service_id} appointment is now rescheduled to ${apptStart.toLocaleDateString()} at ${apptStart.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })}.`;\n conversationState.delete(from); // Reset state\n } else {\n responseMessage = `I didn’t quite get that date or time. Please try again with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'.`;\n }\n } else {\n responseMessage = `When would you like to reschedule your ${apptData.service_id} appointment? Just let me know with something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n }\n } else if (state.step === 'package_inquiry') {\n if (!state.phoneNumberRequested) {\n // Ask for phone number\n state.phoneNumberRequested = true;\n conversationState.set(from, state);\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Process the provided phone number\n const phoneNumber = reply;\n const { data: globalClient, error: globalError } = await supabase\n .schema('clients')\n .from('global_clients')\n .select('global_client_id')\n .eq('client_ph', phoneNumber)\n .single();\n if (globalError || !globalClient) {\n responseMessage = `Sorry, ${name}, I couldn’t find a client with the phone number ${phoneNumber}. Please try again with your registered number or contact support.`;\n conversationState.delete(from); // Reset state\n } else {\n const { data: clientPackages, error: packageError } = await supabase\n .schema('clients')\n .from('client_packages')\n .select(`\n client_package_id,\n store_id,\n package_id,\n global_client_id,\n client_id,\n session_remaining,\n credits_remaining,\n purchase_date,\n status\n `)\n .eq('global_client_id', globalClient.global_client_id);\n if (packageError || !clientPackages || clientPackages.length === 0) {\n responseMessage = `It looks like you don’t have any active packages, ${name}. Would you like to purchase one?`;\n conversationState.delete(from); // Reset state\n } else {\n const packageDetails = await Promise.all(clientPackages.map(async (pkg) => {\n // Fetch store name from store_management.stores\n const { data: store, error: storeError } = await supabase\n .schema('store_management')\n .from('stores')\n .select('store_name')\n .eq('store_id', pkg.store_id)\n .single();\n if (storeError) throw storeError;\n\n // Fetch package details from store_management.packages\n const { data: packageInfo, error: packageInfoError } = await supabase\n .schema('store_management')\n .from('packages')\n .select(`\n package_name,\n package_description,\n num_sessions,\n num_credits,\n package_price,\n package_start,\n package_end\n `)\n .eq('package_id', pkg.package_id)\n .single();\n if (packageInfoError) throw packageInfoError;\n\n const purchaseDate = new Date(pkg.purchase_date).toLocaleDateString();\n const packageStart = new Date(packageInfo.package_start).toLocaleDateString();\n const packageEnd = new Date(packageInfo.package_end).toLocaleDateString();\n\n return `Package: ${packageInfo.package_name} at ${store.store_name}\\n` +\n `Description: ${packageInfo.package_description}\\n` +\n `Price: $${packageInfo.package_price}\\n` +\n `Total Sessions: ${packageInfo.num_sessions}, Sessions Remaining: ${pkg.session_remaining}\\n` +\n `Total Credits: ${packageInfo.num_credits}, Credits Remaining: ${pkg.credits_remaining}\\n` +\n `Purchased On: ${purchaseDate}\\n` +\n `Valid From: ${packageStart} to ${packageEnd}\\n` +\n `Status: ${pkg.status}`;\n }));\n responseMessage = `Here are your package details, ${name}:\\n${packageDetails.join('\\n\\n')}\\nLet me know if you need more information!`;\n conversationState.delete(from); // Reset state\n }\n }\n }\n }\n } else {\n // Handle intents when not in a conversation state\n if (intent === 'GREETING') {\n responseMessage = `Hello ${name}! How can I help you today? You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nJust let me know what you'd like!`;\n } else if (intent === 'BOOK_APPOINTMENT') {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n } else if (intent === 'CHANGE_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to change for ${name}. Would you like to book a new appointment instead?`;\n } else {\n conversationState.set(from, { step: 'change_appointment' });\n responseMessage = `Let's reschedule your ${apptData.service_id} appointment, ${name}. When would you like it? Try something like 'tomorrow at 2 PM' or 'this Friday at 10 am'!`;\n }\n } else if (intent === 'STORE_INFO') {\n conversationState.set(from, { step: 'store_info' });\n responseMessage = `I'd be happy to help with that, ${name}! Which store would you like more details about (e.g., Idle, Glamour Salon)?`;\n } else if (intent === 'CONFIRM_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to confirm for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .update({ status: 'confirmed' })\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Thank you, ${name}! Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} is confirmed.`;\n }\n } else if (intent === 'CANCEL_APPOINTMENT') {\n const { data: apptData, error: apptError } = await supabase\n .schema('appointments')\n .from('appointments')\n .select('appt_id, appt_start, service_id')\n .eq('client_id', clientLink.client_id)\n .order('appt_start', { ascending: false })\n .limit(1)\n .single();\n if (apptError || !apptData) {\n responseMessage = `No appointment found to cancel for ${name}. Would you like to book a new appointment?`;\n } else {\n await supabase\n .schema('appointments')\n .from('appointments')\n .delete()\n .eq('appt_id', apptData.appt_id);\n responseMessage = `Sorry to hear that, ${name}. Your ${apptData.service_id} appointment on ${new Date(apptData.appt_start).toLocaleDateString()} at ${new Date(apptData.appt_start).toLocaleTimeString()} has been canceled.`;\n }\n } else if (intent === 'PACKAGE_INQUIRY') {\n conversationState.set(from, { step: 'package_inquiry', phoneNumberRequested: false });\n responseMessage = `I'd love to help you with your package details, ${name}! Could you please provide your registered phone number (e.g., 96460132) so I can look it up for you?`;\n } else {\n // Fallback for unknown intents\n const lowerMessage = reply.toLowerCase();\n if (lowerMessage.includes('book') || lowerMessage.includes('appointment') || lowerMessage.includes('schedule') || lowerMessage.includes('service')) {\n conversationState.set(from, { step: 'book_appointment' });\n responseMessage = `I'd be happy to help you book an appointment, ${name}! Which store would you like to book at (e.g., Idle, Glamour Salon)?`;\n } else {\n responseMessage = `I'm not sure how to help with that, ${name}. You can:\\n- Book an appointment (e.g., \"I need to book an appointment\")\\n- Change an appointment (e.g., \"I want to change my booking\")\\n- Get store info (e.g., \"Can I get more details about a salon\")\\n- Check your package details (e.g., \"Can you tell me about my package?\")\\nWhat would you like to do?`;\n }\n }\n }\n\n // Rephrase the response using DeepSeek\n const rephrasedMessage = await rephraseWithDeepSeek(responseMessage, name);\n\n const result = await twilioClient.messages.create({\n from: process.env.TWILIO_WHATSAPP_NUMBER,\n to: `whatsapp:+65${clientData.client_ph}`,\n body: rephrasedMessage\n });\n console.log(`Reply from ${name} (${from}): ${reply}`);\n console.log(`Response sent: ${rephrasedMessage}, SID: ${result.sid}`);\n res.status(200).send('Reply processed.');\n } catch (error) {\n console.error(`Failed to process webhook for ${from}:`, error.message);\n res.status(500).send('Error processing reply.');\n }\n});\n\n// Start the server and send reminders\nconst port = process.env.PORT || 3001;\napp.listen(port, async () => {\n console.log(`Server running on port ${port}`);\n try {\n await sendReminders();\n } catch (error) {\n console.error('Error during reminder sending:', error.message);\n }\n});"}}}}