@peopl-health/nexus 2.5.6-fix-switch → 2.5.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.
@@ -2,6 +2,7 @@ const { MessageProvider } = require('../core/MessageProvider');
2
2
  const axios = require('axios');
3
3
  const runtimeConfig = require('../config/runtimeConfig');
4
4
  const { uploadMediaToS3, getFileExtension } = require('../helpers/mediaHelper');
5
+ const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
5
6
  const { sanitizeMediaFilename } = require('../utils/sanitizer');
6
7
  const { generatePresignedUrl } = require('../config/awsConfig');
7
8
  const { validateMedia, getMediaType } = require('../utils/mediaValidator');
@@ -35,13 +36,6 @@ class TwilioProvider extends MessageProvider {
35
36
  }
36
37
  }
37
38
 
38
- ensureWhatsAppFormat(number) {
39
- if (!number) return null;
40
- if (number.startsWith('whatsapp:')) return number;
41
- if (number.startsWith('+')) return `whatsapp:${number}`;
42
- return `whatsapp:+${number}`;
43
- }
44
-
45
39
  async sendMessage(messageData) {
46
40
  if (!this.isConnected || !this.twilioClient) {
47
41
  throw new Error('Twilio provider not initialized');
@@ -49,8 +43,8 @@ class TwilioProvider extends MessageProvider {
49
43
 
50
44
  const { code, body, fileUrl, fileType, variables, contentSid } = messageData;
51
45
 
52
- const formattedFrom = this.ensureWhatsAppFormat(this.whatsappNumber);
53
- const formattedCode = this.ensureWhatsAppFormat(code);
46
+ const formattedFrom = ensureWhatsAppFormat(this.whatsappNumber);
47
+ const formattedCode = ensureWhatsAppFormat(code);
54
48
 
55
49
 
56
50
  if (!formattedFrom || !formattedCode) {
@@ -3,6 +3,7 @@ const llmConfig = require('../config/llmConfig');
3
3
  const { Historial_Clinico_ID } = require('../config/airtableConfig');
4
4
  const { Thread } = require('../models/threadModel');
5
5
  const { withThreadRecovery } = require('../helpers/threadRecoveryHelper');
6
+ const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
6
7
  const { getRecordByFilter } = require('../services/airtableService');
7
8
  const { fetchConversationData, processConversations } = require('../services/conversationService');
8
9
  const { sendMessage } = require('../core/NexusMessaging');
@@ -107,7 +108,7 @@ const getConversationMessagesController = async (req, res) => {
107
108
  const limit = parseInt(req.query.limit) || 50;
108
109
  const { before } = req.query;
109
110
 
110
- const query = { numero: phoneNumber, is_group: false };
111
+ const query = { numero: phoneNumber, groud_id: null };
111
112
  if (before) {
112
113
  try {
113
114
  query.createdAt = { $lt: new Date(before) };
@@ -139,7 +140,7 @@ const getConversationMessagesController = async (req, res) => {
139
140
  return true;
140
141
  }
141
142
 
142
- if (msg.is_media === true) {
143
+ if (msg.media) {
143
144
  if (!msg.media || typeof msg.media !== 'object') {
144
145
  logger.warn('Found media message with invalid media data:', msg._id);
145
146
  return true;
@@ -180,7 +181,7 @@ const getConversationMessagesController = async (req, res) => {
180
181
  body: msg.body || '[Message content unavailable]',
181
182
  createdAt: msg.createdAt || new Date(),
182
183
  from_me: !!msg.from_me,
183
- is_media: !!msg.is_media,
184
+ is_media: !!msg.media,
184
185
  error: 'Message contained non-serializable data'
185
186
  };
186
187
  }
@@ -223,9 +224,7 @@ const getConversationReplyController = async (req, res) => {
223
224
  });
224
225
  }
225
226
 
226
- const formattedPhoneNumber = phoneNumber?.startsWith('whatsapp:')
227
- ? phoneNumber
228
- : `whatsapp:${phoneNumber}`;
227
+ const formattedPhoneNumber = ensureWhatsAppFormat(phoneNumber);
229
228
  logger.info('Formatted phone number:', formattedPhoneNumber);
230
229
 
231
230
  const messageData = {
@@ -315,7 +314,7 @@ const searchConversationsController = async (req, res) => {
315
314
  // Search through all conversations in the database
316
315
  const conversations = await Message.aggregate([
317
316
  { $match: {
318
- is_group: false,
317
+ group_id: null,
319
318
  $or: [
320
319
  { numero: { $regex: escapedQuery, $options: 'i' } },
321
320
  { nombre_whatsapp: { $regex: escapedQuery, $options: 'i' } },
@@ -327,7 +326,6 @@ const searchConversationsController = async (req, res) => {
327
326
  body: 1,
328
327
  createdAt: 1,
329
328
  timestamp: 1,
330
- is_media: 1,
331
329
  media: 1,
332
330
  nombre_whatsapp: 1,
333
331
  from_me: 1
@@ -403,7 +401,7 @@ const searchConversationsController = async (req, res) => {
403
401
  };
404
402
  }
405
403
 
406
- const isMedia = conv.latestMessage.is_media === true;
404
+ const isMedia = !!conv.latestMessage.media;
407
405
  let mediaType = null;
408
406
 
409
407
  if (isMedia && conv?.latestMessage?.media) {
@@ -458,7 +456,7 @@ const searchConversationsController = async (req, res) => {
458
456
  const getConversationsByNameController = async (req, res) => {
459
457
  try {
460
458
  const conversations = await Message.aggregate([
461
- { $match: { from_me: false, is_group: false } },
459
+ { $match: { from_me: false, groud_id: null } },
462
460
  { $sort: { createdAt: -1 } },
463
461
  { $group: {
464
462
  _id: '$numero',
@@ -511,7 +509,7 @@ const getNewMessagesController = async (req, res) => {
511
509
 
512
510
  const query = {
513
511
  numero: phoneNumber,
514
- is_group: false,
512
+ group_id: null,
515
513
  createdAt: { $gt: lastMessage.createdAt }
516
514
  };
517
515
 
@@ -593,12 +591,8 @@ const sendTemplateToNewNumberController = async (req, res) => {
593
591
  });
594
592
  }
595
593
 
596
- // Format phone number for WhatsApp if needed
597
- const formattedPhoneNumber = phoneNumber.startsWith('whatsapp:')
598
- ? phoneNumber
599
- : `whatsapp:${phoneNumber}`;
594
+ const formattedPhoneNumber = ensureWhatsAppFormat(phoneNumber);
600
595
 
601
- // Log template details for debugging
602
596
  logger.info('Sending template to new number with details:', {
603
597
  phoneNumber: formattedPhoneNumber,
604
598
  templateId,
@@ -1,11 +1,12 @@
1
+ const runtimeConfig = require('../config/runtimeConfig');
1
2
  const { Message } = require('../models/messageModel.js');
2
3
  const { ScheduledMessage: DefaultScheduledMessage } = require('../models/agendaMessageModel.js');
3
4
  const {
4
5
  sendMessage: defaultSendMessage,
5
6
  sendScheduledMessage: defaultSendScheduledMessage
6
7
  } = require('../core/NexusMessaging');
8
+ const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
7
9
  const { getRecordByFilter: defaultGetRecordByFilter } = require('../services/airtableService');
8
- const runtimeConfig = require('../config/runtimeConfig');
9
10
  const { logger } = require('../utils/logger');
10
11
  const moment = require('moment-timezone');
11
12
 
@@ -58,29 +59,6 @@ const pickMessageId = (result, fallbackDoc) => {
58
59
  return null;
59
60
  };
60
61
 
61
- const normalizeCode = (code) => {
62
- if (!code || typeof code !== 'string') return code;
63
-
64
- const trimmed = code.trim();
65
- if (trimmed.startsWith('whatsapp:')) {
66
- return trimmed;
67
- }
68
-
69
- if (trimmed.includes('@')) {
70
- return trimmed;
71
- }
72
-
73
- if (trimmed.startsWith('+')) {
74
- return `whatsapp:${trimmed}`;
75
- }
76
-
77
- if (/^\d+$/.test(trimmed)) {
78
- return `whatsapp:+${trimmed}`;
79
- }
80
-
81
- return trimmed;
82
- };
83
-
84
62
  const sendMessageController = async (req, res) => {
85
63
  const {
86
64
  fileUrl,
@@ -112,7 +90,7 @@ const sendMessageController = async (req, res) => {
112
90
  sendTime: sendMoment,
113
91
  contentSid,
114
92
  hidePreview,
115
- code: normalizeCode(code),
93
+ code: ensureWhatsAppFormat(code),
116
94
  author,
117
95
  extraDelay: 0,
118
96
  variables
@@ -173,7 +151,7 @@ const sendBulkMessageController = async (req, res) => {
173
151
  sendTime: new Date(sendMoment + extraDelay),
174
152
  contentSid,
175
153
  hidePreview,
176
- code: normalizeCode(recipient),
154
+ code: ensureWhatsAppFormat(recipient),
177
155
  author,
178
156
  extraDelay,
179
157
  variables
@@ -269,7 +247,7 @@ const sendBulkMessageAirtableController = async (req, res) => {
269
247
  let code = row[columnPhone];
270
248
  if (Array.isArray(code)) code = code[0];
271
249
  if (!code) continue;
272
- code = normalizeCode(code);
250
+ code = ensureWhatsAppFormat(code);
273
251
 
274
252
  if (sentPhones.has(code)) continue;
275
253
  sentPhones.add(code);
@@ -323,7 +301,7 @@ const sendBulkMessageAirtableController = async (req, res) => {
323
301
 
324
302
  const getLastInteractionController = async (req, res) => {
325
303
  const { code } = req.query;
326
- const normalizedCode = normalizeCode(code);
304
+ const normalizedCode = ensureWhatsAppFormat(code);
327
305
 
328
306
  try {
329
307
  const lastMessage = await Message.findOne({
@@ -355,7 +333,7 @@ const checkScheduledMessageStatusController = async (req, res) => {
355
333
  if (!ensureDependency(res, dependencies.ScheduledMessage, 'ScheduledMessage model not configured.')) return;
356
334
 
357
335
  try {
358
- const msg = await dependencies.ScheduledMessage.findOne({ contentSid, code: normalizeCode(code) });
336
+ const msg = await dependencies.ScheduledMessage.findOne({ contentSid, code: ensureWhatsAppFormat(code) });
359
337
  if (!msg) return res.status(404).send({ message: 'ScheduledMessage not found' });
360
338
  return res.status(200).send({ message: 'ScheduledMessage status retrieved successfully.', sent: msg?.status === 'sent', status: msg?.status || null });
361
339
  } catch (error) {
@@ -369,7 +347,7 @@ const checkMessageStatusController = async (req, res) => {
369
347
  if (!contentSid || !code) return res.status(400).send({ message: 'contentSid and code are required' });
370
348
 
371
349
  try {
372
- const msg = await Message.findOne({ content_sid: contentSid, numero: normalizeCode(code) });
350
+ const msg = await Message.findOne({ content_sid: contentSid, numero: ensureWhatsAppFormat(code) });
373
351
  if (!msg) return res.status(404).send({ message: 'Message not found' });
374
352
  const sent = msg?.statusInfo?.status === 'sent' || msg?.statusInfo?.status === 'delivered' || msg?.statusInfo?.status === 'read';
375
353
  return res.status(200).send({ message: 'Message status retrieved successfully.', sent, status: msg?.statusInfo?.status || null });
@@ -585,9 +585,7 @@ class NexusMessaging {
585
585
  const messageObj = convertTwilioToInternalFormat(raw);
586
586
  const values = getMessageValues(
587
587
  messageObj,
588
- messageData.body || messageData.caption || '',
589
- null,
590
- true
588
+ messageData.body || messageData.caption || ''
591
589
  );
592
590
  values.media = mediaPayload;
593
591
 
@@ -15,7 +15,7 @@ async function processMessage(message, messageType) {
15
15
  return;
16
16
  }
17
17
 
18
- const values = getMessageValues(message, content, reply, false);
18
+ const values = getMessageValues(message, content);
19
19
  await insertMessage(values);
20
20
 
21
21
  return values;
@@ -28,7 +28,7 @@ async function processMessage(message, messageType) {
28
28
  async function processMediaMessage(message, logger, messageType, bucketName, sock) {
29
29
  try {
30
30
  const { content, contentType, reply } = extractContentTypeAndReply(message, messageType);
31
- let values = getMessageValues(message, content, reply, true);
31
+ let values = getMessageValues(message, content);
32
32
  // Insert message into the message database
33
33
 
34
34
  const messageID = message.key.id;
@@ -108,7 +108,7 @@ function extractContentTypeAndReply(message, messageType) {
108
108
  }
109
109
 
110
110
  async function getLastMessages(chatId, n) {
111
- const messages = await Message.find({ group_id: chatId })
111
+ const messages = await Message.find({ numero: chatId })
112
112
  .sort({ createdAt: -1 })
113
113
  .limit(n)
114
114
  .select('timestamp numero nombre_whatsapp body');
@@ -213,7 +213,7 @@ const processMediaFilesCore = async (code, reply, provider) => {
213
213
  url_generation_ms: 0
214
214
  };
215
215
 
216
- if (!reply.is_media) {
216
+ if (!reply.media) {
217
217
  return { messagesChat, url, tempFiles, timings };
218
218
  }
219
219
 
@@ -282,7 +282,7 @@ const processMediaFiles = withTracing(
282
282
  'process_media_files',
283
283
  (code, reply) => ({
284
284
  'media.message_id': reply.message_id,
285
- 'media.is_media': reply.is_media
285
+ 'media.is_media': !!reply.media
286
286
  })
287
287
  );
288
288
 
@@ -325,7 +325,7 @@ const processThreadMessageCore = async (code, replies, provider) => {
325
325
  index: i + 1,
326
326
  total: replyArray.length,
327
327
  isPatient,
328
- hasMedia: reply.is_media,
328
+ hasMedia: !!reply.media,
329
329
  hasUrl: !!url
330
330
  });
331
331
 
@@ -104,13 +104,14 @@ async function downloadMedia(twilioMessage, logger) {
104
104
 
105
105
 
106
106
  const ensureWhatsAppFormat = (phoneNumber) => {
107
- if (!phoneNumber) return null;
108
-
109
- if (phoneNumber.startsWith('whatsapp:')) {
110
- return phoneNumber;
111
- }
112
-
113
- return `whatsapp:${phoneNumber}`;
107
+ if (!phoneNumber || typeof phoneNumber !== 'string') return phoneNumber;
108
+
109
+ const trimmed = phoneNumber.trim();
110
+ if (trimmed.startsWith('whatsapp:')) return trimmed;
111
+ if (trimmed.includes('@')) return trimmed;
112
+ if (trimmed.startsWith('+')) return `whatsapp:${trimmed}`;
113
+ if (/^\d+$/.test(trimmed)) return `whatsapp:+${trimmed}`;
114
+ return trimmed;
114
115
  };
115
116
 
116
117
 
package/lib/index.d.ts CHANGED
@@ -286,7 +286,7 @@ declare module '@peopl-health/nexus' {
286
286
  export function delay(ms: number): Promise<void>;
287
287
  export function formatCode(codeBase: string): string;
288
288
  export function calculateDelay(sendTime: string | Date, timeZone?: string): number;
289
- export function ensureWhatsAppFormat(phoneNumber: string): string | null;
289
+ export function ensureWhatsAppFormat(phoneNumber: any): string | null;
290
290
  export function convertTwilioToInternalFormat(twilioMessage: any): any;
291
291
  export function downloadMediaFromTwilio(mediaUrl: string, credentials: any): Promise<Buffer>;
292
292
  export function getMediaTypeFromContentType(contentType: string): string;
@@ -296,7 +296,7 @@ declare module '@peopl-health/nexus' {
296
296
  // Models
297
297
  export const Message: mongoose.Model<any>;
298
298
  export const Thread: mongoose.Model<any>;
299
- export function getMessageValues(message: any, content: string, reply?: string, is_media?: boolean): any;
299
+ export function getMessageValues(message: any, content: string): any;
300
300
  export function formatTimestamp(unixTimestamp: number): string;
301
301
 
302
302
  // Module Exports
@@ -6,15 +6,12 @@ const { logger } = require('../utils/logger');
6
6
 
7
7
 
8
8
  const messageSchema = new mongoose.Schema({
9
- raw: { type: Object, required: false },
10
- nombre_whatsapp: { type: String, required: true },
11
- numero: { type: String, required: true },
9
+ raw: { type: Object, default: null },
12
10
  body: { type: String, required: true },
11
+ numero: { type: String, required: true },
12
+ nombre_whatsapp: { type: String, default: null },
13
13
  timestamp: { type: String, required: true, default: Date.now },
14
- message_id: { type: String, required: true },
15
- is_group: { type: Boolean, required: true },
16
- is_media: { type: Boolean, required: true },
17
- is_interactive: { type: Boolean, default: false },
14
+ message_id: { type: String, default: null},
18
15
  interactive_type: {
19
16
  type: String,
20
17
  enum: ['button', 'list', 'flow', 'quick_reply'],
@@ -22,30 +19,37 @@ const messageSchema = new mongoose.Schema({
22
19
  },
23
20
  interactive_data: { type: Object, default: null },
24
21
  group_id: { type: String, default: null },
25
- reply_id: { type: String, default: null },
26
22
  processed: { type: Boolean, default: false },
27
- thread_id: { type: String, default: null },
23
+ read: { type: Boolean, default: false },
28
24
  assistant_id: { type: String, default: null },
29
25
  content_sid: { type: String, default: null },
30
26
  from_me: { type: Boolean, default: false },
31
27
  origin: {
32
28
  type: String,
33
29
  enum: ['whatsapp_platform', 'assistant', 'patient', 'system', 'instruction'],
34
- default: 'whatsapp_platform' },
35
- tools_executed: [{
36
- tool_name: { type: String, required: true },
37
- tool_arguments: { type: Object, default: null },
38
- tool_output: { type: Object, default: null },
39
- execution_time_ms: { type: Number, default: null },
40
- success: { type: Boolean, default: true },
41
- call_id: { type: String, default: null },
42
- executed_at: { type: Date, default: Date.now }
43
- }],
30
+ default: 'whatsapp_platform'
31
+ },
32
+ tools_executed: {
33
+ type: [{
34
+ tool_name: { type: String, required: true },
35
+ tool_arguments: { type: Object, default: null },
36
+ tool_output: { type: Object, default: null },
37
+ execution_time_ms: { type: Number, default: null },
38
+ success: { type: Boolean, default: true },
39
+ call_id: { type: String, default: null },
40
+ executed_at: { type: Date, default: Date.now }
41
+ }],
42
+ default: []
43
+ },
44
44
  media: {
45
45
  contentType: { type: String, default: null },
46
46
  bucketName: { type: String, default: null },
47
47
  key: { type: String, default: null },
48
- mediaType: { type: String, enum: ['image', 'video', 'audio', 'document', 'sticker', 'other'], default: null },
48
+ mediaType: {
49
+ type: String,
50
+ enum: ['image', 'video', 'audio', 'document', 'sticker', 'other'],
51
+ default: null
52
+ },
49
53
  fileName: { type: String, default: null },
50
54
  fileSize: { type: Number, default: null },
51
55
  duration: { type: Number, default: null },
@@ -60,34 +64,19 @@ const messageSchema = new mongoose.Schema({
60
64
  default: 'active',
61
65
  index: true
62
66
  },
63
- read: {
64
- type: Boolean,
65
- default: false
66
- },
67
67
  statusInfo: {
68
68
  status: {
69
69
  type: String,
70
70
  enum: ['queued', 'sending', 'sent', 'delivered', 'undelivered', 'failed', 'read', null],
71
71
  default: null
72
72
  },
73
- errorCode: {
74
- type: String,
75
- default: null
76
- },
77
- errorMessage: {
78
- type: String,
79
- default: null
80
- },
81
- updatedAt: {
82
- type: Date,
83
- default: null
84
- }
73
+ errorCode: { type: String, default: null },
74
+ errorMessage: { type: String, default: null },
75
+ updatedAt: { type: Date, default: null }
85
76
  }
86
77
  }, { timestamps: true });
87
78
 
88
- messageSchema.index({ message_id: 1, timestamp: 1 }, { unique: true });
89
79
  messageSchema.index({ numero: 1, createdAt: -1 });
90
-
91
80
  messageSchema.index({ numero: 1, processed: 1, origin: 1 }, { name: 'numero_processed_origin_idx' });
92
81
  messageSchema.index({ numero: 1, createdAt: -1, processed: 1 }, { name: 'numero_created_processed_idx' });
93
82
 
@@ -115,47 +104,30 @@ async function getClinicalContext(whatsappId) {
115
104
 
116
105
  async function insertMessage(values) {
117
106
  try {
118
- const skipNumbers = ['5215592261426@s.whatsapp.net', '5215547411345@s.whatsapp.net', '51985959446@s.whatsapp.net'];
119
107
  const clinical_context = await getClinicalContext(values.numero);
120
- const messageData = {
121
- nombre_whatsapp: values.nombre_whatsapp,
122
- numero: values.numero,
123
- body: values.body,
124
- timestamp: values.timestamp,
125
- message_id: values.message_id,
126
- is_group: values.is_group,
127
- is_media: values.is_media,
128
- is_interactive: values.is_interactive || false,
129
- interactive_type: values.interactive_type || null,
130
- interactive_data: values.interactive_data || null,
131
- group_id: values.group_id,
132
- reply_id: values.reply_id,
133
- from_me: values.from_me,
134
- processed: values.processed || skipNumbers.includes(values.numero),
135
- media: values.media ? values.media : null,
136
- content_sid: values.content_sid || null,
137
- clinical_context: clinical_context,
138
- origin: values.origin,
139
- tools_executed: values.tools_executed || [],
140
- raw: values.raw || null,
141
- assistant_id: values.assistant_id || null,
142
- statusInfo: values.statusInfo || (values.delivery_status ? {
108
+ const messageData = Object.fromEntries(
109
+ Object.entries(values)
110
+ .filter(([k, v]) => v !== undefined && !k.startsWith('delivery_'))
111
+ );
112
+ messageData.clinical_context = clinical_context;
113
+ if (!messageData.statusInfo && values.delivery_status) {
114
+ messageData.statusInfo = {
143
115
  status: values.delivery_status,
144
116
  errorCode: values.delivery_error_code || null,
145
117
  errorMessage: values.delivery_error_message || null,
146
118
  updatedAt: values.delivery_status_updated_at || null
147
- } : null)
148
- };
119
+ };
120
+ }
149
121
 
150
122
  await Message.findOneAndUpdate(
151
123
  { message_id: values.message_id, body: values.body },
152
124
  { $setOnInsert: messageData },
153
- { upsert: true, new: true }
125
+ { upsert: true, new: true, setDefaultsOnInsert: true }
154
126
  );
155
127
 
156
128
  logger.info('[MongoStorage] Message inserted or updated successfully');
157
129
  } catch (err) {
158
- logger.error('[MongoStorage] Error inserting message:', err);
130
+ logger.error('[MongoStorage] Error inserting message', { error: err.message, stack: err.stack });
159
131
  throw err;
160
132
  }
161
133
  }
@@ -170,15 +142,13 @@ function formatTimestamp(unixTimestamp) {
170
142
  }
171
143
 
172
144
 
173
- function getMessageValues(message, content, reply, is_media) {
145
+ function getMessageValues(message, content) {
174
146
  const nombre_whatsapp = message.pushName;
175
147
  const numero = message.key.participant || message.key.remoteJid;
176
148
  const body = content;
177
149
  const timestamp = formatTimestamp(message.messageTimestamp);
178
150
  const message_id = message.key.id;
179
- const is_group = message.key.remoteJid.endsWith('@g.us');
180
- const group_id = is_group ? message.key.remoteJid : null;
181
- const reply_id = reply || null;
151
+ const group_id = message.key.remoteJid || null;
182
152
  const from_me = message.key.fromMe;
183
153
 
184
154
  return {
@@ -187,10 +157,7 @@ function getMessageValues(message, content, reply, is_media) {
187
157
  body,
188
158
  timestamp,
189
159
  message_id,
190
- is_group,
191
- is_media,
192
160
  group_id,
193
- reply_id,
194
161
  from_me
195
162
  };
196
163
  }
@@ -98,8 +98,6 @@ const addMsgAssistantCore = async (code, inMessages, role = 'user', reply = fals
98
98
  numero: code,
99
99
  body: message,
100
100
  message_id: message_id,
101
- is_group: false,
102
- is_media: false,
103
101
  from_me: true,
104
102
  processed: true,
105
103
  origin: 'system',
@@ -144,8 +142,6 @@ const addInstructionCore = async (code, instruction, role = 'user') => {
144
142
  numero: code,
145
143
  body: instruction,
146
144
  message_id: message_id,
147
- is_group: false,
148
- is_media: false,
149
145
  from_me: true,
150
146
  processed: true,
151
147
  origin: 'instruction',
@@ -5,270 +5,101 @@ const { logger } = require('../utils/logger');
5
5
 
6
6
 
7
7
  const fetchConversationData = async (filter, skip, limit) => {
8
- let filterConditions = { is_group: false };
8
+ const baseMatch = { group_id: null };
9
+ const unreadMatch = { from_me: false, $or: [{ read: false }, { read: { $exists: false } }] };
9
10
 
10
- switch (filter) {
11
- case 'unread':
12
- filterConditions = {
13
- is_group: false,
14
- from_me: false,
15
- $or: [
16
- { read: false },
17
- { read: { $exists: false } }
18
- ]
19
- };
20
- logger.info('Applying unread filter');
21
- break;
22
-
23
- case 'no-response':
24
- logger.info('Applying no-response filter');
25
- break;
26
-
27
- case 'recent': {
28
- const yesterday = new Date();
29
- yesterday.setDate(yesterday.getDate() - 1);
30
- filterConditions = {
31
- is_group: false,
32
- createdAt: { $gt: yesterday }
33
- };
34
- logger.info('Applying recent filter (last 24 hours)');
35
- break;
36
- }
37
-
38
- case 'all':
39
- default:
40
- filterConditions = { is_group: false };
41
- logger.info('Applying all conversations filter');
42
- break;
43
- }
44
-
45
- logger.info('Executing aggregation pipeline...');
46
- const aggregationStartTime = Date.now();
11
+ const filterMap = {
12
+ unread: { ...baseMatch, ...unreadMatch },
13
+ recent: { ...baseMatch, createdAt: { $gt: new Date(Date.now() - 24 * 60 * 60 * 1000) } },
14
+ 'no-response': baseMatch,
15
+ all: baseMatch
16
+ };
17
+
18
+ const filterConditions = filterMap[filter] || baseMatch;
19
+ logger.info(`Applying ${filter} filter`);
47
20
 
48
- let aggregationPipeline = [
21
+ const pipeline = [
49
22
  { $match: filterConditions },
50
- { $project: {
51
- numero: 1,
52
- body: 1,
53
- createdAt: 1,
54
- timestamp: 1,
55
- is_media: 1,
56
- media: 1,
57
- nombre_whatsapp: 1,
58
- from_me: 1
59
- }},
23
+ { $project: { numero: 1, body: 1, createdAt: 1, timestamp: 1, media: 1, nombre_whatsapp: 1, from_me: 1 } },
60
24
  { $sort: { createdAt: 1, timestamp: 1 } },
61
- { $group: {
62
- _id: '$numero',
63
- latestMessage: { $last: '$$ROOT' },
64
- messageCount: { $sum: 1 }
65
- }},
66
- { $sort: { 'latestMessage.createdAt': -1 } }
67
- ];
68
-
69
- if (filter === 'no-response') {
70
- aggregationPipeline.splice(-1, 0, { $match: { 'latestMessage.from_me': false } });
71
- }
72
-
73
- aggregationPipeline.push(
25
+ { $group: { _id: '$numero', latestMessage: { $last: '$$ROOT' }, messageCount: { $sum: 1 } } },
26
+ ...(filter === 'no-response' ? [{ $match: { 'latestMessage.from_me': false } }] : []),
27
+ { $sort: { 'latestMessage.createdAt': -1 } },
74
28
  { $skip: skip },
75
29
  { $limit: limit }
76
- );
77
-
78
- const conversations = await Message.aggregate(aggregationPipeline);
79
-
80
- const aggregationTime = Date.now() - aggregationStartTime;
81
- logger.info(`Aggregation completed in ${aggregationTime}ms, found ${conversations.length} conversations`);
82
-
83
- // Fetch names from Airtable and WhatsApp
84
- const phoneNumbers = conversations.map(conv => conv._id).filter(Boolean);
85
- const formula = 'OR(' +
86
- phoneNumbers.map(p => `{whatsapp_id} = "${p}"`).join(', ') +
87
- ')';
88
- const patientTable = await getRecordByFilter(Historial_Clinico_ID, 'estado_general', formula);
89
- const airtableNameMap = patientTable.reduce((map, patient) => {
90
- map[patient.whatsapp_id] = patient.name;
91
- return map;
92
- }, {});
93
- logger.info(`Found ${Object.keys(airtableNameMap).length} names in Airtable`);
94
-
95
- const contactNames = await Message.aggregate([
96
- { $match: { is_group: false, from_me: false } },
97
- { $sort: { createdAt: -1 } },
98
- { $group: {
99
- _id: '$numero',
100
- name: { $first: '$nombre_whatsapp' }
101
- }}
102
- ]);
103
-
104
- const nameMap = contactNames?.reduce((map, contact) => {
105
- if (contact && contact._id) {
106
- if (!airtableNameMap[contact._id]) {
107
- map[contact._id] = contact.name || 'Unknown';
108
- }
109
- }
110
- return map;
111
- }, {...airtableNameMap}) || airtableNameMap || {};
112
-
113
- // Fetch unread counts
114
- logger.info('Fetching unread counts using Message.aggregate');
115
- const unreadCounts = await Message.aggregate([
116
- {
117
- $match: {
118
- is_group: false,
119
- from_me: false,
120
- $or: [
121
- { read: false },
122
- { read: { $exists: false } }
123
- ]
124
- }
125
- },
126
- { $group: {
127
- _id: '$numero',
128
- unreadCount: { $sum: 1 }
129
- }}
130
- ]);
131
-
132
- const unreadMap = unreadCounts?.reduce((map, item) => {
133
- if (item && item._id) {
134
- map[item._id] = item.unreadCount || 0;
135
- }
136
- return map;
137
- }, {}) || {};
138
- logger.info('Unread map calculated', { unreadMap });
139
- logger.info('Conversations found', { count: conversations?.length || 0 });
140
-
141
- // Calculate total count for pagination
142
- let totalFilterConditions = { is_group: false };
143
-
144
- if (filter === 'unread') {
145
- totalFilterConditions = {
146
- is_group: false,
147
- from_me: false,
148
- $or: [
149
- { read: false },
150
- { read: { $exists: false } }
151
- ]
152
- };
153
- } else if (filter === 'recent') {
154
- const yesterday = new Date();
155
- yesterday.setDate(yesterday.getDate() - 1);
156
- totalFilterConditions = {
157
- is_group: false,
158
- createdAt: { $gt: yesterday }
159
- };
160
- }
161
-
162
- let totalAggregationPipeline = [
163
- { $match: totalFilterConditions },
164
- { $group: { _id: '$numero' } },
165
- { $count: 'total' }
166
30
  ];
167
-
168
- if (filter === 'no-response') {
169
- totalAggregationPipeline = [
170
- { $match: { is_group: false } },
171
- { $project: {
172
- numero: 1,
173
- from_me: 1,
174
- createdAt: 1,
175
- timestamp: 1
176
- }},
177
- { $sort: { createdAt: -1, timestamp: -1 } },
178
- { $group: {
179
- _id: '$numero',
180
- latestMessage: { $first: '$$ROOT' }
181
- }},
182
- { $match: { 'latestMessage.from_me': false } },
183
- { $count: 'total' }
184
- ];
185
- }
186
-
187
- const totalConversations = await Message.aggregate(totalAggregationPipeline, { allowDiskUse: true });
188
- const total = totalConversations[0]?.total || 0;
189
-
190
- return { conversations, total, nameMap, unreadMap };
31
+
32
+ const startTime = Date.now();
33
+ const [conversations, contactNames, unreadCounts, totalResult] = await Promise.all([
34
+ Message.aggregate(pipeline),
35
+ Message.aggregate([{ $match: { ...baseMatch, from_me: false } }, { $sort: { createdAt: -1 } }, { $group: { _id: '$numero', name: { $first: '$nombre_whatsapp' } } }]),
36
+ Message.aggregate([{ $match: { ...baseMatch, ...unreadMatch } }, { $group: { _id: '$numero', unreadCount: { $sum: 1 } } }]),
37
+ Message.aggregate(filter === 'no-response'
38
+ ? [{ $match: baseMatch }, { $project: { numero: 1, from_me: 1, createdAt: 1, timestamp: 1 } }, { $sort: { createdAt: -1 } }, { $group: { _id: '$numero', latestMessage: { $first: '$$ROOT' } } }, { $match: { 'latestMessage.from_me': false } }, { $count: 'total' }]
39
+ : [{ $match: filterConditions }, { $group: { _id: '$numero' } }, { $count: 'total' }]
40
+ )
41
+ ]);
42
+
43
+ logger.info(`Queries completed in ${Date.now() - startTime}ms, found ${conversations.length} conversations`);
44
+
45
+ const phoneNumbers = conversations.map(conv => conv._id).filter(Boolean);
46
+ const airtableNameMap = phoneNumbers.length ?
47
+ (await getRecordByFilter(Historial_Clinico_ID, 'estado_general', `OR(${phoneNumbers.map(p => `{whatsapp_id} = "${p}"`).join(', ')})`)).reduce((map, patient) => ({ ...map, [patient.whatsapp_id]: patient.name }), {}) : {};
48
+
49
+ const nameMap = { ...airtableNameMap, ...contactNames.reduce((map, contact) => contact?._id && !airtableNameMap[contact._id] ? { ...map, [contact._id]: contact.name || 'Unknown' } : map, {}) };
50
+ const unreadMap = unreadCounts.reduce((map, item) => item?._id ? { ...map, [item._id]: item.unreadCount || 0 } : map, {});
51
+
52
+ return { conversations, total: totalResult[0]?.total || 0, nameMap, unreadMap };
191
53
  };
192
54
 
193
55
  /**
194
56
  * Processes conversations to prepare them for the response
195
57
  */
196
58
  const processConversations = async (conversations, nameMap, unreadMap) => {
197
- logger.info('Processing conversations for response...');
198
-
199
- let processedConversations = [];
59
+ const getMediaType = (media) => {
60
+ if (!media) return null;
61
+ if (media.mediaType) return media.mediaType;
62
+ if (!media.contentType) return null;
63
+ const [type, subtype] = media.contentType.split('/');
64
+ return type === 'application' ? 'document' : subtype === 'webp' ? 'sticker' : type;
65
+ };
66
+
67
+ const createConversation = (conv, index, fallback = {}) => {
68
+ const msg = conv?.latestMessage;
69
+ const phoneNumber = conv?._id || fallback.phoneNumber || `error_${index}`;
70
+ return {
71
+ phoneNumber,
72
+ name: nameMap[phoneNumber] || msg?.nombre_whatsapp || fallback.name || 'Unknown',
73
+ lastMessage: msg?.body || fallback.lastMessage || '',
74
+ lastMessageTime: msg?.createdAt || msg?.timestamp || new Date(),
75
+ messageCount: conv?.messageCount || 0,
76
+ unreadCount: unreadMap[phoneNumber] || 0,
77
+ isLastMessageMedia: !!msg?.media,
78
+ lastMessageType: getMediaType(msg?.media),
79
+ lastMessageFromMe: msg?.from_me || false
80
+ };
81
+ };
82
+
200
83
  try {
201
- processedConversations = (conversations || []).map((conv, index) => {
84
+ const processedConversations = (conversations || []).map((conv, index) => {
202
85
  try {
203
- if (!conv || !conv.latestMessage) {
204
- logger.warn(`Conversation ${index} missing latestMessage:`, conv?._id || 'unknown');
205
- return {
206
- phoneNumber: conv?._id || 'unknown',
207
- name: 'Unknown',
208
- lastMessage: '',
209
- lastMessageTime: new Date(),
210
- messageCount: 0,
211
- unreadCount: 0,
212
- isLastMessageMedia: false,
213
- lastMessageType: null,
214
- lastMessageFromMe: false
215
- };
216
- }
217
-
218
- const isMedia = conv.latestMessage.is_media === true;
219
- let mediaType = null;
220
-
221
- if (isMedia && conv?.latestMessage?.media) {
222
- if (conv.latestMessage.media.mediaType) {
223
- mediaType = conv.latestMessage.media.mediaType;
224
- } else if (conv.latestMessage.media.contentType) {
225
- const contentType = conv.latestMessage.media.contentType;
226
- const contentTypeParts = contentType?.split('/') || ['unknown'];
227
- mediaType = contentTypeParts[0] || 'unknown';
228
-
229
- if (mediaType === 'application') {
230
- mediaType = 'document';
231
- } else if (contentTypeParts[1] === 'webp') {
232
- mediaType = 'sticker';
233
- }
234
- }
86
+ if (!conv?.latestMessage) {
87
+ logger.warn(`Conversation ${index} missing latestMessage:`, conv?._id);
88
+ return createConversation(conv, index, { name: 'Unknown' });
235
89
  }
236
-
237
- return {
238
- phoneNumber: conv._id,
239
- name: nameMap[conv._id] || conv?.latestMessage?.nombre_whatsapp || 'Unknown',
240
- lastMessage: conv?.latestMessage?.body || '',
241
- lastMessageTime: conv?.latestMessage?.createdAt || conv?.latestMessage?.timestamp || new Date(),
242
- messageCount: conv.messageCount || 0,
243
- unreadCount: unreadMap[conv._id] || 0,
244
- isLastMessageMedia: isMedia || false,
245
- lastMessageType: mediaType || null,
246
- lastMessageFromMe: conv?.latestMessage?.from_me || false
247
- };
248
- } catch (convError) {
249
- logger.error(`Error processing conversation ${index}:`, convError);
250
- return {
251
- phoneNumber: conv?._id || `error_${index}`,
252
- name: 'Error Processing',
253
- lastMessage: 'Error processing conversation',
254
- lastMessageTime: new Date(),
255
- messageCount: 0,
256
- unreadCount: 0,
257
- isLastMessageMedia: false,
258
- lastMessageType: null,
259
- lastMessageFromMe: false
260
- };
90
+ return createConversation(conv, index);
91
+ } catch (error) {
92
+ logger.error(`Error processing conversation ${index}:`, error);
93
+ return createConversation(conv, index, { name: 'Error Processing', lastMessage: 'Error processing conversation' });
261
94
  }
262
95
  });
263
-
264
- logger.info(`Successfully processed ${processedConversations.length} conversations`);
265
-
266
- } catch (mappingError) {
267
- logger.error('Error in conversation mapping:', mappingError);
268
- processedConversations = [];
96
+
97
+ logger.info(`Processed ${processedConversations.length} conversations`);
98
+ return processedConversations;
99
+ } catch (error) {
100
+ logger.error('Error in conversation mapping:', error);
101
+ return [];
269
102
  }
270
-
271
- return processedConversations;
272
103
  };
273
104
 
274
105
  module.exports = {
@@ -1,5 +1,12 @@
1
1
  const mongoose = require('mongoose');
2
2
  const runtimeConfig = require('../config/runtimeConfig');
3
+
4
+ const { Message, insertMessage } = require('../models/messageModel');
5
+ const { Interaction } = require('../models/interactionModel');
6
+ const { Thread } = require('../models/threadModel');
7
+
8
+ const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
9
+ const { processTwilioMediaMessage } = require('../helpers/twilioMediaProcessor');
3
10
  const { logger } = require('../utils/logger');
4
11
 
5
12
  /**
@@ -18,10 +25,6 @@ class MongoStorage {
18
25
  }
19
26
 
20
27
  createSchemas() {
21
- const { Message } = require('../models/messageModel');
22
- const { Interaction } = require('../models/interactionModel');
23
- const { Thread } = require('../models/threadModel');
24
-
25
28
  return {
26
29
  Message,
27
30
  Interaction,
@@ -45,7 +48,6 @@ class MongoStorage {
45
48
  try {
46
49
  const enrichedMessage = await this._enrichTwilioMedia(messageData);
47
50
  const values = this.buildMessageValues(enrichedMessage);
48
- const { insertMessage } = require('../models/messageModel');
49
51
  await insertMessage(values);
50
52
  logger.info('[MongoStorage] Message stored');
51
53
  return values;
@@ -76,8 +78,6 @@ class MongoStorage {
76
78
  return messageData;
77
79
  }
78
80
 
79
- const { processTwilioMediaMessage } = require('../helpers/twilioMediaProcessor');
80
-
81
81
  const mediaItems = await processTwilioMediaMessage(rawMessage, bucketName);
82
82
  if (!mediaItems || mediaItems.length === 0) {
83
83
  logger.warn('[MongoStorage] Media processing returned no items');
@@ -112,52 +112,19 @@ class MongoStorage {
112
112
  }
113
113
  }
114
114
 
115
- normalizeNumero(numero) {
116
- if (!numero || typeof numero !== 'string') return numero;
117
-
118
- const trimmed = numero.trim();
119
- if (trimmed.startsWith('whatsapp:')) {
120
- return trimmed;
121
- }
122
-
123
- if (trimmed.includes('@')) {
124
- return trimmed;
125
- }
126
-
127
- if (trimmed.startsWith('+')) {
128
- return `whatsapp:${trimmed}`;
129
- }
130
-
131
- if (/^\d+$/.test(trimmed)) {
132
- return `whatsapp:+${trimmed}`;
133
- }
134
-
135
- return trimmed;
136
- }
137
-
138
-
139
115
  buildMessageValues(messageData = {}) {
140
116
  const numero = messageData.to || messageData.code || messageData.numero || messageData.from;
141
- const rawNumero = typeof numero === 'string' ? numero : '';
142
- const normalizedNumero = this.normalizeNumero(rawNumero);
143
- const isGroup = normalizedNumero.includes('@g.us');
117
+ const normalizedNumero = ensureWhatsAppFormat(numero || '');
144
118
  const isMedia = messageData.isMedia === true || (messageData.fileType && messageData.fileType !== 'text');
145
119
  const now = new Date();
146
- const timestamp = now.toISOString();
147
- const nombre = messageData.nombre_whatsapp || messageData.author || messageData.fromName || runtimeConfig.get('USER_DB_MONGO') || process.env.USER_DB_MONGO || 'Nexus';
148
- const processed = messageData.processed || false;
149
- const origin = messageData.origin || 'whatsapp_platform';
150
120
 
151
- // Use message body directly (template rendering is now handled by the provider)
152
- let textBody = messageData.body;
121
+ const nombre = messageData.nombre_whatsapp || messageData.author || messageData.fromName ||
122
+ runtimeConfig.get('USER_DB_MONGO') || process.env.USER_DB_MONGO || 'Nexus';
153
123
 
154
- if (!textBody && isMedia) {
155
- textBody = `[Media:${messageData.fileType || 'attachment'}]`;
156
- } else if (!textBody) {
157
- textBody = '';
158
- }
124
+ const textBody = messageData.body || (isMedia ? `[Media:${messageData.fileType || 'attachment'}]` : '');
159
125
 
160
- const providerId = messageData.messageId || messageData.sid || messageData.id || messageData._id || `pending-${now.getTime()}-${Math.floor(Math.random()*1000)}`;
126
+ const providerId = messageData.messageId || messageData.sid || messageData.id || messageData._id ||
127
+ `pending-${now.getTime()}-${Math.floor(Math.random()*1000)}`;
161
128
 
162
129
  const media = messageData.media || (messageData.fileUrl ? {
163
130
  url: messageData.fileUrl,
@@ -167,33 +134,30 @@ class MongoStorage {
167
134
  metadata: messageData.mediaMetadata || null
168
135
  } : null);
169
136
 
137
+ const statusInfo = messageData.statusInfo || (messageData.delivery_status ? {
138
+ status: messageData.delivery_status,
139
+ errorCode: messageData.delivery_error_code || null,
140
+ errorMessage: messageData.delivery_error_message || null,
141
+ updatedAt: messageData.delivery_status_updated_at || null
142
+ } : null);
143
+
170
144
  return {
171
145
  nombre_whatsapp: nombre,
172
146
  numero: normalizedNumero,
173
147
  body: textBody,
174
- timestamp,
175
- processed,
148
+ timestamp: now.toISOString(),
149
+ processed: messageData.processed || false,
176
150
  message_id: providerId,
177
- is_group: isGroup,
178
- is_media: isMedia,
179
- is_interactive: messageData.isInteractive || false,
180
151
  interactive_type: messageData.interactionType || null,
181
152
  interactive_data: messageData.interactiveData || null,
182
- group_id: isGroup ? normalizedNumero : null,
183
- reply_id: messageData.reply_id || messageData.replyId || null,
184
153
  from_me: messageData.fromMe !== undefined ? messageData.fromMe : true,
185
154
  media,
186
155
  content_sid: messageData.contentSid || null,
187
156
  template_variables: messageData.variables ? JSON.stringify(messageData.variables) : null,
188
157
  raw: messageData.raw || null,
189
- origin,
190
- tools_executed: messageData.tools_executed || [],
191
- statusInfo: messageData.statusInfo || (messageData.delivery_status ? {
192
- status: messageData.delivery_status,
193
- errorCode: messageData.delivery_error_code || null,
194
- errorMessage: messageData.delivery_error_message || null,
195
- updatedAt: messageData.delivery_status_updated_at || null
196
- } : null)
158
+ origin: messageData.origin || 'whatsapp_platform',
159
+ tools_executed: messageData.tools_executed,
160
+ statusInfo
197
161
  };
198
162
  }
199
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.5.6-fix-switch",
3
+ "version": "2.5.7",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",