@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.
- package/README.md +123 -0
- package/examples/basic-usage.js +22 -77
- package/lib/controllers/assistantController.js +168 -0
- package/lib/controllers/conversationController.js +582 -0
- package/lib/controllers/mediaController.js +105 -0
- package/lib/controllers/messageController.js +218 -0
- package/lib/controllers/templateController.js +631 -0
- package/lib/index.js +8 -6
- package/lib/routes/index.js +87 -0
- package/lib/utils/index.js +24 -11
- package/lib/utils/mongoAuthConfig.js +13 -3
- package/lib/utils/twilioHelper.js +0 -2
- package/lib/utils/whatsappHelper.js +0 -8
- package/package.json +14 -9
|
@@ -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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
DefaultLLMProvider,
|
|
202
|
+
routes,
|
|
203
|
+
// Direct access to route utilities for convenience
|
|
204
|
+
setupDefaultRoutes: routes.setupDefaultRoutes,
|
|
205
|
+
createRouter: routes.createRouter
|
|
204
206
|
};
|