diff --git a/.gitignore b/.gitignore index 7490523..713d500 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/backend/clinicAgent.js b/backend/clinicAgent.js index 220d689..fdd5dfa 100644 --- a/backend/clinicAgent.js +++ b/backend/clinicAgent.js @@ -66,10 +66,29 @@ class ClinicAgent { if (text.includes("book") || text.includes("today")) { return { reply: "I can help with that. Which time works best for you?", - buttons: ["10:00 AM", "11:00 AM", "06:00 PM"] + buttons: ["10:00 AM", "11:00 AM", "06:00 PM"], + nextStep: "AWAIT_TIME" }; } + // Scenario 3.1: Handling time selection + if (context.lastStep === "AWAIT_TIME") { + let time = ""; + if (text.includes("1") || text.includes("10")) time = "10:00"; + else if (text.includes("2") || text.includes("11")) time = "11:00"; + else if (text.includes("3") || text.includes("6") || text.includes("06")) time = "18:00"; + + if (time) { + const today = new Date().toISOString().split('T')[0]; + const result = await queueManager.bookOnline(today, time, phone, 'sharma-clinic'); + if (result.success) { + return { reply: `Awesome! Your token is ${result.token}. You should receive a confirmation message shortly.` }; + } else { + return { reply: `Sorry, ${result.message}` }; + } + } + } + // Scenario 3.5: Medicine Query if (text.includes("have") || text.includes("medicine") || text.includes("syrup") || text.includes("tablet")) { const pharmacyReply = pharmacyAgent.handlePatientQuery(message); diff --git a/backend/notificationManager.js b/backend/notificationManager.js index f71deaf..ee96f50 100644 --- a/backend/notificationManager.js +++ b/backend/notificationManager.js @@ -1,25 +1,33 @@ /** - * NotificationManager: Handles mock WhatsApp notifications for Curio + * NotificationManager: Handles WhatsApp notifications for Curio */ +const whatsappService = require('./whatsappService'); + class NotificationManager { constructor() { this.notificationsSent = []; } - sendRescheduleAlert(patientPhone, tenantName, oldTime, newTime) { - const message = `šŸ”” *Curio Alert*: A slot has just opened up at ${tenantName} for today at ${newTime}! - -Would you like to move your ${oldTime} appointment to this earlier slot? - -Reply: -1. Yes, move me! -2. No, keep my current time.`; - - console.log(`[WhatsApp to ${patientPhone}]: ${message}`); - this.notificationsSent.push({ phone: patientPhone, message, timestamp: new Date() }); - - return { success: true, message: "Notification sent." }; + async sendRescheduleAlert(patientPhone, tenantName, oldTime, newTime) { + this.notificationsSent.push({ phone: patientPhone, type: 'reschedule', timestamp: new Date() }); + return await whatsappService.sendRescheduleOffer(patientPhone, tenantName, oldTime, newTime); + } + + async sendTokenNotification(patientPhone, tokenData) { + this.notificationsSent.push({ phone: patientPhone, type: 'token', timestamp: new Date() }); + return await whatsappService.sendTokenConfirmation( + patientPhone, + tokenData.token, + tokenData.clinicName, + tokenData.slotTime, + tokenData.fee + ); + } + + async sendQueueUpdate(patientPhone, clinicName, position, eta) { + this.notificationsSent.push({ phone: patientPhone, type: 'queue', timestamp: new Date() }); + return await whatsappService.sendQueueUpdate(patientPhone, clinicName, position, eta); } getRecentNotifications() { diff --git a/backend/queueManager.js b/backend/queueManager.js index e9a875d..047fbc3 100644 --- a/backend/queueManager.js +++ b/backend/queueManager.js @@ -64,7 +64,7 @@ class QueueManager { return this.slots[key]; } - bookOnline(date, time, tenantId = 'default') { + async bookOnline(date, time, patientPhone, tenantId = 'default') { if (!this.isAvailable(tenantId, date, time)) { return { success: false, message: "The clinic is closed at this time / on this day." }; } @@ -72,9 +72,18 @@ class QueueManager { const config = getTenantConfig(tenantId); if (status.online < status.maxOnline) { status.online++; + const token = `ON-${status.online}`; + if (patientPhone) { + await notificationManager.sendTokenNotification(patientPhone, { + token, + clinicName: config.name, + slotTime: time, + fee: config.consultationFee + }); + } return { success: true, - token: `ON-${status.online}`, + token, slot: this.getSlotKey(tenantId, date, time), status: 'PENDING_PAYMENT', fee: config.consultationFee @@ -83,7 +92,7 @@ class QueueManager { return { success: false, message: "Slot full for online booking. Try next slot." }; } - bookWalkin(date, time, tenantId = 'default') { + async bookWalkin(date, time, patientPhone, tenantId = 'default') { if (!this.isAvailable(tenantId, date, time)) { return { success: false, message: "The clinic is closed at this time / on this day." }; } @@ -91,9 +100,18 @@ class QueueManager { const config = getTenantConfig(tenantId); if (status.walkin < status.maxWalkin) { status.walkin++; + const token = `WK-${status.walkin}`; + if (patientPhone) { + await notificationManager.sendTokenNotification(patientPhone, { + token, + clinicName: config.name, + slotTime: time, + fee: config.consultationFee + }); + } return { success: true, - token: `WK-${status.walkin}`, + token, status: 'PENDING_PAYMENT', fee: config.consultationFee }; @@ -110,24 +128,24 @@ class QueueManager { ]; } - cancelBooking(date, time, tenantId = 'default') { + async cancelBooking(date, time, tenantId = 'default') { const status = this.getSlotStatus(tenantId, date, time); if (status.online > 0) { status.online--; - this.notifyAvailableSlot(tenantId, date, time); + await this.notifyAvailableSlot(tenantId, date, time); return { success: true, message: "Booking cancelled and slot opened." }; } return { success: false, message: "No bookings found for this slot." }; } - notifyAvailableSlot(tenantId, date, time) { + async notifyAvailableSlot(tenantId, date, time) { const config = getTenantConfig(tenantId); console.log(`[Queue] Triggering notifications for open slot: ${time}`); // Mock finding a patient with a later slot - const mockLaterPatient = { phone: "9876543210", appointmentTime: "19:00" }; + const mockLaterPatient = { phone: "+918500176938", appointmentTime: "19:00" }; - notificationManager.sendRescheduleAlert( + await notificationManager.sendRescheduleAlert( mockLaterPatient.phone, config.name, mockLaterPatient.appointmentTime, diff --git a/backend/server.js b/backend/server.js index c61eae1..395f668 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,8 +1,11 @@ +require('dotenv').config(); const express = require('express'); const path = require('path'); const crypto = require('crypto'); const bcrypt = require('bcryptjs'); +const twilio = require('twilio'); +const whatsappService = require('./whatsappService'); const botLogic = require('./botLogic'); const { getTenantConfig, updateTenantConfig } = require('./configManager'); const { getTemporalClient } = require('./temporalClient'); @@ -487,7 +490,10 @@ app.patch('/api/clinic/settings', requireAuth('OWNER'), async (req, res) => { // ── WHATSAPP WEBHOOK ────────────────────────────────────────────────────────── -app.post('/whatsapp/webhook', async (req, res) => { +// Using twilio.webhook() middleware for request validation +// Disable validation in local environment to allow testing via ngrok/local tools +const shouldValidate = process.env.NODE_ENV === 'production'; +app.post('/whatsapp/webhook', twilio.webhook({ validate: shouldValidate }), async (req, res) => { const from = req.body.From || req.body.from; const body = req.body.Body || req.body.body; @@ -513,8 +519,51 @@ app.post('/whatsapp/webhook', async (req, res) => { }); res.type('text/xml').send(''); } catch (err) { - console.error('[WhatsApp Webhook] Error:', err); - res.status(500).send('Internal Server Error'); + console.error('[WhatsApp Webhook] Temporal unavailable, falling back to local botLogic. Error:', err.message); + + // Fallback to local bot logic if Temporal is not running + try { + const botResponse = await botLogic.handleMessage(from, body); + let replyText = botResponse.reply; + + // Format buttons for standard WhatsApp text since we aren't using Twilio Content Templates + if (botResponse.buttons && botResponse.buttons.length > 0) { + replyText += '\n\n' + botResponse.buttons.map((b, i) => `${i + 1}. ${b}`).join('\n'); + } + + await whatsappService.sendWhatsApp(from, replyText); + res.type('text/xml').send(''); + } catch (botErr) { + console.error('[WhatsApp Webhook] Fallback logic error:', botErr); + res.status(500).send('Internal Server Error'); + } + } +}); + +// ── MANUAL TOKEN NOTIFICATION ────────────────────────────────────────────────── + +app.post('/api/whatsapp/send-token', requireAuth(), async (req, res) => { + const { patientPhone, tokenNumber, clinicName, slotTime, fee } = req.body; + if (!patientPhone || !tokenNumber || !clinicName || !slotTime) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + const result = await whatsappService.sendTokenConfirmation( + patientPhone, + tokenNumber, + clinicName, + slotTime, + fee || 0 + ); + if (result.success) { + res.json({ success: true, sid: result.sid }); + } else { + res.status(500).json({ error: result.error }); + } + } catch (err) { + console.error('[Send Token]', err); + res.status(500).json({ error: 'Failed to send token via WhatsApp' }); } }); diff --git a/backend/whatsappService.js b/backend/whatsappService.js new file mode 100644 index 0000000..20bc0db --- /dev/null +++ b/backend/whatsappService.js @@ -0,0 +1,58 @@ +require('dotenv').config(); +const twilio = require('twilio'); + +// Initialize Twilio using API Key +// Account SID is still required for API Key auth +const accountSid = process.env.TWILIO_ACCOUNT_SID; +const apiKey = process.env.TWILIO_API_KEY_SID; +const apiSecret = process.env.TWILIO_API_KEY_SECRET; + +const client = twilio(apiKey, apiSecret, { accountSid }); +const fromNumber = process.env.TWILIO_WHATSAPP_FROM || 'whatsapp:+14155238886'; // Fallback to sandbox if not set + +class WhatsAppService { + async sendWhatsApp(to, body) { + if (!to) { + console.error('[WhatsAppService] Missing destination phone number'); + return { success: false, error: 'Missing phone number' }; + } + + // Ensure "whatsapp:" prefix + const toFormatted = to.startsWith('whatsapp:') ? to : `whatsapp:${to.startsWith('+') ? to : '+' + to}`; + + try { + const message = await client.messages.create({ + body: body, + from: fromNumber, + to: toFormatted + }); + console.log(`[WhatsAppService] Message sent to ${toFormatted}. SID: ${message.sid}`); + return { success: true, sid: message.sid }; + } catch (error) { + console.error(`[WhatsAppService] Failed to send message to ${toFormatted}:`, error.message); + return { success: false, error: error.message }; + } + } + + async sendTokenConfirmation(phone, tokenNumber, clinicName, slotTime, fee) { + const message = `šŸ„ *CuraFlow Token Confirmed*\n\nšŸ”¢ Token: ${tokenNumber}\nšŸØ Clinic: ${clinicName}\nšŸ• Slot: ${slotTime}\nšŸ’° Fee: ₹${fee}\n\nPlease arrive 10 mins early.`; + return this.sendWhatsApp(phone, message); + } + + async sendQueueUpdate(phone, clinicName, position, estimatedTime) { + const message = `šŸ“‹ *Queue Update*\n\nYou are #${position} in line at ${clinicName}.\nā± Estimated wait: ~${estimatedTime} mins`; + return this.sendWhatsApp(phone, message); + } + + async sendRescheduleOffer(phone, clinicName, oldTime, newTime) { + const message = `šŸ”” *Slot Available!*\n\nA slot opened at ${clinicName} for ${newTime}!\nYour current: ${oldTime}\n\nReply:\n1. Yes, move me!\n2. No, keep my current time.`; + return this.sendWhatsApp(phone, message); + } + + async sendCancellationConfirmation(phone, clinicName, slotTime) { + const message = `āœ… *Booking Cancelled*\n\nYour token at ${clinicName} for ${slotTime} has been cancelled.`; + return this.sendWhatsApp(phone, message); + } +} + +module.exports = new WhatsAppService(); diff --git a/k8s/app.yaml b/k8s/app.yaml index bb10bc0..0339993 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -26,6 +26,14 @@ spec: value: "postgres://postgres:curio_secret@curio-db:5432/curio" - name: TEMPORAL_URL value: "temporal-frontend.ai-core.svc.cluster.local:7233" + - name: TWILIO_ACCOUNT_SID + value: "AC601e1962dc8417ddb3ae53cd8d865020" + - name: TWILIO_API_KEY_SID + value: "SK48dd3d06318997be3a809a6ef0cd761d" + - name: TWILIO_API_KEY_SECRET + value: "rN5RpKAbFVCv0G45ehluf1JbTWt2dgNW" + - name: TWILIO_WHATSAPP_FROM + value: "whatsapp:+14155238886" livenessProbe: httpGet: path: / diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 1c903bc..d186b2f 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -201,6 +201,41 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -231,6 +266,24 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -340,6 +393,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -376,6 +441,12 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -391,6 +462,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -410,6 +490,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -484,6 +576,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -581,6 +688,42 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -678,6 +821,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", @@ -710,6 +868,42 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1162,6 +1356,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -1236,6 +1439,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", + "deprecated": "Just use Node.js's crypto.timingSafeEqual()", + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", @@ -1424,6 +1634,24 @@ "node": ">=0.6" } }, + "node_modules/twilio": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-6.0.2.tgz", + "integrity": "sha512-RN3TZxUtxLz2HBZVt62+LdZxQbrMVgYKtuzLgwmO7nqKvR+gQS5mCackD9hf4Y7MmoK/bX7tCm7kaJC8kC8zFA==", + "license": "MIT", + "dependencies": { + "axios": "^1.13.5", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.3", + "qs": "^6.14.1", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1500,6 +1728,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package-lock.json b/package-lock.json index ab00616..e3ab43f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,11 @@ "dependencies": { "@temporalio/client": "^1.16.2", "bcryptjs": "^3.0.3", + "dotenv": "^17.4.2", "express": "^4.18.2", "jsonwebtoken": "^9.0.3", "pg": "^8.21.0", + "twilio": "^6.0.2", "uuid": "^14.0.0" } }, @@ -213,6 +215,41 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -243,6 +280,24 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -352,6 +407,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -388,6 +455,12 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -403,6 +476,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -422,6 +504,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -496,6 +590,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -593,6 +702,42 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -690,6 +835,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", @@ -722,6 +882,42 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1174,6 +1370,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -1248,6 +1453,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", + "deprecated": "Just use Node.js's crypto.timingSafeEqual()", + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", @@ -1436,6 +1648,24 @@ "node": ">=0.6" } }, + "node_modules/twilio": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-6.0.2.tgz", + "integrity": "sha512-RN3TZxUtxLz2HBZVt62+LdZxQbrMVgYKtuzLgwmO7nqKvR+gQS5mCackD9hf4Y7MmoK/bX7tCm7kaJC8kC8zFA==", + "license": "MIT", + "dependencies": { + "axios": "^1.13.5", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.3", + "qs": "^6.14.1", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1512,6 +1742,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index c61f6eb..e4078e2 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "dependencies": { "@temporalio/client": "^1.16.2", "bcryptjs": "^3.0.3", + "dotenv": "^17.4.2", "express": "^4.18.2", "jsonwebtoken": "^9.0.3", "pg": "^8.21.0", + "twilio": "^6.0.2", "uuid": "^14.0.0" } } diff --git a/testTwilio.js b/testTwilio.js new file mode 100644 index 0000000..ed05a02 --- /dev/null +++ b/testTwilio.js @@ -0,0 +1,15 @@ +const whatsappService = require('./backend/whatsappService'); + +async function test() { + console.log('Sending test token message...'); + const result = await whatsappService.sendTokenConfirmation( + '+918500176938', + 'ON-777', + 'CuraFlow Testing Clinic', + '04:30 PM', + 500 + ); + console.log('Result:', result); +} + +test();