@peopl-health/nexus 2.0.7 → 2.0.8

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.
@@ -0,0 +1,63 @@
1
+ const { Message } = require('../models/messageModel');
2
+ const { addRecord, getRecordByFilter } = require('../services/airtableService');
3
+ const { Logging_ID } = require('../config/airtableConfig');
4
+
5
+ async function logBugReportToAirtable(reporter, whatsapp_id, description, severity, messageIds = []) {
6
+ try {
7
+ let conversation = null;
8
+ if (messageIds && messageIds.length > 0) {
9
+ const messageObjects = await Message.find({ _id: { $in: messageIds } }).sort({ timestamp: 1 });
10
+ conversation = messageObjects.map(msg => {
11
+ const timestamp = new Date(msg.timestamp).toISOString().slice(0, 16).replace('T', ' ');
12
+ const role = msg.from_me ? 'Assistant' : 'Patient';
13
+ return `[${timestamp}] ${role}: ${msg.body || '(media)'}`;
14
+ }).join('\n');
15
+ }
16
+
17
+ let patientId = null;
18
+ try {
19
+ const patientRecords = await getRecordByFilter(Logging_ID, 'estado_general', `{whatsapp_id}='${whatsapp_id}'`);
20
+ if (patientRecords && patientRecords.length > 0) {
21
+ patientId = patientRecords[0].record_logging_id;
22
+ }
23
+ } catch (err) {
24
+ console.warn('Could not find patient in estado_general:', err.message);
25
+ }
26
+
27
+ const airtableData = {
28
+ reporter,
29
+ patient_id: patientId ? [patientId] : undefined,
30
+ description,
31
+ severity,
32
+ conversation: conversation || undefined
33
+ };
34
+
35
+ await addRecord(Logging_ID, 'bug_reports', airtableData);
36
+ console.log('Bug report logged to Airtable successfully');
37
+ } catch (error) {
38
+ console.error('Error logging bug report to Airtable:', error);
39
+ }
40
+ }
41
+
42
+ const reportBugController = async (req, res) => {
43
+ try {
44
+ const { reporter, whatsapp_id, description, severity, messages } = req.body;
45
+
46
+ if (!reporter) return res.status(400).json({ success: false, error: 'Reporter username is required' });
47
+ if (!whatsapp_id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
48
+ if (!severity || !['low', 'medium', 'high'].includes(severity)) {
49
+ return res.status(400).json({ success: false, error: 'Severity must be low, medium, or high' });
50
+ }
51
+
52
+ logBugReportToAirtable(reporter, whatsapp_id, description, severity, messages).catch(err =>
53
+ console.error('Background bug report logging failed:', err)
54
+ );
55
+
56
+ res.status(201).json({ success: true, message: 'Bug report submitted successfully' });
57
+ } catch (error) {
58
+ console.error('Error submitting bug report:', error);
59
+ res.status(500).json({ success: false, error: error.message });
60
+ }
61
+ };
62
+
63
+ module.exports = { reportBugController };
@@ -1,17 +1,59 @@
1
1
  const { Interaction } = require('../models/interactionModel');
2
+ const { Message } = require('../models/messageModel');
2
3
  const { INTERACTION_QUALITY_VALUES } = require('../config/interactionConfig');
4
+ const { addRecord, getRecordByFilter } = require('../services/airtableService');
5
+ const { Logging_ID } = require('../config/airtableConfig');
6
+
7
+ async function logInteractionToAirtable(messageIds, whatsapp_id, voter_username) {
8
+ try {
9
+ const messageObjects = await Message.find({ _id: { $in: messageIds } }).sort({ createdAt: -1 });
10
+
11
+ const conversation = messageObjects.map(msg => {
12
+ const timestamp = new Date(msg.timestamp).toISOString().slice(0, 16).replace('T', ' ');
13
+ const role = msg.from_me ? 'Assistant' : 'Patient';
14
+ return `[${timestamp}] ${role}: ${msg.body || '(media)'}`;
15
+ }).join('\n');
16
+
17
+ let patientId = null;
18
+ try {
19
+ const patientRecords = await getRecordByFilter(Logging_ID, 'estado_general', `{whatsapp_id}='${whatsapp_id}'`);
20
+ if (patientRecords && patientRecords.length > 0) {
21
+ patientId = patientRecords[0].record_logging_id;
22
+ }
23
+ } catch (err) {
24
+ console.warn('Could not find patient in estado_general:', err.message);
25
+ }
26
+
27
+ const airtableData = {
28
+ patient_id: patientId ? [patientId] : undefined,
29
+ reporter: voter_username,
30
+ conversation
31
+ };
32
+
33
+ await addRecord(Logging_ID, 'interactions', airtableData);
34
+ console.log('Interaction logged to Airtable successfully');
35
+ } catch (error) {
36
+ console.error('Error logging interaction to Airtable:', error);
37
+ }
38
+ }
3
39
 
4
40
  const addInteractionController = async (req, res) => {
5
41
  try {
6
- const { messages, whatsapp_id, quality, description } = req.body;
42
+ const { messages, whatsapp_id, voter_username, quality, description } = req.body;
7
43
  if (!messages || !Array.isArray(messages) || messages.length === 0) {
8
44
  return res.status(400).json({ success: false, error: 'Messages array is required and must not be empty' });
9
45
  }
10
46
  if (!whatsapp_id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
47
+ if (!voter_username) return res.status(400).json({ success: false, error: 'Voter username is required' });
11
48
  if (!quality || !INTERACTION_QUALITY_VALUES.includes(quality)) {
12
49
  return res.status(400).json({ success: false, error: `Quality must be one of: ${INTERACTION_QUALITY_VALUES.join(', ')}` });
13
50
  }
14
- const interaction = await Interaction.create({ messages, whatsapp_id, quality, description });
51
+ const interaction = await Interaction.create({ messages, whatsapp_id, voter_username, quality, description });
52
+
53
+ logInteractionToAirtable(messages, whatsapp_id, voter_username).catch(err =>
54
+ console.error('Background Airtable logging failed:', err)
55
+ );
56
+
15
57
  res.status(201).json({ success: true, interaction });
16
58
  } catch (error) {
17
59
  console.error('Error adding interaction:', error);
@@ -348,40 +348,10 @@ const getLastInteractionController = async (req, res) => {
348
348
  }
349
349
  };
350
350
 
351
- const getMessageQualityController = async (req, res) => {
352
- try {
353
- const { id } = req.params;
354
- const message = await Message.findById(id);
355
- if (!message) return res.status(404).json({ success: false, error: 'Message not found' });
356
- res.status(200).json({ success: true, messageId: id, quality: message.quality });
357
- } catch (error) {
358
- console.error('Error fetching message quality:', error);
359
- res.status(500).json({ success: false, error: error.message });
360
- }
361
- };
362
-
363
- const updateMessageQualityController = async (req, res) => {
364
- try {
365
- const { id } = req.params;
366
- const { quality } = req.body;
367
- if (!['low', 'medium', 'high'].includes(quality)) {
368
- return res.status(400).json({ success: false, error: 'Invalid quality value. Must be low, medium, or high' });
369
- }
370
- const message = await Message.findByIdAndUpdate(id, { quality }, { new: true });
371
- if (!message) return res.status(404).json({ success: false, error: 'Message not found' });
372
- res.status(200).json({ success: true, messageId: id, quality: message.quality });
373
- } catch (error) {
374
- console.error('Error updating message quality:', error);
375
- res.status(500).json({ success: false, error: error.message });
376
- }
377
- };
378
-
379
351
  module.exports = {
380
352
  sendMessageController,
381
353
  sendBulkMessageController,
382
354
  sendBulkMessageAirtableController,
383
355
  getLastInteractionController,
384
- getMessageQualityController,
385
- updateMessageQualityController,
386
356
  configureMessageController
387
357
  };
@@ -0,0 +1,81 @@
1
+ const { QualityMessage } = require('../models/qualityMessageModel');
2
+ const { Message } = require('../models/messageModel');
3
+
4
+ const addQualityVoteController = async (req, res) => {
5
+ try {
6
+ const { message_id, voter_username, quality, notes, context } = req.body;
7
+
8
+ if (!message_id) return res.status(400).json({ success: false, error: 'Message ID is required' });
9
+ if (!voter_username) return res.status(400).json({ success: false, error: 'Voter username is required' });
10
+ if (!quality || !['low', 'medium', 'high'].includes(quality)) {
11
+ return res.status(400).json({ success: false, error: 'Quality must be low, medium, or high' });
12
+ }
13
+
14
+ const message = await Message.findById(message_id);
15
+ if (!message) return res.status(404).json({ success: false, error: 'Message not found' });
16
+
17
+ const qualityVote = await QualityMessage.findOneAndUpdate(
18
+ { message_id, voter_username },
19
+ { quality, notes, context },
20
+ { upsert: true, new: true }
21
+ );
22
+
23
+ res.status(201).json({ success: true, qualityVote });
24
+ } catch (error) {
25
+ console.error('Error adding quality vote:', error);
26
+ res.status(500).json({ success: false, error: error.message });
27
+ }
28
+ };
29
+
30
+ const getQualityVotesByMessageController = async (req, res) => {
31
+ try {
32
+ const { message_id } = req.params;
33
+ const votes = await QualityMessage.find({ message_id }).sort({ createdAt: -1 });
34
+
35
+ const summary = {
36
+ total: votes.length,
37
+ low: votes.filter(v => v.quality === 'low').length,
38
+ medium: votes.filter(v => v.quality === 'medium').length,
39
+ high: votes.filter(v => v.quality === 'high').length
40
+ };
41
+
42
+ res.status(200).json({ success: true, messageId: message_id, summary, votes });
43
+ } catch (error) {
44
+ console.error('Error fetching quality votes:', error);
45
+ res.status(500).json({ success: false, error: error.message });
46
+ }
47
+ };
48
+
49
+ const getQualityVotesByVoterController = async (req, res) => {
50
+ try {
51
+ const { voter_username } = req.params;
52
+ const votes = await QualityMessage.find({ voter_username }).populate('message_id').sort({ createdAt: -1 });
53
+ res.status(200).json({ success: true, voterUsername: voter_username, count: votes.length, votes });
54
+ } catch (error) {
55
+ console.error('Error fetching voter quality votes:', error);
56
+ res.status(500).json({ success: false, error: error.message });
57
+ }
58
+ };
59
+
60
+ const getQualityVoteByMessageAndVoterController = async (req, res) => {
61
+ try {
62
+ const { message_id, voter_username } = req.params;
63
+ const vote = await QualityMessage.findOne({ message_id, voter_username }).populate('message_id');
64
+
65
+ if (!vote) {
66
+ return res.status(404).json({ success: false, error: 'Vote not found' });
67
+ }
68
+
69
+ res.status(200).json({ success: true, vote });
70
+ } catch (error) {
71
+ console.error('Error fetching quality vote:', error);
72
+ res.status(500).json({ success: false, error: error.message });
73
+ }
74
+ };
75
+
76
+ module.exports = {
77
+ addQualityVoteController,
78
+ getQualityVotesByMessageController,
79
+ getQualityVotesByVoterController,
80
+ getQualityVoteByMessageAndVoterController
81
+ };
@@ -1,11 +1,13 @@
1
1
  const { Message, getMessageValues, formatTimestamp } = require('./messageModel');
2
2
  const { Thread } = require('./threadModel');
3
3
  const { Interaction } = require('./interactionModel');
4
+ const { QualityMessage } = require('./qualityMessageModel');
4
5
 
5
6
  module.exports = {
6
7
  Message,
7
8
  Thread,
8
9
  Interaction,
10
+ QualityMessage,
9
11
  getMessageValues,
10
12
  formatTimestamp
11
13
  };
@@ -4,6 +4,7 @@ const { INTERACTION_QUALITY_VALUES } = require('../config/interactionConfig');
4
4
  const interactionSchema = new mongoose.Schema({
5
5
  messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message', required: true }],
6
6
  whatsapp_id: { type: String, required: true, index: true },
7
+ voter_username: { type: String, required: true, index: true },
7
8
  quality: {
8
9
  type: String,
9
10
  enum: INTERACTION_QUALITY_VALUES,
@@ -44,11 +44,6 @@ const messageSchema = new mongoose.Schema({
44
44
  metadata: { type: Object, default: null }
45
45
  },
46
46
  clinical_context: { type: Object, default: null },
47
- quality: {
48
- type: String,
49
- enum: ['low', 'medium', 'high'],
50
- default: 'medium'
51
- },
52
47
  memoryType: {
53
48
  type: String,
54
49
  enum: ['active', 'archived'],
@@ -76,7 +71,7 @@ async function getClinicalContext(whatsappId) {
76
71
  try {
77
72
  const records = await getRecordByFilter(Monitoreo_ID, 'estado_general', `{whatsapp_id}='${whatsappId}'`);
78
73
  if (records && records.length > 0 && records[0]['clinical-context-json']) {
79
- return records[0]['clinical-context-json'];
74
+ return JSON.parse(records[0]['clinical-context-json']);
80
75
  }
81
76
  return null;
82
77
  } catch (error) {
@@ -0,0 +1,19 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const qualityMessageSchema = new mongoose.Schema({
4
+ message_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Message', required: true, index: true },
5
+ voter_username: { type: String, required: true, index: true },
6
+ quality: {
7
+ type: String,
8
+ enum: ['low', 'medium', 'high'],
9
+ required: true
10
+ },
11
+ notes: { type: String, default: null },
12
+ context: { type: String, default: null }
13
+ }, { timestamps: true });
14
+
15
+ qualityMessageSchema.index({ message_id: 1, voter_username: 1 }, { unique: true });
16
+
17
+ const QualityMessage = mongoose.model('QualityMessage', qualityMessageSchema);
18
+
19
+ module.exports = { QualityMessage, qualityMessageSchema };
@@ -20,7 +20,8 @@ const conversationRouteDefinitions = {
20
20
  'GET /:phoneNumber/new': 'getNewMessagesController',
21
21
  'POST /reply': 'getConversationReplyController',
22
22
  'POST /send-template': 'sendTemplateToNewNumberController',
23
- 'POST /:phoneNumber/read': 'markMessagesAsReadController'
23
+ 'POST /:phoneNumber/read': 'markMessagesAsReadController',
24
+ 'POST /report-bug': 'reportBugController'
24
25
  };
25
26
 
26
27
  const mediaRouteDefinitions = {
@@ -33,8 +34,10 @@ const messageRouteDefinitions = {
33
34
  'POST /send-bulk': 'sendBulkMessageController',
34
35
  'POST /send-bulk-airtable': 'sendBulkMessageAirtableController',
35
36
  'GET /last': 'getLastInteractionController',
36
- 'GET /:id/quality': 'getMessageQualityController',
37
- 'PUT /:id/quality': 'updateMessageQualityController'
37
+ 'POST /quality': 'addQualityVoteController',
38
+ 'GET /quality/:message_id': 'getQualityVotesByMessageController',
39
+ 'GET /quality/:message_id/voter/:voter_username': 'getQualityVoteByMessageAndVoterController',
40
+ 'GET /quality/voter/:voter_username': 'getQualityVotesByVoterController'
38
41
  };
39
42
 
40
43
  const interactionRouteDefinitions = {
@@ -78,11 +81,13 @@ const createRouter = (routeDefinitions, controllers) => {
78
81
 
79
82
  // Import built-in controllers
80
83
  const assistantController = require('../controllers/assistantController');
84
+ const bugReportController = require('../controllers/bugReportController');
81
85
  const conversationController = require('../controllers/conversationController');
82
86
  const interactionController = require('../controllers/interactionController');
83
87
  const mediaController = require('../controllers/mediaController');
84
88
  const messageController = require('../controllers/messageController');
85
89
  const patientController = require('../controllers/patientController');
90
+ const qualityMessageController = require('../controllers/qualityMessageController');
86
91
  const templateController = require('../controllers/templateController');
87
92
  const templateFlowController = require('../controllers/templateFlowController');
88
93
  const uploadController = require('../controllers/uploadController');
@@ -108,6 +113,7 @@ const builtInControllers = {
108
113
  getConversationReplyController: conversationController.getConversationReplyController,
109
114
  sendTemplateToNewNumberController: conversationController.sendTemplateToNewNumberController,
110
115
  markMessagesAsReadController: conversationController.markMessagesAsReadController,
116
+ reportBugController: bugReportController.reportBugController,
111
117
 
112
118
  // Interaction controllers
113
119
  addInteractionController: interactionController.addInteractionController,
@@ -122,8 +128,10 @@ const builtInControllers = {
122
128
  sendBulkMessageController: messageController.sendBulkMessageController,
123
129
  sendBulkMessageAirtableController: messageController.sendBulkMessageAirtableController,
124
130
  getLastInteractionController: messageController.getLastInteractionController,
125
- getMessageQualityController: messageController.getMessageQualityController,
126
- updateMessageQualityController: messageController.updateMessageQualityController,
131
+ addQualityVoteController: qualityMessageController.addQualityVoteController,
132
+ getQualityVotesByMessageController: qualityMessageController.getQualityVotesByMessageController,
133
+ getQualityVoteByMessageAndVoterController: qualityMessageController.getQualityVoteByMessageAndVoterController,
134
+ getQualityVotesByVoterController: qualityMessageController.getQualityVotesByVoterController,
127
135
 
128
136
  // Patient controllers
129
137
  getPatientInfoController: patientController.getPatientInfoController,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",