@peopl-health/nexus 2.7.0 → 2.8.0-fix.1

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.
@@ -1,5 +1,6 @@
1
1
  const { MessageProvider } = require('../core/MessageProvider');
2
2
  const { logger } = require('../utils/logger');
3
+ const { calculateDelay } = require('../utils/scheduleUtils');
3
4
 
4
5
  /**
5
6
  * Baileys WhatsApp messaging provider
@@ -150,7 +151,7 @@ class BaileysProvider extends MessageProvider {
150
151
 
151
152
  async sendScheduledMessage(scheduledMessage) {
152
153
  const { sendTime, timeZone, __nexusSend } = scheduledMessage;
153
- const delay = this.calculateDelay(sendTime, timeZone);
154
+ const delay = calculateDelay(sendTime, timeZone);
154
155
 
155
156
  const sender = typeof __nexusSend === 'function'
156
157
  ? __nexusSend
@@ -181,11 +182,6 @@ class BaileysProvider extends MessageProvider {
181
182
  return { scheduled: true, delay };
182
183
  }
183
184
 
184
- calculateDelay(sendTime) {
185
- const now = new Date();
186
- const targetTime = new Date(sendTime);
187
- return Math.max(0, targetTime.getTime() - now.getTime());
188
- }
189
185
 
190
186
  getConnectionStatus() {
191
187
  return this.waSocket?.user ? true : false;
@@ -3,10 +3,11 @@ const axios = require('axios');
3
3
  const runtimeConfig = require('../config/runtimeConfig');
4
4
  const { uploadMediaToS3, getFileExtension } = require('../helpers/mediaHelper');
5
5
  const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
6
- const { sanitizeMediaFilename } = require('../utils/sanitizer');
6
+ const { sanitizeMediaFilename } = require('../utils/inputSanitizer');
7
7
  const { generatePresignedUrl } = require('../config/awsConfig');
8
8
  const { validateMedia, getMediaType } = require('../utils/mediaValidator');
9
9
  const { logger } = require('../utils/logger');
10
+ const { calculateDelay } = require('../utils/scheduleUtils');
10
11
  const { v4: uuidv4 } = require('uuid');
11
12
 
12
13
  /**
@@ -220,7 +221,7 @@ class TwilioProvider extends MessageProvider {
220
221
 
221
222
  async sendScheduledMessage(scheduledMessage) {
222
223
  const { sendTime, timeZone, __nexusSend } = scheduledMessage;
223
- const delay = this.calculateDelay(sendTime, timeZone);
224
+ const delay = calculateDelay(sendTime, timeZone);
224
225
 
225
226
  const sender = typeof __nexusSend === 'function'
226
227
  ? __nexusSend
@@ -423,11 +424,6 @@ class TwilioProvider extends MessageProvider {
423
424
  }
424
425
  }
425
426
 
426
- calculateDelay(sendTime) {
427
- const now = new Date();
428
- const targetTime = new Date(sendTime);
429
- return Math.max(0, targetTime.getTime() - now.getTime());
430
- }
431
427
 
432
428
  /**
433
429
  * Split a message into chunks at sentence boundaries, respecting Twilio's character limit
@@ -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 =>
@@ -63,6 +63,7 @@ async function getMessageStatusController(req, res) {
63
63
  message: {
64
64
  messageSid: message.message_id,
65
65
  statusInfo: message.statusInfo || null,
66
+ recoveryInfo: message.recoveryInfo || null,
66
67
  numero: message.numero,
67
68
  body: message.body
68
69
  }
@@ -43,9 +43,14 @@ class NexusMessaging {
43
43
  };
44
44
  // Message processing with check-after strategy
45
45
  this.processingLocks = new Map(); // Per-chat locks to prevent parallel processing
46
+ this.activeRequests = new Map(); // Track active AI requests per chat
47
+ this.abandonedRuns = new Set(); // Track runs that should be ignored
46
48
  this.batchingConfig = {
47
49
  enabled: config.messageBatching?.enabled ?? true, // Enabled by default with check-after
48
- checkDelayMs: config.messageBatching?.checkDelayMs ?? 100 // Delay before checking for new messages
50
+ abortOnNewMessage: config.messageBatching?.abortOnNewMessage ?? true, // Abort ongoing AI calls when new messages arrive
51
+ immediateRestart: config.messageBatching?.immediateRestart ?? true, // Start new processing immediately without waiting
52
+ batchWindowMs: config.messageBatching?.batchWindowMs ?? 2000, // Wait up to 2s for message bursts
53
+ maxBatchWait: config.messageBatching?.maxBatchWait ?? 5000 // Maximum time to wait for batching
49
54
  };
50
55
  }
51
56
 
@@ -348,6 +353,7 @@ class NexusMessaging {
348
353
  if (this.messageStorage) {
349
354
  await this.messageStorage.saveMessage({
350
355
  ...messageData,
356
+ messageId: messageData.id,
351
357
  timestamp: new Date(),
352
358
  fromMe: false,
353
359
  origin: 'patient'
@@ -651,49 +657,83 @@ class NexusMessaging {
651
657
  * Handle message with check-after strategy - process immediately, check for new messages after
652
658
  */
653
659
  async _handleWithCheckAfter(chatId) {
654
- // If already processing this chat, just return (message is saved, will be picked up)
660
+ const typingInterval = await this._startTypingRefresh(chatId);
661
+
655
662
  if (this.processingLocks.has(chatId)) {
656
663
  logger.info(`[CheckAfter] Already processing ${chatId}, new message will be included`);
664
+
665
+ if (this.batchingConfig.abortOnNewMessage && this.activeRequests.has(chatId)) {
666
+ const runId = this.activeRequests.get(chatId);
667
+ this.abandonedRuns.add(runId);
668
+ logger.info(`[CheckAfter] Marked run ${runId} as abandoned for ${chatId}`);
669
+
670
+ if (this.batchingConfig.immediateRestart) {
671
+ this.processingLocks.delete(chatId);
672
+ this.activeRequests.delete(chatId);
673
+
674
+ logger.info(`[CheckAfter] Starting immediate reprocessing for ${chatId}`);
675
+ await this._processWithLock(chatId, null);
676
+ }
677
+ }
657
678
  return;
658
679
  }
659
680
 
660
- await this._processWithLock(chatId);
681
+ await this._processWithLock(chatId, typingInterval);
661
682
  }
662
683
 
663
684
  /**
664
685
  * Process messages with per-chat lock and check-after logic
665
686
  */
666
- async _processWithLock(chatId) {
687
+ async _processWithLock(chatId, existingTypingInterval = null) {
667
688
  this.processingLocks.set(chatId, true);
668
- let typingInterval = null;
689
+ let typingInterval = existingTypingInterval;
690
+ let runId = null;
669
691
 
670
692
  try {
671
- typingInterval = await this._startTypingRefresh(chatId);
672
- logger.info(`[CheckAfter] Processing messages for ${chatId}`);
693
+ if (!typingInterval) {
694
+ typingInterval = await this._startTypingRefresh(chatId);
695
+ }
696
+
697
+ const startTime = Date.now();
698
+ let messageCount = await this._getUnprocessedMessageCount(chatId);
699
+ let lastCount = messageCount;
700
+
701
+ while (Date.now() - startTime < this.batchingConfig.batchWindowMs) {
702
+ await new Promise(resolve => setTimeout(resolve, 500));
703
+ const newCount = await this._getUnprocessedMessageCount(chatId);
704
+
705
+ if (newCount > lastCount) {
706
+ lastCount = newCount;
707
+ logger.info(`[Batching] New message detected for ${chatId}, extending wait`);
708
+ }
709
+
710
+ if (Date.now() - startTime >= this.batchingConfig.maxBatchWait) {
711
+ logger.info(`[Batching] Max wait reached for ${chatId}`);
712
+ break;
713
+ }
714
+ }
715
+
716
+ logger.info(`[CheckAfter] Processing ${lastCount} messages for ${chatId} after batching`);
673
717
 
674
- // Process with assistant
675
- const result = await replyAssistant(chatId);
718
+ runId = `run_${Date.now()}_${Math.random().toString(36).substring(7)}`;
719
+ this.activeRequests.set(chatId, runId);
720
+
721
+ const result = await replyAssistant(chatId, null, null, { runId });
722
+
723
+ if (this.abandonedRuns.has(runId)) {
724
+ logger.info(`[CheckAfter] Discarding abandoned run ${runId} for ${chatId}`);
725
+ this.abandonedRuns.delete(runId);
726
+ return;
727
+ }
728
+
676
729
  const botResponse = typeof result === 'string' ? result : result?.output;
677
730
  const tools_executed = typeof result === 'object' ? result?.tools_executed : undefined;
678
731
 
679
- // Small delay to catch very recent DB writes
680
- await new Promise(resolve => setTimeout(resolve, this.batchingConfig.checkDelayMs));
681
-
682
- // Check for new unprocessed messages
683
- const hasNewMessages = await Message.exists({
684
- numero: chatId,
685
- processed: false,
686
- from_me: false
687
- });
688
-
689
- if (hasNewMessages) {
690
- logger.info(`[CheckAfter] New messages detected for ${chatId}, discarding response and reprocessing`);
691
- if (typingInterval) clearInterval(typingInterval);
692
- // Recursively process with new messages
693
- return await this._processWithLock(chatId);
732
+ if (typingInterval) {
733
+ clearInterval(typingInterval);
734
+ typingInterval = null;
694
735
  }
695
736
 
696
- // No new messages - send response and mark processed
697
737
  if (botResponse) {
698
738
  await this.sendMessage({
699
739
  code: chatId,
@@ -711,9 +751,25 @@ class NexusMessaging {
711
751
  } finally {
712
752
  if (typingInterval) clearInterval(typingInterval);
713
753
  this.processingLocks.delete(chatId);
754
+ this.activeRequests.delete(chatId);
755
+ if (this.abandonedRuns.size > 100) {
756
+ this.abandonedRuns.clear();
757
+ }
714
758
  }
715
759
  }
716
760
 
761
+ /**
762
+ * Get count of unprocessed messages for a chat
763
+ */
764
+ async _getUnprocessedMessageCount(chatId) {
765
+ const { Message } = require('../models/messageModel');
766
+ return await Message.countDocuments({
767
+ numero: chatId,
768
+ processed: false,
769
+ from_me: false
770
+ });
771
+ }
772
+
717
773
  /**
718
774
  * Start typing indicator refresh interval
719
775
  */
@@ -725,10 +781,16 @@ class NexusMessaging {
725
781
  const lastMessage = await Message.findOne({
726
782
  numero: chatId,
727
783
  from_me: false,
728
- message_id: { $exists: true, $ne: null }
784
+ message_id: { $exists: true, $ne: null, $not: /^pending-/ },
785
+ createdAt: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
729
786
  }).sort({ createdAt: -1 });
730
787
 
731
- if (!lastMessage?.message_id) return null;
788
+ if (!lastMessage?.message_id) {
789
+ logger.debug(`[_startTypingRefresh] No valid message for typing indicator: ${chatId}`);
790
+ return null;
791
+ }
792
+
793
+ logger.debug(`[_startTypingRefresh] Starting typing indicator for message: ${lastMessage.message_id}`);
732
794
 
733
795
  return setInterval(() =>
734
796
  this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
@@ -7,7 +7,7 @@ const sharp = require('sharp');
7
7
 
8
8
  const { downloadFileFromS3 } = require('../config/awsConfig.js');
9
9
  const { Message } = require('../models/messageModel.js');
10
- const { sanitizeFilename } = require('../utils/sanitizer.js');
10
+ const { sanitizeFilename } = require('../utils/inputSanitizer.js');
11
11
  const { logger } = require('../utils/logger');
12
12
 
13
13
  async function convertPdfToImages(pdfName, existingPdfPath = null) {
@@ -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,7 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const AWS = require('../config/awsConfig.js');
4
- const { sanitizeMediaFilename } = require('../utils/sanitizer.js');
4
+ const { sanitizeMediaFilename } = require('../utils/inputSanitizer.js');
5
5
  const { logger } = require('../utils/logger');
6
6
 
7
7
  async function uploadMediaToS3(buffer, messageID, titleFile, bucketName, contentType, messageType) {
@@ -10,13 +10,13 @@ async function uploadMediaToS3(buffer, messageID, titleFile, bucketName, content
10
10
  const fileName = sanitizedTitle
11
11
  ? `${messageType}/${messageID}_${sanitizedTitle}.${extension}`
12
12
  : `${messageType}/${messageID}.${extension}`;
13
- logger.info(titleFile, messageType);
13
+ logger.info('[uploadMediaToS3] Media file prepared for upload', { fileName, messageType });
14
14
 
15
15
  try {
16
16
  await AWS.uploadBufferToS3(buffer, bucketName, fileName, contentType);
17
17
  return fileName;
18
18
  } catch (error) {
19
- logger.error('Failed to upload media to S3:', error.stack);
19
+ logger.error('[uploadMediaToS3] Failed to upload media to S3:', error.stack);
20
20
  throw error;
21
21
  }
22
22
  }
@@ -2,19 +2,31 @@ const moment = require('moment-timezone');
2
2
  const { Message } = require('../models/messageModel.js');
3
3
  const { logger } = require('../utils/logger');
4
4
 
5
- const updateMessageRecord = async (reply, thread) => {
5
+ const updateMessageRecord = async (reply, thread, processedContent = null) => {
6
6
  const threadId = thread.getConversationId();
7
7
 
8
+ const updateData = {
9
+ assistant_id: thread.getAssistantId(),
10
+ thread_id: threadId,
11
+ processed: true
12
+ };
13
+
14
+ if (processedContent && reply.media) {
15
+ updateData.media = {
16
+ ...reply.media,
17
+ metadata: {
18
+ ...(reply.media.metadata || {}),
19
+ processed_content: processedContent
20
+ }
21
+ };
22
+ }
23
+
8
24
  await Message.updateOne(
9
25
  { message_id: reply.message_id, timestamp: reply.timestamp },
10
- { $set: {
11
- assistant_id: thread.getAssistantId(),
12
- thread_id: threadId,
13
- processed: true
14
- } }
26
+ { $set: updateData }
15
27
  );
16
28
 
17
- logger.info(`[updateMessageRecord] Record updated - ID: ${reply.message_id}, Thread: ${threadId}, Processed: true`);
29
+ logger.info(`[updateMessageRecord] Record updated - ID: ${reply.message_id}, Thread: ${threadId}, Processed: true, MediaContentUpdated: ${!!(processedContent && reply.media)}`);
18
30
  };
19
31
 
20
32
 
@@ -81,7 +93,12 @@ function formatMessage(reply) {
81
93
  .locale('es')
82
94
  .format('dddd, D [de] MMMM [de] YYYY [a las] h:mm A');
83
95
 
84
- return `[${mexicoCityTime}] ${reply.body}`;
96
+ let messageContent = reply.body;
97
+ if (reply.media?.metadata?.processed_content) {
98
+ messageContent = reply.media.metadata.processed_content;
99
+ }
100
+
101
+ return `[${mexicoCityTime}] ${messageContent}`;
85
102
  } catch (error) {
86
103
  logger.error(`[formatMessage] Error for message ID: ${reply.message_id}:`, error?.message || String(error));
87
104
  return null;
@@ -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
  /**
@@ -76,7 +85,7 @@ async function handleStatusCallback(twilioStatusData) {
76
85
  async function getMessageStatus(messageSid) {
77
86
  try {
78
87
  const message = await Message.findOne({ message_id: messageSid })
79
- .select('statusInfo message_id numero body')
88
+ .select('statusInfo recoveryInfo message_id numero body')
80
89
  .lean();
81
90
 
82
91
  return message;
@@ -2,8 +2,7 @@ const fs = require('fs');
2
2
  const { generatePresignedUrl } = require('../config/awsConfig.js');
3
3
  const { analyzeImage } = require('./llmsHelper.js');
4
4
  const { cleanupFiles, downloadMediaAndCreateFile } = require('./filesHelper.js');
5
- const { formatMessage } = require('./messageHelper.js');
6
- const { sanitizeLogMetadata } = require('../utils/sanitizer.js');
5
+ const { sanitizeLogMetadata } = require('../utils/inputSanitizer.js');
7
6
  const { withTracing } = require('../utils/tracingDecorator.js');
8
7
 
9
8
  /**
@@ -35,23 +34,19 @@ const logger = {
35
34
 
36
35
  /**
37
36
  * Dedicated message processing utilities
38
- * Handles text messages, media files, audio transcription, and thread operations
39
37
  */
40
38
  const processTextMessage = (reply) => {
41
- const formattedMessage = formatMessage(reply);
42
39
  logger.info('processTextMessage', {
43
40
  message_id: reply.message_id,
44
41
  timestamp: reply.timestamp,
45
42
  from_me: reply.from_me,
46
43
  body: reply.body,
47
- hasContent: !!formattedMessage,
48
- formattedMessage
44
+ hasContent: !!reply.body
49
45
  });
50
- logger.debug('processTextMessage_content', { formattedMessage });
51
46
 
52
47
  const messagesChat = [];
53
- if (formattedMessage) {
54
- messagesChat.push({ type: 'text', text: formattedMessage });
48
+ if (reply.body) {
49
+ messagesChat.push({ type: 'text', text: reply.body });
55
50
  }
56
51
 
57
52
  return messagesChat;
@@ -121,8 +116,6 @@ const processImageFileCore = async (fileName, reply) => {
121
116
  ...timings
122
117
  });
123
118
 
124
- logger.debug('processImageFile_analysis', { imageAnalysis });
125
-
126
119
  } catch (error) {
127
120
  logger.error('processImageFile', error, {
128
121
  message_id: reply.message_id,
@@ -12,12 +12,6 @@ async function generateQRBuffer(text) {
12
12
  }
13
13
  }
14
14
 
15
- function bufferToBase64(buffer) {
16
- return buffer.toString('base64');
17
- }
18
-
19
-
20
15
  module.exports = {
21
- generateQRBuffer,
22
- bufferToBase64
16
+ generateQRBuffer
23
17
  };
@@ -0,0 +1,152 @@
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 existingMessage = await Message.findOne({ message_id: messageSid })
15
+ .select('recoveryInfo')
16
+ .lean();
17
+
18
+ if (existingMessage?.recoveryInfo?.status === 'in_progress') {
19
+ logger.info('[TemplateRecovery] Recovery already in progress', { messageSid });
20
+ return;
21
+ }
22
+
23
+ const messaging = getDefaultInstance();
24
+ const provider = messaging?.getProvider();
25
+ if (!provider?.createTemplate) return;
26
+
27
+ configureTemplateProvider(provider);
28
+
29
+ const templateName = `auto_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
30
+ const template = new Template(templateName, 'UTILITY', 'es');
31
+ template.setBody(message.body, []);
32
+ const twilioContent = await template.save();
33
+
34
+ // Submit for approval
35
+ const approvalName = `${templateName}_${Date.now()}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
36
+ await provider.submitForApproval(twilioContent.sid, approvalName, 'UTILITY');
37
+
38
+ await Message.updateOne(
39
+ { message_id: messageSid },
40
+ {
41
+ $set: {
42
+ 'recoveryInfo.status': 'in_progress',
43
+ 'recoveryInfo.templateSid': twilioContent.sid,
44
+ 'recoveryInfo.startedAt': new Date()
45
+ }
46
+ }
47
+ );
48
+
49
+ logger.info('[TemplateRecovery] Template created', { messageSid, templateSid: twilioContent.sid });
50
+
51
+ const checkApproval = async (attempt = 0, maxAttempts = 40) => {
52
+ if (attempt >= maxAttempts) {
53
+ await Message.updateOne(
54
+ { message_id: messageSid },
55
+ {
56
+ $set: {
57
+ 'recoveryInfo.status': 'failed',
58
+ 'recoveryInfo.completedAt': new Date(),
59
+ 'recoveryInfo.error': 'Max attempts reached, template not approved yet'
60
+ }
61
+ }
62
+ );
63
+
64
+ logger.warn('[TemplateRecovery] Max attempts reached, template not approved yet', { messageSid, templateSid: twilioContent.sid });
65
+ return;
66
+ }
67
+
68
+ setTimeout(async () => {
69
+ try {
70
+ await Message.updateOne(
71
+ { message_id: messageSid },
72
+ { $set: { 'recoveryInfo.lastCheckedAt': new Date() } }
73
+ );
74
+
75
+ const status = await provider.checkApprovalStatus(twilioContent.sid);
76
+ const approvalStatus = status?.approvalRequest?.status?.toUpperCase();
77
+
78
+ if (approvalStatus === 'APPROVED') {
79
+ await sendMessage({ code: message.numero, contentSid: twilioContent.sid, variables: {} });
80
+
81
+ await Message.updateOne(
82
+ { message_id: messageSid },
83
+ {
84
+ $set: {
85
+ 'recoveryInfo.status': 'completed',
86
+ 'recoveryInfo.completedAt': new Date()
87
+ }
88
+ }
89
+ );
90
+
91
+ logger.info('[TemplateRecovery] Template sent', { messageSid, templateSid: twilioContent.sid });
92
+ } else if (approvalStatus === 'REJECTED') {
93
+ await Message.updateOne(
94
+ { message_id: messageSid },
95
+ {
96
+ $set: {
97
+ 'recoveryInfo.status': 'failed',
98
+ 'recoveryInfo.completedAt': new Date(),
99
+ 'recoveryInfo.error': 'Template rejected by Twilio'
100
+ }
101
+ }
102
+ );
103
+
104
+ logger.warn('[TemplateRecovery] Template rejected', { messageSid, templateSid: twilioContent.sid });
105
+ } else {
106
+ checkApproval(attempt + 1, maxAttempts);
107
+ }
108
+ } catch (err) {
109
+ logger.error('[TemplateRecovery] Error checking approval', { error: err.message, attempt });
110
+
111
+ await Message.updateOne(
112
+ { message_id: messageSid },
113
+ { $set: { 'recoveryInfo.error': err.message } }
114
+ );
115
+
116
+ if (attempt + 1 < maxAttempts) {
117
+ checkApproval(attempt + 1, maxAttempts);
118
+ } else {
119
+ await Message.updateOne(
120
+ { message_id: messageSid },
121
+ {
122
+ $set: {
123
+ 'recoveryInfo.status': 'failed',
124
+ 'recoveryInfo.completedAt': new Date(),
125
+ 'recoveryInfo.error': `Max attempts reached: ${err.message}`
126
+ }
127
+ }
128
+ );
129
+ }
130
+ }
131
+ }, 15 * 60 * 1000);
132
+ };
133
+
134
+ checkApproval(0);
135
+
136
+ } catch (error) {
137
+ await Message.updateOne(
138
+ { message_id: messageSid },
139
+ {
140
+ $set: {
141
+ 'recoveryInfo.status': 'failed',
142
+ 'recoveryInfo.completedAt': new Date(),
143
+ 'recoveryInfo.error': error.message
144
+ }
145
+ }
146
+ ).catch(err => logger.error('[TemplateRecovery] Failed to update error status', { error: err.message }));
147
+
148
+ logger.error('[TemplateRecovery] Error', { messageSid, error: error.message });
149
+ }
150
+ }
151
+
152
+ module.exports = { handle24HourWindowError };