@peopl-health/nexus 3.1.0 → 3.1.2

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.
@@ -46,8 +46,8 @@ const reportBugController = async (req, res) => {
46
46
 
47
47
  if (!reporter) return res.status(400).json({ success: false, error: 'Reporter username is required' });
48
48
  if (!whatsapp_id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
49
- if (!severity || !['low', 'medium', 'high'].includes(severity)) {
50
- return res.status(400).json({ success: false, error: 'Severity must be low, medium, or high' });
49
+ if (!severity || !['low', 'medium', 'high', 'critical'].includes(severity)) {
50
+ return res.status(400).json({ success: false, error: 'Severity must be low, medium, high, or critical' });
51
51
  }
52
52
 
53
53
  logBugReportToAirtable(reporter, whatsapp_id, description, severity, messages).catch(err =>
@@ -182,7 +182,11 @@ async function downloadMediaAndCreateFile(code, reply) {
182
182
 
183
183
  const sanitizedCode = sanitizeFilename(code, 20);
184
184
  const sanitizedSubType = sanitizeFilename(subType, 10);
185
- const sanitizedFileName = sanitizeFilename(fileName, 50);
185
+
186
+ const fileExt = path.extname(fileName);
187
+ const fileBaseName = path.basename(fileName, fileExt);
188
+ const sanitizedBaseName = sanitizeFilename(fileBaseName, 50 - fileExt.length);
189
+ const sanitizedFileName = sanitizedBaseName + fileExt;
186
190
 
187
191
  const sourceFile = `${sanitizedCode}-${sanitizedSubType}-${sanitizedFileName}`;
188
192
  const downloadPath = path.join(__dirname, 'assets', 'tmp', sourceFile);
@@ -1,27 +1,28 @@
1
1
  const { Message } = require('../models/messageModel');
2
2
  const { logger } = require('../utils/logger');
3
+ const { handle24HourWindowError } = require('./templateRecoveryHelper');
3
4
 
4
5
  /**
5
6
  * Update message delivery status in the database based on Twilio status callback data
6
7
  */
7
8
  async function updateMessageStatus(messageSid, status, errorCode = null, errorMessage = null) {
8
9
  try {
9
- const statusInfo = {
10
- status,
11
- updatedAt: new Date()
10
+ const updateData = {
11
+ 'statusInfo.status': status,
12
+ 'statusInfo.updatedAt': new Date()
12
13
  };
13
14
 
14
15
  if (errorCode) {
15
- statusInfo.errorCode = errorCode;
16
+ updateData['statusInfo.errorCode'] = errorCode;
16
17
  }
17
18
 
18
19
  if (errorMessage) {
19
- statusInfo.errorMessage = errorMessage;
20
+ updateData['statusInfo.errorMessage'] = errorMessage;
20
21
  }
21
22
 
22
23
  const updated = await Message.findOneAndUpdate(
23
24
  { message_id: messageSid },
24
- { $set: { statusInfo } },
25
+ { $set: updateData },
25
26
  { new: true }
26
27
  );
27
28
 
@@ -62,12 +63,20 @@ async function handleStatusCallback(twilioStatusData) {
62
63
  return null;
63
64
  }
64
65
 
65
- return await updateMessageStatus(
66
+ const updated = await updateMessageStatus(
66
67
  MessageSid,
67
68
  MessageStatus.toLowerCase(),
68
69
  ErrorCode || null,
69
70
  ErrorMessage || null
70
71
  );
72
+
73
+ if ((ErrorCode === 63016 || ErrorCode === '63016') && updated) {
74
+ handle24HourWindowError(updated, MessageSid).catch(err =>
75
+ logger.error('[MessageStatus] Recovery error', { messageSid: MessageSid, error: err.message })
76
+ );
77
+ }
78
+
79
+ return updated;
71
80
  }
72
81
 
73
82
  /**
@@ -0,0 +1,72 @@
1
+ const { logger } = require('../utils/logger');
2
+ const { getDefaultInstance } = require('../core/NexusMessaging');
3
+ const { Template, configureNexusProvider: configureTemplateProvider } = require('../templates/templateStructure');
4
+ const { sendMessage } = require('../core/NexusMessaging');
5
+ const { Message } = require('../models/messageModel');
6
+
7
+ /**
8
+ * Handle 24-hour window error by creating template and sending when approved
9
+ */
10
+ async function handle24HourWindowError(message, messageSid) {
11
+ try {
12
+ if (!message?.body || !message?.numero) return;
13
+
14
+ const messaging = getDefaultInstance();
15
+ const provider = messaging?.getProvider();
16
+ if (!provider?.createTemplate) return;
17
+
18
+ configureTemplateProvider(provider);
19
+
20
+ const templateName = `auto_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
21
+ const template = new Template(templateName, 'UTILITY', 'es');
22
+ template.setBody(message.body, []);
23
+ const twilioContent = await template.save();
24
+
25
+ // Submit for approval
26
+ const approvalName = `${templateName}_${Date.now()}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
27
+ await provider.submitForApproval(twilioContent.sid, approvalName, 'UTILITY');
28
+
29
+ await Message.updateOne(
30
+ { message_id: messageSid },
31
+ { $set: { 'statusInfo.recoveryTemplateSid': twilioContent.sid } }
32
+ );
33
+
34
+ logger.info('[TemplateRecovery] Template created', { messageSid, templateSid: twilioContent.sid });
35
+
36
+ const checkApproval = async (attempt = 0, maxAttempts = 40) => {
37
+ if (attempt >= maxAttempts) {
38
+ logger.warn('[TemplateRecovery] Max attempts reached, template not approved yet', { messageSid, templateSid: twilioContent.sid });
39
+ return;
40
+ }
41
+
42
+ setTimeout(async () => {
43
+ try {
44
+ const status = await provider.checkApprovalStatus(twilioContent.sid);
45
+ const approvalStatus = status?.approvalRequest?.status?.toUpperCase();
46
+
47
+ if (approvalStatus === 'APPROVED') {
48
+ await sendMessage({ code: message.numero, contentSid: twilioContent.sid, variables: {} });
49
+ logger.info('[TemplateRecovery] Template sent', { messageSid, templateSid: twilioContent.sid });
50
+ } else if (approvalStatus === 'REJECTED') {
51
+ logger.warn('[TemplateRecovery] Template rejected', { messageSid, templateSid: twilioContent.sid });
52
+ } else {
53
+ checkApproval(attempt + 1, maxAttempts);
54
+ }
55
+ } catch (err) {
56
+ logger.error('[TemplateRecovery] Error checking approval', { error: err.message, attempt });
57
+ // Retry on error (but count as attempt)
58
+ if (attempt + 1 < maxAttempts) {
59
+ checkApproval(attempt + 1, maxAttempts);
60
+ }
61
+ }
62
+ }, 15 * 60 * 1000);
63
+ };
64
+
65
+ checkApproval(0);
66
+
67
+ } catch (error) {
68
+ logger.error('[TemplateRecovery] Error', { messageSid, error: error.message });
69
+ }
70
+ }
71
+
72
+ module.exports = { handle24HourWindowError };
@@ -124,6 +124,9 @@ async function insertMessage(values) {
124
124
  updatedAt: values.delivery_status_updated_at || null
125
125
  };
126
126
  }
127
+ if (messageData.tools_executed === undefined) {
128
+ messageData.tools_executed = [];
129
+ }
127
130
 
128
131
  await Message.findOneAndUpdate(
129
132
  { message_id: values.message_id, body: values.body },
@@ -39,6 +39,8 @@ const messageRouteDefinitions = {
39
39
  'GET /last': 'getLastInteractionController',
40
40
  'GET /scheduled-status': 'checkScheduledMessageStatusController',
41
41
  'GET /status': 'checkMessageStatusController',
42
+ 'GET /status/:messageSid': 'getMessageStatusController',
43
+ 'POST /status-callback': 'messageStatusCallbackController',
42
44
  'POST /quality': 'addQualityVoteController',
43
45
  'GET /quality/:message_id': 'getQualityVotesByMessageController',
44
46
  'GET /quality/:message_id/voter/:voter_username': 'getQualityVoteByMessageAndVoterController',
@@ -97,6 +99,7 @@ const conversationController = require('../controllers/conversationController');
97
99
  const interactionController = require('../controllers/interactionController');
98
100
  const mediaController = require('../controllers/mediaController');
99
101
  const messageController = require('../controllers/messageController');
102
+ const messageStatusController = require('../controllers/messageStatusController');
100
103
  const patientController = require('../controllers/patientController');
101
104
  const qualityMessageController = require('../controllers/qualityMessageController');
102
105
  const templateController = require('../controllers/templateController');
@@ -144,6 +147,8 @@ const builtInControllers = {
144
147
  getLastInteractionController: messageController.getLastInteractionController,
145
148
  checkScheduledMessageStatusController: messageController.checkScheduledMessageStatusController,
146
149
  checkMessageStatusController: messageController.checkMessageStatusController,
150
+ getMessageStatusController: messageStatusController.getMessageStatusController,
151
+ messageStatusCallbackController: messageStatusController.messageStatusCallbackController,
147
152
  addQualityVoteController: qualityMessageController.addQualityVoteController,
148
153
  getQualityVotesByMessageController: qualityMessageController.getQualityVotesByMessageController,
149
154
  getQualityVoteByMessageAndVoterController: qualityMessageController.getQualityVoteByMessageAndVoterController,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",