@peopl-health/nexus 1.0.3 → 1.1.0

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.
@@ -0,0 +1,631 @@
1
+ const TwilioService = require('../services/twilioService');
2
+ const { handleApiError } = require('../utils/errorHandler');
3
+
4
+ // Import flow functions from templateFlowController
5
+ const { createFlow, deleteFlow } = require('./templateFlowController');
6
+ const { Template } = require('../templates/templateStructure');
7
+ const TemplateModel = require('../models/templateModel');
8
+ const predefinedTemplates = require('../templates/predefinedTemplates');
9
+
10
+ /**
11
+ * Create a new template and store it in both Twilio and our database
12
+ */
13
+ const createTemplate = async (req, res) => {
14
+ try {
15
+ const { name, category, language, body, variables, footer, buttons, templateId } = req.body;
16
+
17
+ let template;
18
+
19
+ // Handle predefined templates if a templateId is provided
20
+ if (templateId) {
21
+ const templateFn = predefinedTemplates[templateId];
22
+ if (!templateFn) {
23
+ return res.status(404).json({
24
+ success: false,
25
+ error: `Predefined template ${templateId} not found`
26
+ });
27
+ }
28
+ template = templateFn();
29
+ } else {
30
+ // Validate required fields
31
+ if (!name || !language) {
32
+ return res.status(400).json({
33
+ success: false,
34
+ error: 'Name and language are required for creating a template'
35
+ });
36
+ }
37
+
38
+ // Create a new template
39
+ const timestamp = Date.now().toString().substring(0, 10);
40
+ const templateName = `${name}_${timestamp}`;
41
+ template = new Template(templateName, category, language);
42
+
43
+ if (!body) return res.status(400).json({ success: false, error: 'Template body is required' });
44
+
45
+ template.setBody(body, variables);
46
+ if (footer) template.setFooter(footer);
47
+
48
+ if (buttons && Array.isArray(buttons)) {
49
+ buttons.forEach(button => {
50
+ if (button.type === 'quick_reply') template.addQuickReply(button.text);
51
+ else if (button.type === 'url') template.addCallToAction(button.text, button.url);
52
+ });
53
+ }
54
+ }
55
+
56
+ // Save to Twilio
57
+ const twilioContent = await template.save();
58
+
59
+ // Save to our database
60
+ let processedVariables = [];
61
+
62
+ // Handle variables - can be array or object format
63
+ if (variables) {
64
+ if (Array.isArray(variables)) {
65
+ // Handle array format
66
+ processedVariables = variables.map((variable, index) => {
67
+ if (typeof variable === 'string') {
68
+ return {
69
+ name: `var_${index + 1}`,
70
+ description: variable,
71
+ example: `Example ${index + 1}`
72
+ };
73
+ }
74
+ return variable;
75
+ });
76
+ } else if (typeof variables === 'object') {
77
+ // Handle object format with keys as variable placeholders
78
+ processedVariables = Object.entries(variables).map(([key, value]) => {
79
+ return {
80
+ name: `var_${key}`,
81
+ description: value,
82
+ example: `Example ${key}`
83
+ };
84
+ });
85
+ }
86
+ }
87
+
88
+ // CRITICAL FIX: Save the new template to the local MongoDB
89
+ // Ensure we have valid dates for created and updated
90
+ const currentDate = new Date();
91
+ let dateCreated = twilioContent.dateCreated ? new Date(twilioContent.dateCreated) : currentDate;
92
+ const lastUpdated = currentDate;
93
+
94
+ // Make sure the dates are valid before saving
95
+ if (isNaN(dateCreated.getTime())) {
96
+ console.log('Invalid dateCreated, using current date');
97
+ dateCreated = currentDate;
98
+ }
99
+
100
+ const newDbTemplate = await TemplateModel.create({
101
+ sid: twilioContent.sid,
102
+ name: (twilioContent.friendlyName || `template_${twilioContent.sid}`).replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(),
103
+ friendlyName: twilioContent.friendlyName,
104
+ category: category || 'UTILITY', // Use category from request, default if not provided
105
+ language: language, // Use language from request
106
+ status: twilioContent.status,
107
+ body: body, // Use body from request
108
+ footer: footer, // Use footer from request
109
+ variables: processedVariables, // Use processed variables from request
110
+ components: twilioContent.components || [], // Assuming components might come from Twilio response or be empty
111
+ dateCreated: dateCreated,
112
+ lastUpdated: lastUpdated
113
+ });
114
+
115
+ return res.status(201).json({
116
+ success: true,
117
+ message: 'Template created successfully',
118
+ template: {
119
+ id: newDbTemplate.sid, // Use newDbTemplate fields for response consistency
120
+ sid: newDbTemplate.sid,
121
+ friendlyName: newDbTemplate.friendlyName,
122
+ name: newDbTemplate.name, // This was original 'name', should be consistent with DB name
123
+ status: newDbTemplate.status,
124
+ variables: newDbTemplate.variables,
125
+ body: newDbTemplate.body,
126
+ footer: newDbTemplate.footer,
127
+ category: newDbTemplate.category,
128
+ language: newDbTemplate.language,
129
+ components: newDbTemplate.components
130
+ }
131
+ });
132
+ } catch (error) {
133
+ return handleApiError(res, error, 'Failed to create template');
134
+ }
135
+ };
136
+
137
+ /**
138
+ * List all templates from both Twilio and our database
139
+ */
140
+ const listTemplates = async (req, res) => {
141
+ try {
142
+ const { status: queryStatus, type, limit = 50, showFlows: queryShowFlows } = req.query;
143
+
144
+ const twilioRawTemplates = await TwilioService.listTemplates({ limit: parseInt(limit, 10) });
145
+
146
+ const showFlows = type === 'flow' || queryShowFlows === 'true';
147
+ const filteredTwilioTemplates = twilioRawTemplates.filter(template => {
148
+ const isFlow = template.types && template.types['twilio/flex'];
149
+ const statusMatch = queryStatus ? template.status === queryStatus : true;
150
+ return (showFlows || !isFlow) && statusMatch;
151
+ });
152
+
153
+ const validTwilioSids = filteredTwilioTemplates.map(t => t.sid);
154
+
155
+ await TemplateModel.deleteMany({ sid: { $nin: validTwilioSids } });
156
+
157
+ for (const twilioTemplate of filteredTwilioTemplates) {
158
+ const updateFields = {
159
+ friendlyName: twilioTemplate.friendlyName,
160
+ category: twilioTemplate.types?.['twilio/text']?.categories?.[0] || 'UTILITY',
161
+ language: twilioTemplate.language || 'es',
162
+ status: twilioTemplate.status || 'DRAFT',
163
+ lastUpdated: new Date(),
164
+ };
165
+
166
+ try {
167
+ const approvalInfo = await TwilioService.checkApprovalStatus(twilioTemplate.sid);
168
+ if (approvalInfo && approvalInfo.approvalRequest) {
169
+ const reqData = approvalInfo.approvalRequest;
170
+ updateFields.approvalRequest = {
171
+ sid: reqData.sid,
172
+ status: reqData.status || 'PENDING',
173
+ dateSubmitted: reqData.dateCreated ? new Date(reqData.dateCreated) : new Date(),
174
+ dateUpdated: reqData.dateUpdated ? new Date(reqData.dateUpdated) : new Date(),
175
+ rejectionReason: reqData.rejectionReason || ''
176
+ };
177
+ if (reqData.status === 'APPROVED') updateFields.status = 'APPROVED';
178
+ else if (reqData.status === 'REJECTED') updateFields.status = 'REJECTED';
179
+ else if (reqData.status === 'PENDING') updateFields.status = 'PENDING';
180
+ }
181
+ } catch (err) {
182
+ console.warn(`Could not fetch approval status for template ${twilioTemplate.sid}:`, err.message);
183
+ }
184
+
185
+ const onInsertFields = {
186
+ sid: twilioTemplate.sid,
187
+ name: (twilioTemplate.friendlyName || `template_${twilioTemplate.sid}`).replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(),
188
+ dateCreated: new Date(twilioTemplate.dateCreated),
189
+ body: '',
190
+ footer: '',
191
+ variables: [],
192
+ components: []
193
+ };
194
+
195
+ await TemplateModel.updateOne(
196
+ { sid: twilioTemplate.sid },
197
+ {
198
+ $set: updateFields,
199
+ $setOnInsert: onInsertFields
200
+ },
201
+ { upsert: true }
202
+ );
203
+ }
204
+
205
+ const findCriteria = { sid: { $in: validTwilioSids } };
206
+ if (queryStatus) {
207
+ findCriteria.status = queryStatus;
208
+ }
209
+
210
+ let finalTemplates = await TemplateModel.find(findCriteria)
211
+ .sort({ dateCreated: -1 })
212
+ .limit(parseInt(limit, 10))
213
+ .lean();
214
+
215
+ const formattedTemplates = finalTemplates.map(template => ({
216
+ id: template.sid,
217
+ sid: template.sid,
218
+ name: template.name,
219
+ friendlyName: template.friendlyName,
220
+ status: template.status,
221
+ language: template.language,
222
+ created: template.dateCreated,
223
+ updated: template.lastUpdated,
224
+ category: template.category,
225
+ body: template.body,
226
+ footer: template.footer,
227
+ variables: template.variables,
228
+ components: template.components,
229
+ approvalRequest: template.approvalRequest
230
+ }));
231
+
232
+ return res.status(200).json({
233
+ success: true,
234
+ templates: formattedTemplates,
235
+ count: formattedTemplates.length
236
+ });
237
+
238
+ } catch (error) {
239
+ console.error('Error in listTemplates:', error);
240
+ return handleApiError(res, error, 'Failed to list templates');
241
+ }
242
+ };
243
+
244
+ /**
245
+ * Get a specific template by ID
246
+ */
247
+ const getTemplate = async (req, res) => {
248
+ try {
249
+ const { id } = req.params;
250
+
251
+ let template = await TemplateModel.findOne({ sid: id });
252
+ let twilioTemplate;
253
+
254
+ twilioTemplate = await TwilioService.getTemplate(id);
255
+
256
+ if (!template) {
257
+ // If template wasn't in our database, create it based on Twilio data
258
+ template = await TemplateModel.create({
259
+ sid: twilioTemplate.sid,
260
+ name: (twilioTemplate.friendlyName || `template_${twilioTemplate.sid}`).replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(),
261
+ friendlyName: twilioTemplate.friendlyName,
262
+ category: twilioTemplate.types?.['twilio/text']?.categories?.[0] || 'UTILITY',
263
+ language: twilioTemplate.language,
264
+ status: twilioTemplate.status,
265
+ body: twilioTemplate.types?.['twilio/text']?.body || '',
266
+ variables: Object.keys(twilioTemplate.variables || {}).map(key => ({
267
+ name: key,
268
+ description: `Variable ${key}`,
269
+ example: twilioTemplate.variables[key] || `Example ${key}`
270
+ })),
271
+ dateCreated: new Date(twilioTemplate.dateCreated),
272
+ lastUpdated: new Date(twilioTemplate.dateUpdated || twilioTemplate.dateCreated)
273
+ });
274
+ } else {
275
+ // CRITICAL FIX: Update existing template with latest Twilio data more comprehensively
276
+ template.friendlyName = twilioTemplate.friendlyName || template.friendlyName;
277
+ template.language = twilioTemplate.language || template.language;
278
+ template.status = twilioTemplate.status || template.status;
279
+ template.category = twilioTemplate.types?.['twilio/text']?.categories?.[0] || template.category;
280
+ template.body = twilioTemplate.types?.['twilio/text']?.body || template.body;
281
+ template.variables = Object.keys(twilioTemplate.variables || {}).map(key => ({
282
+ name: key,
283
+ description: `Variable ${key}`,
284
+ example: twilioTemplate.variables[key] || `Example ${key}`
285
+ })) || template.variables;
286
+ template.lastUpdated = new Date(twilioTemplate.dateUpdated || Date.now());
287
+ await template.save();
288
+ }
289
+
290
+ if (!template) {
291
+ return handleApiError(res, 'Template not found', 'Template not found');
292
+ }
293
+
294
+ const formattedTemplate = {
295
+ id: template.sid,
296
+ sid: template.sid,
297
+ name: template.name,
298
+ friendlyName: template.friendlyName,
299
+ status: template.status,
300
+ language: template.language,
301
+ created: template.dateCreated,
302
+ updated: template.lastUpdated,
303
+ category: template.category,
304
+ body: template.body,
305
+ footer: template.footer,
306
+ variables: template.variables,
307
+ buttons: template.buttons,
308
+ approvalRequest: template.approvalRequest
309
+ };
310
+
311
+ return res.json({
312
+ success: true,
313
+ template: formattedTemplate
314
+ });
315
+ } catch (error) {
316
+ return handleApiError(res, error, 'Failed to retrieve template');
317
+ }
318
+ };
319
+
320
+ /**
321
+ * Submit a template for approval and update our database
322
+ */
323
+ const submitForApproval = async (req, res) => {
324
+ try {
325
+ const { contentSid, name, category } = req.body;
326
+ if (!contentSid) return res.status(400).json({ success: false, error: 'Content SID is required' });
327
+
328
+ // Generate a random suffix to ensure uniqueness
329
+ const timestamp = Date.now().toString();
330
+ const randomSuffix = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
331
+
332
+ // Create name pattern with underscores: template_name_123456789_123
333
+ const approvalName = `${name || 'template'}_${timestamp}_${randomSuffix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
334
+ const approvalCategory = category || 'UTILITY';
335
+
336
+ const response = await TwilioService.submitForApproval(contentSid, approvalName, approvalCategory);
337
+
338
+ const dateCreated = response.date_created || response.dateCreated;
339
+ const dateUpdated = response.date_updated || response.dateUpdated || dateCreated;
340
+
341
+ const submittedDate = dateCreated ? new Date(dateCreated) : new Date();
342
+ const updatedDate = dateUpdated ? new Date(dateUpdated) : new Date();
343
+
344
+ const validSubmittedDate = !isNaN(submittedDate.getTime()) ? submittedDate : new Date();
345
+ const validUpdatedDate = !isNaN(updatedDate.getTime()) ? updatedDate : new Date();
346
+
347
+ await TemplateModel.updateOne(
348
+ { sid: contentSid },
349
+ {
350
+ status: 'PENDING',
351
+ approvalRequest: {
352
+ sid: response.sid,
353
+ status: response.status || 'PENDING',
354
+ dateSubmitted: validSubmittedDate,
355
+ dateUpdated: validUpdatedDate,
356
+ rejectionReason: response.rejection_reason || ''
357
+ }
358
+ },
359
+ { upsert: true }
360
+ );
361
+
362
+ return res.status(200).json({
363
+ success: true,
364
+ message: 'Template submitted for approval',
365
+ contentSid,
366
+ approvalRequestSid: response.sid,
367
+ approvalName: response.name,
368
+ approvalCategory: response.category,
369
+ approvalStatus: response.status,
370
+ approvalSubmitted: response.date_created
371
+ });
372
+ } catch (error) {
373
+ return handleApiError(res, error, 'Error submitting template for approval');
374
+ }
375
+ };
376
+
377
+ /**
378
+ * Check the approval status of a template
379
+ */
380
+ const checkApprovalStatus = async (req, res) => {
381
+ try {
382
+ const { sid: contentSid } = req.params;
383
+
384
+ if (!contentSid) {
385
+ return res.status(400).json({ success: false, error: 'Content SID is required' });
386
+ }
387
+
388
+ const dbTemplate = await TemplateModel.findOne({ sid: contentSid });
389
+
390
+ try {
391
+ const status = await TwilioService.checkApprovalStatus(contentSid);
392
+
393
+ if (dbTemplate) {
394
+ // Use approval status as the authoritative source if available
395
+ let finalStatus = status.content.status || dbTemplate.status;
396
+
397
+ if (status.approvalRequest) {
398
+ if (status.approvalRequest.status === 'APPROVED') {
399
+ finalStatus = 'APPROVED';
400
+ } else if (status.approvalRequest.status === 'REJECTED') {
401
+ finalStatus = 'REJECTED';
402
+ } else if (status.approvalRequest.status === 'PENDING') {
403
+ finalStatus = 'PENDING';
404
+ }
405
+ }
406
+
407
+ dbTemplate.status = finalStatus;
408
+
409
+ if (status.approvalRequest) {
410
+ // Use content dates as fallback since approval request doesn't have its own dates
411
+ const dateSubmitted = status.approvalRequest.date_created || status.approvalRequest.dateCreated || status.content.dateCreated ?
412
+ new Date(status.approvalRequest.date_created || status.approvalRequest.dateCreated || status.content.dateCreated) :
413
+ new Date();
414
+ const dateUpdated = status.approvalRequest.date_updated || status.approvalRequest.dateUpdated || status.content.dateUpdated ?
415
+ new Date(status.approvalRequest.date_updated || status.approvalRequest.dateUpdated || status.content.dateUpdated) :
416
+ dateSubmitted;
417
+ dbTemplate.approvalRequest = {
418
+ sid: status.approvalRequest.sid,
419
+ status: status.approvalRequest.status,
420
+ dateSubmitted: dateSubmitted,
421
+ dateUpdated: dateUpdated,
422
+ rejectionReason: status.approvalRequest.rejection_reason
423
+ };
424
+ }
425
+
426
+ await dbTemplate.save();
427
+ }
428
+
429
+ return res.status(200).json({
430
+ success: true,
431
+ contentSid,
432
+ content: status.content,
433
+ approvalRequest: status.approvalRequest,
434
+ template: dbTemplate
435
+ });
436
+ } catch (err) {
437
+ // If Twilio check fails but we have DB record, return what we know
438
+ if (dbTemplate) {
439
+ return res.status(200).json({
440
+ success: true,
441
+ contentSid,
442
+ content: {
443
+ sid: dbTemplate.sid,
444
+ status: dbTemplate.status
445
+ },
446
+ approvalRequest: dbTemplate.approvalRequest,
447
+ template: dbTemplate,
448
+ warning: 'Could not fetch latest status from Twilio'
449
+ });
450
+ }
451
+
452
+ throw err;
453
+ }
454
+ } catch (error) {
455
+ return handleApiError(res, error, 'Error checking template status');
456
+ }
457
+ };
458
+
459
+ /**
460
+ * Delete a template
461
+ */
462
+ const deleteTemplate = async (req, res) => {
463
+ try {
464
+ const { id } = req.params;
465
+
466
+ await TwilioService.deleteTemplate(id);
467
+
468
+ await TemplateModel.deleteOne({ sid: id });
469
+
470
+ return res.json({
471
+ success: true,
472
+ message: 'Template deleted successfully'
473
+ });
474
+ } catch (error) {
475
+ return handleApiError(res, error, 'Failed to delete template');
476
+ }
477
+ };
478
+
479
+ const getPredefinedTemplates = (req, res) => {
480
+ try {
481
+ const templatesList = Object.entries(predefinedTemplates).map(([key, createTemplateFn]) => {
482
+ const template = createTemplateFn();
483
+ return {
484
+ id: key,
485
+ name: template.name,
486
+ category: template.category,
487
+ description: `Template for ${key.replace(/_/g, ' ')}`,
488
+ language: template.language,
489
+ body: template.components.body[0]?.text || '',
490
+ variables: template.variables.map(v => ({
491
+ name: v.name,
492
+ description: v.description,
493
+ example: v.example,
494
+ })),
495
+ variationCount: template.variations.length
496
+ };
497
+ });
498
+
499
+ return res.json({
500
+ success: true,
501
+ templates: {
502
+ predefined: templatesList
503
+ }
504
+ });
505
+
506
+ } catch (error) {
507
+ console.error('Error getting predefined templates:', error);
508
+ res.status(500).json({
509
+ success: false,
510
+ error: 'Failed to retrieve predefined templates',
511
+ message: error.message
512
+ });
513
+ }
514
+ };
515
+
516
+ const getCompleteTemplate = async (req, res) => {
517
+ try {
518
+ const { sid } = req.params;
519
+
520
+ let template = await TemplateModel.findOne({ sid }).lean();
521
+
522
+ if (!template) {
523
+ return res.status(404).json({
524
+ success: false,
525
+ error: 'Template not found'
526
+ });
527
+ }
528
+
529
+ if (!template.body || !template.variables || template.variables.length === 0) {
530
+ try {
531
+ const twilioTemplate = await TwilioService.getTemplate(sid);
532
+ console.log('Fetched template from Twilio:', twilioTemplate);
533
+
534
+ let body = '';
535
+ let footer = '';
536
+ let variables = [];
537
+ let type = 'text';
538
+
539
+ if (twilioTemplate.types && twilioTemplate.types['twilio/flows']) {
540
+ type = 'flow';
541
+ body = twilioTemplate.types['twilio/flows'].body || '';
542
+ } else if (twilioTemplate.types && twilioTemplate.types['twilio/quick-reply']) {
543
+ type = 'quick-reply';
544
+ body = twilioTemplate.types['twilio/quick-reply'].body || '';
545
+
546
+ if (twilioTemplate.types['twilio/quick-reply'].actions &&
547
+ Array.isArray(twilioTemplate.types['twilio/quick-reply'].actions)) {
548
+ const actions = twilioTemplate.types['twilio/quick-reply'].actions;
549
+ template.actions = actions;
550
+ }
551
+ } else if (twilioTemplate.types && twilioTemplate.types['twilio/text']) {
552
+ body = twilioTemplate.types['twilio/text'].body || '';
553
+ }
554
+
555
+ const variableMatches = body.match(/\{\{([^}]+)\}\}/g) || [];
556
+ variables = variableMatches.map(match => {
557
+ const varName = match.replace(/[{}]/g, '');
558
+
559
+ const exampleValue = twilioTemplate.variables && twilioTemplate.variables[varName]
560
+ ? twilioTemplate.variables[varName]
561
+ : `Example ${varName}`;
562
+
563
+ return {
564
+ name: varName,
565
+ placeholder: `Variable ${varName}`,
566
+ example: exampleValue
567
+ };
568
+ });
569
+
570
+ const components = [];
571
+
572
+ if (body) {
573
+ components.push({ type: 'BODY', text: body });
574
+ }
575
+
576
+ if (footer) {
577
+ components.push({ type: 'FOOTER', text: footer });
578
+ }
579
+
580
+ if (body) {
581
+ const updateData = {
582
+ body,
583
+ footer,
584
+ variables,
585
+ components,
586
+ type,
587
+ lastUpdated: new Date()
588
+ };
589
+
590
+ if (type === 'quick-reply' && template.actions) {
591
+ updateData.actions = template.actions;
592
+ }
593
+
594
+ await TemplateModel.updateOne(
595
+ { sid },
596
+ { $set: updateData }
597
+ );
598
+
599
+ template.body = body;
600
+ template.footer = footer;
601
+ template.variables = variables;
602
+ template.components = components;
603
+ template.type = type;
604
+ }
605
+ } catch (twilioError) {
606
+ console.error('Error fetching complete template from Twilio:', twilioError);
607
+ }
608
+ }
609
+
610
+ return res.status(200).json({
611
+ success: true,
612
+ template
613
+ });
614
+ } catch (error) {
615
+ return handleApiError(res, error, 'Failed to get complete template');
616
+ }
617
+ };
618
+
619
+ module.exports = {
620
+ createTemplate,
621
+ listTemplates,
622
+ getTemplate,
623
+ getPredefinedTemplates,
624
+ submitForApproval,
625
+ checkApprovalStatus,
626
+ deleteTemplate,
627
+ getCompleteTemplate,
628
+ // Flow functions from templateFlowController
629
+ createFlow,
630
+ deleteFlow
631
+ };
package/lib/index.js CHANGED
@@ -188,17 +188,19 @@ class Nexus {
188
188
  }
189
189
  }
190
190
 
191
+ // Import routes
192
+ const routes = require('./routes');
193
+
191
194
  // Export main class and individual components
192
195
  module.exports = {
193
196
  Nexus,
194
- NexusMessaging,
195
197
  TwilioProvider,
196
198
  BaileysProvider,
197
199
  MongoStorage,
198
200
  MessageParser,
199
- adapters,
200
- core,
201
- storage,
202
- utils,
203
- models
201
+ DefaultLLMProvider,
202
+ routes,
203
+ // Direct access to route utilities for convenience
204
+ setupDefaultRoutes: routes.setupDefaultRoutes,
205
+ createRouter: routes.createRouter
204
206
  };