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();