Add Twilio WhatsApp integration
Build Curio HMS / build-and-deploy (push) Successful in 57s Details

This commit is contained in:
Deep Koluguri 2026-05-25 23:17:16 -04:00
parent 1b3911ee94
commit 75562d6ffe
11 changed files with 680 additions and 27 deletions

BIN
.gitignore vendored

Binary file not shown.

View File

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

View File

@ -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}!
async sendRescheduleAlert(patientPhone, tenantName, oldTime, newTime) {
this.notificationsSent.push({ phone: patientPhone, type: 'reschedule', timestamp: new Date() });
return await whatsappService.sendRescheduleOffer(patientPhone, tenantName, oldTime, newTime);
}
Would you like to move your ${oldTime} appointment to this earlier slot?
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
);
}
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 sendQueueUpdate(patientPhone, clinicName, position, eta) {
this.notificationsSent.push({ phone: patientPhone, type: 'queue', timestamp: new Date() });
return await whatsappService.sendQueueUpdate(patientPhone, clinicName, position, eta);
}
getRecentNotifications() {

View File

@ -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,

View File

@ -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('<Response></Response>');
} 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('<Response></Response>');
} 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' });
}
});

View File

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

View File

@ -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: /

237
node_modules/.package-lock.json generated vendored
View File

@ -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",

239
package-lock.json generated
View File

@ -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",

View File

@ -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"
}
}

15
testTwilio.js Normal file
View File

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