@peopl-health/nexus 1.1.6 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/controllers/conversationController.js +13 -2
- package/lib/controllers/templateController.js +69 -34
- package/lib/controllers/templateFlowController.js +108 -0
- package/lib/controllers/uploadController.js +82 -0
- package/lib/models/templateModel.js +72 -0
- package/lib/routes/index.js +5 -3
- package/lib/templates/predefinedTemplates.js +81 -0
- package/lib/templates/templateStructure.js +204 -0
- package/lib/utils/errorHandler.js +8 -0
- package/package.json +2 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
// Use mongoose models directly to avoid conflicts
|
|
2
|
+
const mongoose = require('mongoose');
|
|
2
3
|
const { fetchConversationData, processConversations } = require('../services/conversationService');
|
|
3
4
|
const { sendMessage } = require('../core/NexusMessaging');
|
|
4
5
|
|
|
6
|
+
const Message = mongoose.models.Message;
|
|
7
|
+
|
|
5
8
|
const getConversationController = async (req, res) => {
|
|
6
9
|
const startTime = Date.now();
|
|
7
10
|
console.log('Starting getConversationController at', new Date().toISOString());
|
|
@@ -15,7 +18,15 @@ const getConversationController = async (req, res) => {
|
|
|
15
18
|
|
|
16
19
|
console.log(`Pagination: page ${page}, limit ${limit}, skip ${skip}, filter: ${filter}`);
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
if (!Message) {
|
|
22
|
+
console.log('Message model not found, returning empty conversations list');
|
|
23
|
+
return res.status(200).json({
|
|
24
|
+
success: true,
|
|
25
|
+
conversations: [],
|
|
26
|
+
emptyDatabase: true
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
const messageCount = await Message.countDocuments({});
|
|
20
31
|
console.log('Total message count:', messageCount);
|
|
21
32
|
|
|
@@ -1,23 +1,51 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// Nexus provider will be injected - templates only work with Twilio
|
|
2
|
+
let nexusProvider = null;
|
|
3
|
+
|
|
4
|
+
// Configure Nexus provider
|
|
5
|
+
const configureNexusProvider = (provider) => {
|
|
6
|
+
nexusProvider = provider;
|
|
7
7
|
};
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
|
|
9
|
+
// Check if provider supports templates
|
|
10
|
+
const checkTemplateSupport = () => {
|
|
11
|
+
if (!nexusProvider) {
|
|
12
|
+
throw new Error('Nexus provider not configured');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Only Twilio provider supports template operations
|
|
16
|
+
if (!nexusProvider.listTemplates || typeof nexusProvider.listTemplates !== 'function') {
|
|
17
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
18
|
+
}
|
|
11
19
|
};
|
|
20
|
+
const mongoose = require('mongoose');
|
|
21
|
+
const { handleApiError } = require('../utils/errorHandler');
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
const { Template } = require('../templates/templateStructure');
|
|
24
|
+
const predefinedTemplates = require('../templates/predefinedTemplates');
|
|
25
|
+
|
|
26
|
+
// Use mongoose models to avoid conflicts
|
|
27
|
+
const getTemplateModel = () => {
|
|
28
|
+
if (mongoose.models.Template) {
|
|
29
|
+
return mongoose.models.Template;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If not in mongoose.models, require and return the model
|
|
33
|
+
try {
|
|
34
|
+
const TemplateModel = require('../models/templateModel');
|
|
35
|
+
return TemplateModel;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to load Template model:', error);
|
|
38
|
+
// Return a stub model with required methods
|
|
39
|
+
return {
|
|
40
|
+
deleteMany: () => Promise.resolve({ deletedCount: 0 }),
|
|
41
|
+
find: () => ({ sort: () => ({ limit: () => ({ lean: () => Promise.resolve([]) }) }) }),
|
|
42
|
+
findOne: () => Promise.resolve(null),
|
|
43
|
+
create: () => Promise.resolve({}),
|
|
44
|
+
updateOne: () => Promise.resolve({}),
|
|
45
|
+
deleteOne: () => Promise.resolve({})
|
|
46
|
+
};
|
|
47
|
+
}
|
|
19
48
|
};
|
|
20
|
-
const predefinedTemplates = [];
|
|
21
49
|
|
|
22
50
|
/**
|
|
23
51
|
* Create a new template and store it in both Twilio and our database
|
|
@@ -109,6 +137,7 @@ const createTemplate = async (req, res) => {
|
|
|
109
137
|
dateCreated = currentDate;
|
|
110
138
|
}
|
|
111
139
|
|
|
140
|
+
const TemplateModel = getTemplateModel();
|
|
112
141
|
const newDbTemplate = await TemplateModel.create({
|
|
113
142
|
sid: twilioContent.sid,
|
|
114
143
|
name: (twilioContent.friendlyName || `template_${twilioContent.sid}`).replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(),
|
|
@@ -152,8 +181,13 @@ const createTemplate = async (req, res) => {
|
|
|
152
181
|
const listTemplates = async (req, res) => {
|
|
153
182
|
try {
|
|
154
183
|
const { status: queryStatus, type, limit = 50, showFlows: queryShowFlows } = req.query;
|
|
184
|
+
const TemplateModel = getTemplateModel();
|
|
155
185
|
|
|
156
|
-
|
|
186
|
+
console.log('nexusProvider:', nexusProvider ? 'configured' : 'not configured');
|
|
187
|
+
console.log('nexusProvider methods:', nexusProvider ? Object.keys(nexusProvider) : 'none');
|
|
188
|
+
|
|
189
|
+
checkTemplateSupport();
|
|
190
|
+
const twilioRawTemplates = await nexusProvider.listTemplates({ limit: parseInt(limit, 10) });
|
|
157
191
|
|
|
158
192
|
const showFlows = type === 'flow' || queryShowFlows === 'true';
|
|
159
193
|
const filteredTwilioTemplates = twilioRawTemplates.filter(template => {
|
|
@@ -176,7 +210,7 @@ const listTemplates = async (req, res) => {
|
|
|
176
210
|
};
|
|
177
211
|
|
|
178
212
|
try {
|
|
179
|
-
const approvalInfo = await
|
|
213
|
+
const approvalInfo = await nexusProvider.checkApprovalStatus(twilioTemplate.sid);
|
|
180
214
|
if (approvalInfo && approvalInfo.approvalRequest) {
|
|
181
215
|
const reqData = approvalInfo.approvalRequest;
|
|
182
216
|
updateFields.approvalRequest = {
|
|
@@ -260,10 +294,12 @@ const getTemplate = async (req, res) => {
|
|
|
260
294
|
try {
|
|
261
295
|
const { id } = req.params;
|
|
262
296
|
|
|
297
|
+
const TemplateModel = getTemplateModel();
|
|
263
298
|
let template = await TemplateModel.findOne({ sid: id });
|
|
264
299
|
let twilioTemplate;
|
|
265
300
|
|
|
266
|
-
|
|
301
|
+
checkTemplateSupport();
|
|
302
|
+
twilioTemplate = await nexusProvider.getTemplate(id);
|
|
267
303
|
|
|
268
304
|
if (!template) {
|
|
269
305
|
// If template wasn't in our database, create it based on Twilio data
|
|
@@ -345,7 +381,8 @@ const submitForApproval = async (req, res) => {
|
|
|
345
381
|
const approvalName = `${name || 'template'}_${timestamp}_${randomSuffix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
346
382
|
const approvalCategory = category || 'UTILITY';
|
|
347
383
|
|
|
348
|
-
|
|
384
|
+
checkTemplateSupport();
|
|
385
|
+
const response = await nexusProvider.submitForApproval(contentSid, approvalName, approvalCategory);
|
|
349
386
|
|
|
350
387
|
const dateCreated = response.date_created || response.dateCreated;
|
|
351
388
|
const dateUpdated = response.date_updated || response.dateUpdated || dateCreated;
|
|
@@ -356,6 +393,7 @@ const submitForApproval = async (req, res) => {
|
|
|
356
393
|
const validSubmittedDate = !isNaN(submittedDate.getTime()) ? submittedDate : new Date();
|
|
357
394
|
const validUpdatedDate = !isNaN(updatedDate.getTime()) ? updatedDate : new Date();
|
|
358
395
|
|
|
396
|
+
const TemplateModel = getTemplateModel();
|
|
359
397
|
await TemplateModel.updateOne(
|
|
360
398
|
{ sid: contentSid },
|
|
361
399
|
{
|
|
@@ -397,10 +435,12 @@ const checkApprovalStatus = async (req, res) => {
|
|
|
397
435
|
return res.status(400).json({ success: false, error: 'Content SID is required' });
|
|
398
436
|
}
|
|
399
437
|
|
|
438
|
+
const TemplateModel = getTemplateModel();
|
|
400
439
|
const dbTemplate = await TemplateModel.findOne({ sid: contentSid });
|
|
401
440
|
|
|
402
441
|
try {
|
|
403
|
-
|
|
442
|
+
checkTemplateSupport();
|
|
443
|
+
const status = await nexusProvider.checkApprovalStatus(contentSid);
|
|
404
444
|
|
|
405
445
|
if (dbTemplate) {
|
|
406
446
|
// Use approval status as the authoritative source if available
|
|
@@ -475,8 +515,10 @@ const deleteTemplate = async (req, res) => {
|
|
|
475
515
|
try {
|
|
476
516
|
const { id } = req.params;
|
|
477
517
|
|
|
478
|
-
|
|
518
|
+
checkTemplateSupport();
|
|
519
|
+
await nexusProvider.deleteTemplate(id);
|
|
479
520
|
|
|
521
|
+
const TemplateModel = getTemplateModel();
|
|
480
522
|
await TemplateModel.deleteOne({ sid: id });
|
|
481
523
|
|
|
482
524
|
return res.json({
|
|
@@ -529,6 +571,7 @@ const getCompleteTemplate = async (req, res) => {
|
|
|
529
571
|
try {
|
|
530
572
|
const { sid } = req.params;
|
|
531
573
|
|
|
574
|
+
const TemplateModel = getTemplateModel();
|
|
532
575
|
let template = await TemplateModel.findOne({ sid }).lean();
|
|
533
576
|
|
|
534
577
|
if (!template) {
|
|
@@ -540,7 +583,8 @@ const getCompleteTemplate = async (req, res) => {
|
|
|
540
583
|
|
|
541
584
|
if (!template.body || !template.variables || template.variables.length === 0) {
|
|
542
585
|
try {
|
|
543
|
-
|
|
586
|
+
checkTemplateSupport();
|
|
587
|
+
const twilioTemplate = await nexusProvider.getTemplate(sid);
|
|
544
588
|
console.log('Fetched template from Twilio:', twilioTemplate);
|
|
545
589
|
|
|
546
590
|
let body = '';
|
|
@@ -603,6 +647,7 @@ const getCompleteTemplate = async (req, res) => {
|
|
|
603
647
|
updateData.actions = template.actions;
|
|
604
648
|
}
|
|
605
649
|
|
|
650
|
+
const TemplateModel = getTemplateModel();
|
|
606
651
|
await TemplateModel.updateOne(
|
|
607
652
|
{ sid },
|
|
608
653
|
{ $set: updateData }
|
|
@@ -628,15 +673,6 @@ const getCompleteTemplate = async (req, res) => {
|
|
|
628
673
|
}
|
|
629
674
|
};
|
|
630
675
|
|
|
631
|
-
// Flow functions (inline since templateFlowController was removed)
|
|
632
|
-
const createFlow = async (req, res) => {
|
|
633
|
-
return res.status(501).json({ success: false, error: 'Flow creation not available' });
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
const deleteFlow = async (req, res) => {
|
|
637
|
-
return res.status(501).json({ success: false, error: 'Flow deletion not available' });
|
|
638
|
-
};
|
|
639
|
-
|
|
640
676
|
module.exports = {
|
|
641
677
|
createTemplate,
|
|
642
678
|
listTemplates,
|
|
@@ -646,6 +682,5 @@ module.exports = {
|
|
|
646
682
|
checkApprovalStatus,
|
|
647
683
|
deleteTemplate,
|
|
648
684
|
getCompleteTemplate,
|
|
649
|
-
|
|
650
|
-
deleteFlow
|
|
685
|
+
configureNexusProvider
|
|
651
686
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Nexus provider will be injected
|
|
2
|
+
let nexusProvider = null;
|
|
3
|
+
|
|
4
|
+
// Configure Nexus provider
|
|
5
|
+
const configureNexusProvider = (provider) => {
|
|
6
|
+
nexusProvider = provider;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Check if provider supports templates
|
|
10
|
+
const checkTemplateSupport = () => {
|
|
11
|
+
if (!nexusProvider) {
|
|
12
|
+
throw new Error('Nexus provider not configured. Call configureNexusProvider() first.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!nexusProvider.createTemplate || typeof nexusProvider.createTemplate !== 'function') {
|
|
16
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const { handleApiError } = require('../utils/errorHandler');
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
const createFlow = async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const { friendlyName, language, flowType, body, buttonText, subtitle, pages } = req.body;
|
|
25
|
+
|
|
26
|
+
const missingParams = [];
|
|
27
|
+
if (!friendlyName) missingParams.push('friendlyName');
|
|
28
|
+
if (!language) missingParams.push('language');
|
|
29
|
+
if (!pages || !Array.isArray(pages)) missingParams.push('pages');
|
|
30
|
+
|
|
31
|
+
if (missingParams.length > 0) {
|
|
32
|
+
return res.status(400).json({
|
|
33
|
+
success: false,
|
|
34
|
+
error: `Missing required parameters: ${missingParams.join(', ')}`
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Generate unique identifiers for anti-rejection
|
|
39
|
+
const timestamp = Date.now().toString();
|
|
40
|
+
const uniqueId = require('uuid').v4().substring(0, 6);
|
|
41
|
+
const uniqueFriendlyName = `${friendlyName}_${timestamp}_${uniqueId}`;
|
|
42
|
+
|
|
43
|
+
// Match Twilio's expected structure for flex templates
|
|
44
|
+
checkTemplateSupport();
|
|
45
|
+
const content = await nexusProvider.createTemplate({
|
|
46
|
+
friendly_name: uniqueFriendlyName,
|
|
47
|
+
language,
|
|
48
|
+
categories: ['UTILITY'],
|
|
49
|
+
types: {
|
|
50
|
+
'twilio/flows': {
|
|
51
|
+
body: body,
|
|
52
|
+
button_text: buttonText,
|
|
53
|
+
subtitle: subtitle || null,
|
|
54
|
+
pages: pages,
|
|
55
|
+
type: flowType
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return res.status(201).json({
|
|
61
|
+
success: true,
|
|
62
|
+
message: 'Flow template created successfully',
|
|
63
|
+
contentSid: content.sid,
|
|
64
|
+
friendlyName: content.friendly_name,
|
|
65
|
+
approvalStatus: 'Pending review',
|
|
66
|
+
approvalLinks: content.links
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return handleApiError(res, error, 'Error creating WhatsApp flow template');
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
const deleteFlow = async (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
const { sid: contentSid } = req.params;
|
|
77
|
+
|
|
78
|
+
if (!contentSid) {
|
|
79
|
+
return res.status(400).json({
|
|
80
|
+
success: false,
|
|
81
|
+
error: 'Content SID is required'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
checkTemplateSupport();
|
|
86
|
+
await nexusProvider.deleteTemplate(contentSid);
|
|
87
|
+
|
|
88
|
+
return res.status(200).json({
|
|
89
|
+
success: true,
|
|
90
|
+
message: 'Template deleted successfully'
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error.code === 20404) {
|
|
94
|
+
return res.status(404).json({
|
|
95
|
+
success: false,
|
|
96
|
+
error: 'Template not found'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return handleApiError(res, error, 'Error deleting template');
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
createFlow,
|
|
106
|
+
deleteFlow,
|
|
107
|
+
configureNexusProvider
|
|
108
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const multer = require('multer');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
const { generatePresignedUrl, uploadBufferToS3 } = require('../config/awsConfig');
|
|
4
|
+
const bucketName = process.env.AWS_S3_BUCKET_NAME;
|
|
5
|
+
|
|
6
|
+
const storage = multer.memoryStorage();
|
|
7
|
+
|
|
8
|
+
const fileFilter = (req, file, cb) => {
|
|
9
|
+
if (file.mimetype.startsWith('image/') ||
|
|
10
|
+
file.mimetype.startsWith('application/') ||
|
|
11
|
+
file.mimetype.startsWith('audio/') ||
|
|
12
|
+
file.mimetype.startsWith('video/')) {
|
|
13
|
+
cb(null, true);
|
|
14
|
+
} else {
|
|
15
|
+
cb(new Error('Unsupported file type'), false);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const upload = multer({
|
|
20
|
+
storage: storage,
|
|
21
|
+
fileFilter: fileFilter,
|
|
22
|
+
limits: {
|
|
23
|
+
fileSize: 50 * 1024 * 1024, // Increased to 50MB
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const handleFileUpload = async (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
if (!req.file) {
|
|
30
|
+
return res.status(400).json({
|
|
31
|
+
success: false,
|
|
32
|
+
error: 'No file provided'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const file = req.file;
|
|
37
|
+
|
|
38
|
+
let filePrefix = 'document';
|
|
39
|
+
if (file.mimetype.startsWith('image/')) {
|
|
40
|
+
filePrefix = 'image';
|
|
41
|
+
} else if (file.mimetype.startsWith('video/')) {
|
|
42
|
+
filePrefix = 'video';
|
|
43
|
+
} else if (file.mimetype.startsWith('audio/')) {
|
|
44
|
+
filePrefix = 'audio';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const uniqueId = uuidv4();
|
|
48
|
+
const originalFileName = file.originalname;
|
|
49
|
+
const sanitizedFileName = file.originalname.replace(/[^a-zA-Z0-9.]/g, '_').replace('.pdf', '');
|
|
50
|
+
|
|
51
|
+
const key = `${filePrefix}/${sanitizedFileName}_${uniqueId}`;
|
|
52
|
+
|
|
53
|
+
await uploadBufferToS3(file.buffer, bucketName, key, file.mimetype, true);
|
|
54
|
+
const whatsappExpirationSeconds = 7 * 24 * 60 * 60;
|
|
55
|
+
const presignedUrl = await generatePresignedUrl(bucketName, key, whatsappExpirationSeconds);
|
|
56
|
+
|
|
57
|
+
res.status(200).json({
|
|
58
|
+
success: true,
|
|
59
|
+
message: 'File uploaded successfully',
|
|
60
|
+
fileUrl: presignedUrl,
|
|
61
|
+
key: key,
|
|
62
|
+
fileType: filePrefix,
|
|
63
|
+
fileName: originalFileName,
|
|
64
|
+
sanitizedFileName: sanitizedFileName,
|
|
65
|
+
fileSize: file.size,
|
|
66
|
+
contentType: file.mimetype
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Error uploading file:', error);
|
|
70
|
+
res.status(500).json({
|
|
71
|
+
success: false,
|
|
72
|
+
error: error.message || 'Failed to upload file'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const uploadSingleFile = upload.single('file');
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
uploadSingleFile,
|
|
81
|
+
handleFileUpload
|
|
82
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const TemplateSchema = new mongoose.Schema({
|
|
4
|
+
sid: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
unique: true
|
|
8
|
+
},
|
|
9
|
+
name: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true
|
|
12
|
+
},
|
|
13
|
+
friendlyName: String,
|
|
14
|
+
category: {
|
|
15
|
+
type: String,
|
|
16
|
+
default: 'UTILITY',
|
|
17
|
+
enum: ['UTILITY', 'MARKETING', 'AUTHENTICATION']
|
|
18
|
+
},
|
|
19
|
+
language: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: 'es'
|
|
22
|
+
},
|
|
23
|
+
status: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: 'DRAFT',
|
|
26
|
+
enum: ['DRAFT', 'PENDING', 'APPROVED', 'REJECTED', 'UNKNOWN']
|
|
27
|
+
},
|
|
28
|
+
body: String,
|
|
29
|
+
footer: String,
|
|
30
|
+
variables: [
|
|
31
|
+
{
|
|
32
|
+
name: String,
|
|
33
|
+
description: String,
|
|
34
|
+
example: String
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
buttons: [
|
|
38
|
+
{
|
|
39
|
+
type: {
|
|
40
|
+
type: String,
|
|
41
|
+
enum: ['quick_reply', 'url']
|
|
42
|
+
},
|
|
43
|
+
text: String,
|
|
44
|
+
url: String
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
approvalRequest: {
|
|
48
|
+
sid: String,
|
|
49
|
+
status: String,
|
|
50
|
+
dateSubmitted: Date,
|
|
51
|
+
dateUpdated: Date,
|
|
52
|
+
rejectionReason: String
|
|
53
|
+
},
|
|
54
|
+
dateCreated: {
|
|
55
|
+
type: Date,
|
|
56
|
+
default: Date.now
|
|
57
|
+
},
|
|
58
|
+
lastUpdated: {
|
|
59
|
+
type: Date,
|
|
60
|
+
default: Date.now
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Pre-save middleware to update lastUpdated
|
|
65
|
+
TemplateSchema.pre('save', function(next) {
|
|
66
|
+
this.lastUpdated = new Date();
|
|
67
|
+
next();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const Template = mongoose.model('Template', TemplateSchema);
|
|
71
|
+
|
|
72
|
+
module.exports = Template;
|
package/lib/routes/index.js
CHANGED
|
@@ -70,6 +70,8 @@ const conversationController = require('../controllers/conversationController');
|
|
|
70
70
|
const mediaController = require('../controllers/mediaController');
|
|
71
71
|
const messageController = require('../controllers/messageController');
|
|
72
72
|
const templateController = require('../controllers/templateController');
|
|
73
|
+
const templateFlowController = require('../controllers/templateFlowController');
|
|
74
|
+
const uploadController = require('../controllers/uploadController');
|
|
73
75
|
|
|
74
76
|
// Built-in controllers mapping
|
|
75
77
|
const builtInControllers = {
|
|
@@ -95,7 +97,7 @@ const builtInControllers = {
|
|
|
95
97
|
|
|
96
98
|
// Media controllers
|
|
97
99
|
getMediaController: mediaController.getMediaController,
|
|
98
|
-
handleFileUpload:
|
|
100
|
+
handleFileUpload: uploadController.handleFileUpload,
|
|
99
101
|
|
|
100
102
|
// Message controllers
|
|
101
103
|
sendMessageController: messageController.sendMessageController,
|
|
@@ -108,8 +110,8 @@ const builtInControllers = {
|
|
|
108
110
|
getPredefinedTemplates: templateController.getPredefinedTemplates,
|
|
109
111
|
getTemplate: templateController.getTemplate,
|
|
110
112
|
getCompleteTemplate: templateController.getCompleteTemplate,
|
|
111
|
-
createFlow:
|
|
112
|
-
deleteFlow:
|
|
113
|
+
createFlow: templateFlowController.createFlow,
|
|
114
|
+
deleteFlow: templateFlowController.deleteFlow,
|
|
113
115
|
submitForApproval: templateController.submitForApproval,
|
|
114
116
|
checkApprovalStatus: templateController.checkApprovalStatus,
|
|
115
117
|
deleteTemplate: templateController.deleteTemplate
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { Template } = require('./templateStructure');
|
|
2
|
+
|
|
3
|
+
const predefinedTemplates = {
|
|
4
|
+
citaDrLimon: () => {
|
|
5
|
+
const template = new Template('cita_dr_limon', 'UTILITY', 'es');
|
|
6
|
+
|
|
7
|
+
const patientVars = [
|
|
8
|
+
{
|
|
9
|
+
name: 'Nombre del paciente',
|
|
10
|
+
description: 'Nombre completo del paciente',
|
|
11
|
+
example: 'Juan Pérez'
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'Fecha de la cita',
|
|
15
|
+
description: 'Fecha en formato DD/MM',
|
|
16
|
+
example: '18/05'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'Hora de la cita',
|
|
20
|
+
description: 'Hora en formato HH:MM am/pm',
|
|
21
|
+
example: '9:30 am'
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
template.addBodyVariation(
|
|
26
|
+
'Hola buenos días {{1}}, \n\nSoy Briggite del equipo del Dr Limón \n\n📆 Tienes programado una cita con el dr Limón para mañana {{2}} a las {{3}}\n\n✅ Confirma tu asistencia por favor',
|
|
27
|
+
patientVars
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
template.addBodyVariation(
|
|
31
|
+
'Hola {{1}}, buenos días\n\nLe saluda Briggite del equipo médico del Dr Limón\n\n📆 Su cita con el Dr Limón está programada para mañana {{2}} a las {{3}}\n\n✅ Por favor confirme su asistencia',
|
|
32
|
+
patientVars
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
template.addBodyVariation(
|
|
36
|
+
'Buenos días {{1}}\n\nSoy Briggite, asistente del Dr Limón\n\n📆 Le recordamos su cita para mañana {{2}} a las {{3}} con el Dr Limón\n\n✅ Agradecemos su confirmación',
|
|
37
|
+
patientVars
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
template.setBody('Hola buenos días {{1}}, \n\nSoy Briggite del equipo del Dr Limón \n\n📆 Tienes programado una cita con el dr Limón para mañana {{2}} a las {{3}}\n\n✅ Confirma tu asistencia por favor',
|
|
41
|
+
patientVars);
|
|
42
|
+
|
|
43
|
+
return template;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
seguimientoDrLimon: () => {
|
|
47
|
+
const template = new Template('seguimiento_dr_limon', 'UTILITY', 'es');
|
|
48
|
+
|
|
49
|
+
const followUpVars = [
|
|
50
|
+
{
|
|
51
|
+
name: 'Nombre del paciente',
|
|
52
|
+
description: 'Nombre completo del paciente',
|
|
53
|
+
example: 'Carmen Rodríguez'
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
template.addBodyVariation(
|
|
58
|
+
'Hola {{1}} buenos días ¿cómo han amanecido? ¿cómo les fue con las recomendaciones?',
|
|
59
|
+
followUpVars
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
template.addBodyVariation(
|
|
63
|
+
'Buenos días {{1}}, ¿cómo se encuentra hoy? ¿ha podido seguir las recomendaciones médicas?',
|
|
64
|
+
followUpVars
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
template.addBodyVariation(
|
|
68
|
+
'Hola {{1}}, buenos días. Seguimiento médico: ¿cómo se ha sentido? ¿pudo implementar las recomendaciones indicadas?',
|
|
69
|
+
followUpVars
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
template.setBody('Hola {{1}} buenos días ¿cómo han amanecido? ¿cómo les fue con las recomendaciones?',
|
|
73
|
+
followUpVars);
|
|
74
|
+
|
|
75
|
+
return template;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
predefinedTemplates
|
|
81
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
|
|
3
|
+
// Nexus provider will be injected
|
|
4
|
+
let nexusProvider = null;
|
|
5
|
+
|
|
6
|
+
// Configure Nexus provider
|
|
7
|
+
const configureNexusProvider = (provider) => {
|
|
8
|
+
nexusProvider = provider;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Check if provider supports templates
|
|
12
|
+
const checkTemplateSupport = () => {
|
|
13
|
+
if (!nexusProvider) {
|
|
14
|
+
throw new Error('Nexus provider not configured. Call configureNexusProvider() first.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!nexusProvider.createTemplate || typeof nexusProvider.createTemplate !== 'function') {
|
|
18
|
+
throw new Error('Template operations are only supported with Twilio provider');
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
class Template {
|
|
23
|
+
constructor(name, category = 'UTILITY', language = 'es') {
|
|
24
|
+
this.name = name;
|
|
25
|
+
this.category = category.toUpperCase();
|
|
26
|
+
this.language = language;
|
|
27
|
+
this.components = {
|
|
28
|
+
header: [],
|
|
29
|
+
body: [],
|
|
30
|
+
footer: [],
|
|
31
|
+
buttons: []
|
|
32
|
+
};
|
|
33
|
+
this.variables = [];
|
|
34
|
+
|
|
35
|
+
const timestamp = Date.now().toString();
|
|
36
|
+
const uniqueId = uuidv4().substring(0, 6);
|
|
37
|
+
this.friendlyName = `${name}_${timestamp}_${uniqueId}`;
|
|
38
|
+
this.templateName = `${name}_${Date.now().toString().substring(0, 10)}`;
|
|
39
|
+
this.approvalName = this.name || `template_${name}_${Math.floor(Math.random() * 1000)}`;
|
|
40
|
+
this.status = 'DRAFT';
|
|
41
|
+
|
|
42
|
+
this.variations = [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
addBodyVariation(text, variableDescriptions = []) {
|
|
46
|
+
this.variations.push({
|
|
47
|
+
text: text,
|
|
48
|
+
variables: variableDescriptions
|
|
49
|
+
});
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setBody(text, variableDescriptions = []) {
|
|
54
|
+
this.addBodyVariation(text, variableDescriptions);
|
|
55
|
+
|
|
56
|
+
const selectedVariation = this.variations.length > 1 ?
|
|
57
|
+
this.variations[Math.floor(Math.random() * this.variations.length)] :
|
|
58
|
+
{ text, variables: variableDescriptions };
|
|
59
|
+
|
|
60
|
+
const enhancedText = `${selectedVariation.text}`;
|
|
61
|
+
|
|
62
|
+
const processedVariables = selectedVariation.variables.map((desc, i) => {
|
|
63
|
+
if (typeof desc === 'string') {
|
|
64
|
+
return {
|
|
65
|
+
name: `var_${i + 1}`,
|
|
66
|
+
description: desc,
|
|
67
|
+
example: `Ejemplo ${i + 1}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name: desc.name || `var_${i + 1}`,
|
|
72
|
+
description: desc.description || `Variable ${i + 1}`,
|
|
73
|
+
example: desc.example || `Ejemplo ${i + 1}`
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.components.body = [{
|
|
78
|
+
type: 'BODY',
|
|
79
|
+
text: enhancedText,
|
|
80
|
+
example: {
|
|
81
|
+
body_text: processedVariables.map(v => v.example)
|
|
82
|
+
}
|
|
83
|
+
}];
|
|
84
|
+
|
|
85
|
+
this.variables = processedVariables;
|
|
86
|
+
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
addQuickReply(text) {
|
|
91
|
+
if (this.components.buttons.length >= 3) {
|
|
92
|
+
throw new Error('Maximum of 3 buttons allowed');
|
|
93
|
+
}
|
|
94
|
+
this.components.buttons.push({
|
|
95
|
+
type: 'QUICK_REPLY',
|
|
96
|
+
text: text
|
|
97
|
+
});
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
addCallToAction(text, url) {
|
|
102
|
+
if (this.components.buttons.length >= 3) {
|
|
103
|
+
throw new Error('Maximum of 3 buttons allowed');
|
|
104
|
+
}
|
|
105
|
+
this.components.buttons.push({
|
|
106
|
+
type: 'URL',
|
|
107
|
+
text: text,
|
|
108
|
+
url: url
|
|
109
|
+
});
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setFooter(text) {
|
|
114
|
+
if (text.length > 60) {
|
|
115
|
+
throw new Error('Footer text must be 60 characters or less');
|
|
116
|
+
}
|
|
117
|
+
this.components.footer = [{
|
|
118
|
+
type: 'FOOTER',
|
|
119
|
+
text: text
|
|
120
|
+
}];
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
generateTemplateName(baseName) {
|
|
125
|
+
const timestamp = new Date().toISOString().replace(/[^0-9]/g, '').slice(-8);
|
|
126
|
+
return `${baseName}_${timestamp}`.toUpperCase();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
validate() {
|
|
130
|
+
if (!this.components.body || !this.components.body[0]?.text) {
|
|
131
|
+
throw new Error('Template body is required');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const varMatches = this.components.body[0].text.match(/\{\{\d+\}\}/g) || [];
|
|
135
|
+
const varCount = new Set(varMatches.map(m => m.match(/\d+/)[0])).size;
|
|
136
|
+
|
|
137
|
+
if (varCount !== this.variables.length) {
|
|
138
|
+
throw new Error(`Mismatch between variable placeholders (${varCount}) and variable definitions (${this.variables.length})`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
toTwilioFormat() {
|
|
145
|
+
this.validate();
|
|
146
|
+
|
|
147
|
+
const variables = {};
|
|
148
|
+
this.variables.forEach((variable, i) => {
|
|
149
|
+
variables[`${i + 1}`] = variable.example;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const template = {
|
|
153
|
+
friendly_name: this.friendlyName,
|
|
154
|
+
language: this.language,
|
|
155
|
+
variables: variables,
|
|
156
|
+
types: {
|
|
157
|
+
'twilio/text': {
|
|
158
|
+
body: this.components.body[0].text
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (this.category) {
|
|
164
|
+
template.categories = [this.category];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (this.components.buttons && this.components.buttons.length > 0) {
|
|
168
|
+
const actions = this.components.buttons.map((button, index) => {
|
|
169
|
+
return {
|
|
170
|
+
title: button.text,
|
|
171
|
+
id: `button_${index + 1}`
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
template.types['twilio/quick-reply'] = {
|
|
176
|
+
body: this.components.body[0].text,
|
|
177
|
+
actions: actions
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.components.footer && this.components.footer.length > 0) {
|
|
182
|
+
const textTypes = ['twilio/text', 'twilio/quick-reply'];
|
|
183
|
+
textTypes.forEach(type => {
|
|
184
|
+
if (template.types[type]) {
|
|
185
|
+
template.types[type].footer = this.components.footer[0].text;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return template;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async save() {
|
|
194
|
+
checkTemplateSupport();
|
|
195
|
+
const twilioFormat = this.toTwilioFormat();
|
|
196
|
+
const createdTemplate = await nexusProvider.createTemplate(twilioFormat);
|
|
197
|
+
return createdTemplate;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
Template,
|
|
203
|
+
configureNexusProvider
|
|
204
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peopl-health/nexus",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "Core messaging and assistant library for WhatsApp communication platforms",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"dotenv": "^16.4.7",
|
|
57
57
|
"moment-timezone": "^0.5.43",
|
|
58
58
|
"mongoose": "^7.5.0",
|
|
59
|
+
"multer": "1.4.5-lts.1",
|
|
59
60
|
"pdf-lib": "1.17.1",
|
|
60
61
|
"pino": "^8.15.0",
|
|
61
62
|
"pino-pretty": "^10.2.0",
|