@peopl-health/nexus 3.5.1 → 3.5.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.
@@ -344,18 +344,58 @@ const searchConversationsController = async (req, res) => {
344
344
 
345
345
  const getConversationsByNameController = async (req, res) => {
346
346
  try {
347
- const conversations = await Message.aggregate([
348
- { $match: { from_me: false, group_id: null } },
349
- { $sort: { createdAt: -1 } },
350
- { $group: {
351
- _id: '$numero',
352
- name: { $first: '$nombre_whatsapp' },
353
- latestMessage: { $first: '$$ROOT' },
354
- messageCount: { $sum: 1 }
355
- }},
356
- { $sort: { 'latestMessage.createdAt': -1 } }
357
- ]);
358
-
347
+ const { name } = req.query;
348
+
349
+ if (!name || name.trim().length === 0) {
350
+ return res.status(400).json({
351
+ success: false,
352
+ error: 'Name parameter is required'
353
+ });
354
+ }
355
+
356
+ const escapedName = name.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
357
+
358
+ const limit = Math.min(Math.max(parseInt(req.query.limit) || 10, 1), 100);
359
+ const page = Math.max(parseInt(req.query.page) || 1, 1);
360
+ const skip = (page - 1) * limit;
361
+
362
+ const matchStage = {
363
+ $match: {
364
+ group_id: null,
365
+ nombre_whatsapp: { $regex: escapedName, $options: 'i' }
366
+ }
367
+ };
368
+
369
+ const pipeline = [
370
+ matchStage,
371
+ {
372
+ $group: {
373
+ _id: '$numero',
374
+ name: { $first: '$nombre_whatsapp' },
375
+ latestMessage: { $first: '$$ROOT' },
376
+ messageCount: { $sum: 1 }
377
+ }
378
+ },
379
+ { $sort: { 'latestMessage.createdAt': -1 } },
380
+ {
381
+ $facet: {
382
+ data: [
383
+ { $skip: skip },
384
+ { $limit: limit }
385
+ ],
386
+ totalCount: [
387
+ { $count: 'total' }
388
+ ]
389
+ }
390
+ }
391
+ ];
392
+
393
+ const [facetResult] = await Message.aggregate(pipeline);
394
+
395
+ const conversations = facetResult?.data ?? [];
396
+ const total = facetResult?.totalCount[0]?.total ?? 0;
397
+ const totalPages = Math.ceil(total / limit);
398
+
359
399
  res.status(200).json({
360
400
  success: true,
361
401
  conversations: conversations.map(conv => ({
@@ -364,7 +404,15 @@ const getConversationsByNameController = async (req, res) => {
364
404
  lastMessage: conv.latestMessage.body,
365
405
  lastMessageTime: conv.latestMessage.timestamp,
366
406
  messageCount: conv.messageCount
367
- }))
407
+ })),
408
+ pagination: {
409
+ page,
410
+ limit,
411
+ total,
412
+ totalPages,
413
+ hasNext: page < totalPages,
414
+ hasPrev: page > 1
415
+ }
368
416
  });
369
417
  } catch (error) {
370
418
  logger.error('Error fetching conversations by name:', { error });
@@ -563,11 +611,73 @@ const getOpenAIThreadMessagesController = async (req, res) => {
563
611
  }
564
612
  };
565
613
 
614
+ const searchMessagesByNumberController = async (req, res) => {
615
+ try {
616
+ const { phoneNumber } = req.params;
617
+ const { query } = req.query;
618
+
619
+ if (!phoneNumber || phoneNumber.trim().length === 0) {
620
+ return res.status(400).json({
621
+ success: false,
622
+ error: 'Phone number is required'
623
+ });
624
+ }
625
+
626
+ if (!query || query.trim().length === 0) {
627
+ return res.status(400).json({
628
+ success: false,
629
+ error: 'Search query is required'
630
+ });
631
+ }
632
+
633
+ const maxLimit = 100;
634
+ const limit = Math.min(Math.max(parseInt(req.query.limit) || 50, 1), maxLimit);
635
+ const page = Math.max(parseInt(req.query.page) || 1, 1);
636
+ const skip = (page - 1) * limit;
637
+
638
+ const escapedQuery = query.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
639
+ const formattedPhoneNumber = ensureWhatsAppFormat(phoneNumber);
640
+
641
+ const mongoQuery = {
642
+ group_id: null,
643
+ numero: formattedPhoneNumber,
644
+ body: { $regex: escapedQuery, $options: 'i' }
645
+ };
646
+
647
+ const mongoSort = { createdAt: -1 };
648
+
649
+ const total = await Message.countDocuments(mongoQuery);
650
+ const messages = await Message.find(mongoQuery).sort(mongoSort).skip(skip).limit(limit).lean();
651
+ const totalPages = Math.ceil(total / limit);
652
+
653
+ res.status(200).json({
654
+ success: true,
655
+ messages,
656
+ pagination: {
657
+ page,
658
+ limit,
659
+ total,
660
+ totalPages,
661
+ hasNext: page < totalPages,
662
+ hasPrev: page > 1
663
+ },
664
+ });
665
+
666
+ } catch (error) {
667
+ logger.error('Error fetching conversation messages by number:', {error: error.message});
668
+ res.status(500).json({
669
+ success: false,
670
+ error: error.message || 'Failed to fetch conversation messages'
671
+ });
672
+ }
673
+ };
674
+
566
675
  module.exports = {
567
676
  getConversationController,
568
677
  getConversationMessagesController,
569
678
  getConversationReplyController,
570
679
  getConversationsByNameController,
680
+ searchMessagesByNumberController,
571
681
  getNewMessagesController,
572
682
  markMessagesAsReadController,
573
683
  searchConversationsController,
@@ -1,8 +1,9 @@
1
1
  const { logger } = require('../utils/logger');
2
2
  const { Message } = require('../models/messageModel');
3
- const { requireProvider, sendMessage } = require('../core/NexusMessaging');
4
3
  const { Template } = require('../templates/templateStructure');
5
4
 
5
+ const getMessaging = () => require('../core/NexusMessaging');
6
+
6
7
  async function handle24HourWindowError(message, messageSid) {
7
8
  try {
8
9
  if (!message?.body || !message?.numero) return;
@@ -26,6 +27,7 @@ async function handle24HourWindowError(message, messageSid) {
26
27
  return;
27
28
  }
28
29
 
30
+ const { requireProvider, sendMessage } = getMessaging();
29
31
  const provider = requireProvider();
30
32
  if (typeof provider.createTemplate !== 'function') return;
31
33
 
@@ -90,6 +90,9 @@ messageSchema.index({ numero: 1, createdAt: -1, processed: 1 }, { name: 'numero_
90
90
  messageSchema.index({ group_id: 1, createdAt: 1 }, { name: 'conversation_sort_idx' });
91
91
  messageSchema.index({ group_id: 1, from_me: 1, read: 1 }, { name: 'unread_filter_idx' });
92
92
  messageSchema.index({ group_id: 1, numero: 1, createdAt: -1 }, { name: 'conversation_lookup_idx' });
93
+
94
+ messageSchema.index({ nombre_whatsapp: 1, createdAt: -1 }, { name: 'nombre_whatsapp_idx' });
95
+
93
96
  messageSchema.index({ createdAt: -1 }, { name: 'global_sort_idx' });
94
97
 
95
98
  messageSchema.pre('save', function (next) {
@@ -117,6 +120,7 @@ async function getClinicalContext(whatsappId) {
117
120
  try {
118
121
  const records = await getRecordByFilter(Monitoreo_ID, 'estado_general', `{whatsapp_id}='${whatsappId}'`);
119
122
  let clinicalContext = null;
123
+ let nombreWhatsapp = records?.[0]?.['name'] || null;
120
124
 
121
125
  if (records && records.length > 0 && records[0]['clinical-context-json']) {
122
126
  clinicalContext = JSON.parse(records[0]['clinical-context-json']);
@@ -129,11 +133,11 @@ async function getClinicalContext(whatsappId) {
129
133
  }
130
134
 
131
135
  CLINICAL_CONTEXT_CACHE.set(whatsappId, {
132
- data: clinicalContext,
136
+ data: {nombreWhatsapp, clinicalContext},
133
137
  timestamp: now
134
138
  });
135
139
 
136
- return clinicalContext;
140
+ return {nombreWhatsapp, clinicalContext};
137
141
  } catch (error) {
138
142
  logger.error('Error fetching clinical context from Airtable:', error);
139
143
  return null;
@@ -142,12 +146,15 @@ async function getClinicalContext(whatsappId) {
142
146
 
143
147
  async function insertMessage(values) {
144
148
  try {
145
- const clinical_context = await getClinicalContext(values.numero);
149
+ const {nombreWhatsapp, clinicalContext} = await getClinicalContext(values.numero);
146
150
  const messageData = Object.fromEntries(
147
151
  Object.entries(values)
148
152
  .filter(([k, v]) => v !== undefined && !k.startsWith('delivery_'))
149
153
  );
150
- messageData.clinical_context = clinical_context;
154
+ messageData.clinical_context = clinicalContext;
155
+ if (nombreWhatsapp && values.from_me === false) {
156
+ messageData.nombre_whatsapp = nombreWhatsapp;
157
+ }
151
158
  if (!messageData.statusInfo && values.delivery_status) {
152
159
  messageData.statusInfo = {
153
160
  status: values.delivery_status,
@@ -18,6 +18,7 @@ const conversationRouteDefinitions = {
18
18
  'GET /:phoneNumber': 'getConversationMessagesController',
19
19
  'GET /:phoneNumber/new': 'getNewMessagesController',
20
20
  'GET /:phoneNumber/openai': 'getOpenAIThreadMessagesController',
21
+ 'GET /:phoneNumber/search': 'searchMessagesByNumberController',
21
22
  'POST /reply': 'getConversationReplyController',
22
23
  'POST /send-template': 'sendTemplateToNewNumberController',
23
24
  'POST /:phoneNumber/read': 'markMessagesAsReadController',
@@ -127,6 +128,7 @@ const builtInControllers = {
127
128
  getConversationReplyController: conversationController.getConversationReplyController,
128
129
  sendTemplateToNewNumberController: conversationController.sendTemplateToNewNumberController,
129
130
  markMessagesAsReadController: conversationController.markMessagesAsReadController,
131
+ searchMessagesByNumberController: conversationController.searchMessagesByNumberController,
130
132
  reportBugController: bugReportController.reportBugController,
131
133
  caseDocumentationController: caseDocumentationController.caseDocumentationController,
132
134
 
@@ -1,6 +1,6 @@
1
1
  const { v4: uuidv4 } = require('uuid');
2
2
 
3
- const { requireProvider } = require('../core/NexusMessaging');
3
+ const getRequireProvider = () => require('../core/NexusMessaging').requireProvider;
4
4
 
5
5
  class Template {
6
6
  constructor(name, category = 'UTILITY', language = 'es') {
@@ -145,7 +145,7 @@ class Template {
145
145
 
146
146
  async save() {
147
147
  const twilioFormat = this.toTwilioFormat();
148
- return requireProvider().createTemplate(twilioFormat);
148
+ return getRequireProvider()().createTemplate(twilioFormat);
149
149
  }
150
150
 
151
151
  static normalizeVariables(input = []) {
@@ -1,7 +1,10 @@
1
1
  const { logger } = require('../utils/logger');
2
2
 
3
3
  const handleApiError = (res, error, message) => {
4
- logger.error(message, { error });
4
+ logger.error(message, {
5
+ error: error?.message || String(error),
6
+ stack: error?.stack
7
+ });
5
8
  const code = error.statusCode;
6
9
  const status = (typeof code === 'number' && code >= 400 && code < 600) ? code : 500;
7
10
  const bodyMessage = error.message || message;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.5.1",
3
+ "version": "3.5.2",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",