@peopl-health/nexus 3.5.2 → 3.5.4

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.
@@ -524,7 +524,7 @@ class TwilioProvider extends MessageProvider {
524
524
  const wa = req.whatsapp;
525
525
  approvalRequest = {
526
526
  sid: req.sid,
527
- status: (wa?.status || req.status || 'UNKNOWN').toUpperCase(),
527
+ status: (wa?.status || req.status).toUpperCase(),
528
528
  ...(wa && { category: wa.category, name: wa.name, rejectionReason: wa.rejection_reason, contentType: wa.content_type }),
529
529
  dateCreated: req.dateCreated || req.date_created,
530
530
  dateUpdated: req.dateUpdated || req.date_updated
@@ -3,11 +3,10 @@ const { Logging_ID } = require('../config/airtableConfig');
3
3
  const { logger } = require('../utils/logger');
4
4
 
5
5
  const { Message } = require('../models/messageModel');
6
+ const { getBug, VALID_SEVERITIES } = require('../models/bugModel');
6
7
 
7
8
  const { addRecord, getRecordByFilter } = require('../services/airtableService');
8
9
 
9
- const VALID_SEVERITIES = ['low', 'medium', 'high', 'critical'];
10
-
11
10
  async function logBugReportToAirtable(reporter, whatsapp_id, description, severity, messageIds = []) {
12
11
  try {
13
12
  let conversation = null;
@@ -46,18 +45,68 @@ const reportBugController = async (req, res) => {
46
45
  if (!reporter) return res.status(400).json({ success: false, error: 'Reporter username is required' });
47
46
  if (!whatsapp_id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
48
47
  if (!VALID_SEVERITIES.includes(severity)) {
49
- return res.status(400).json({ success: false, error: 'Severity must be low, medium, high, or critical' });
48
+ return res.status(400).json({ success: false, error: `Severity must be one of: ${VALID_SEVERITIES.join(', ')}` });
50
49
  }
51
50
 
51
+ const Bug = getBug();
52
+ const bug = await Bug.create({ reporter, whatsapp_id, description, severity, messages: messages || [] });
53
+
52
54
  logBugReportToAirtable(reporter, whatsapp_id, description, severity, messages).catch(err =>
53
55
  logger.error('Background bug report logging failed:', { error: err.message })
54
56
  );
55
57
 
56
- res.status(201).json({ success: true, message: 'Bug report submitted successfully' });
58
+ res.status(201).json({ success: true, message: 'Bug report submitted successfully', bug });
57
59
  } catch (error) {
58
60
  logger.error('Error submitting bug report:', { error: error.message });
59
61
  res.status(500).json({ success: false, error: error.message });
60
62
  }
61
63
  };
62
64
 
63
- module.exports = { reportBugController };
65
+ const getBugByWhatsappIdController = async (req, res) => {
66
+ try {
67
+ const { whatsapp_id } = req.params;
68
+
69
+ if (!whatsapp_id || whatsapp_id.trim().length === 0) {
70
+ return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
71
+ }
72
+
73
+ const maxLimit = 100;
74
+ const limit = Math.min(Math.max(parseInt(req.query.limit) || 50, 1), maxLimit);
75
+ const page = Math.max(parseInt(req.query.page) || 1, 1);
76
+ const skip = (page - 1) * limit;
77
+
78
+ const Bug = getBug();
79
+ const total = await Bug.countDocuments({ whatsapp_id: whatsapp_id });
80
+ const bugs = await Bug.find({ whatsapp_id: whatsapp_id })
81
+ .populate('messages')
82
+ .sort({ createdAt: -1 })
83
+ .skip(skip)
84
+ .limit(limit);
85
+
86
+ const bugsWithCount = bugs.map(bug => ({
87
+ ...bug.toObject(),
88
+ count: bug.messages ? bug.messages.length : 0
89
+ }));
90
+
91
+ const totalPages = Math.ceil(total / limit);
92
+
93
+ res.status(200).json({
94
+ success: true,
95
+ whatsappId: whatsapp_id,
96
+ bugs: bugsWithCount,
97
+ pagination: {
98
+ page,
99
+ limit,
100
+ total,
101
+ totalPages,
102
+ hasNext: page < totalPages,
103
+ hasPrev: page > 1
104
+ }
105
+ });
106
+ } catch (error) {
107
+ logger.error('Error fetching bug reports:', { error: error.message });
108
+ res.status(500).json({ success: false, error: error.message });
109
+ }
110
+ };
111
+
112
+ module.exports = { reportBugController, getBugByWhatsappIdController };
@@ -113,87 +113,86 @@ const createTemplate = async (req, res) => {
113
113
 
114
114
  const listTemplates = async (req, res) => {
115
115
  try {
116
- const { status: queryStatus, type, limit = 50, showFlows: queryShowFlows } = req.query;
117
- const provider = requireProvider();
118
- const twilioRawTemplates = await provider.listTemplates({ limit: parseInt(limit, 10) });
119
-
120
- const showFlows = type === 'flow' || queryShowFlows === 'true';
121
- const filteredTwilioTemplates = twilioRawTemplates.filter(template => {
122
- const isFlow = template.types && template.types['twilio/flex'];
123
- const statusMatch = queryStatus ? template.status === queryStatus : true;
124
- return (showFlows || !isFlow) && statusMatch;
125
- });
126
-
127
- const validTwilioSids = filteredTwilioTemplates.map(t => t.sid);
128
-
129
- await TemplateModel.deleteMany({ sid: { $nin: validTwilioSids } });
130
-
131
- for (const twilioTemplate of filteredTwilioTemplates) {
132
- const updateFields = {
133
- friendlyName: twilioTemplate.friendlyName,
134
- category: twilioTemplate.types?.['twilio/text']?.categories?.[0] || 'UTILITY',
135
- language: twilioTemplate.language || 'es',
136
- status: twilioTemplate.status || 'DRAFT',
137
- lastUpdated: new Date(),
138
- };
139
-
140
- try {
141
- const approvalInfo = await provider.checkApprovalStatus(twilioTemplate.sid);
142
- if (approvalInfo && approvalInfo.approvalRequest) {
143
- const reqData = approvalInfo.approvalRequest;
144
- updateFields.approvalRequest = {
145
- sid: reqData.sid,
146
- status: reqData.status || 'PENDING',
147
- dateSubmitted: reqData.dateCreated ? new Date(reqData.dateCreated) : new Date(),
148
- dateUpdated: reqData.dateUpdated ? new Date(reqData.dateUpdated) : new Date(),
149
- rejectionReason: reqData.rejectionReason || ''
150
- };
151
- if (reqData.status === 'APPROVED') updateFields.status = 'APPROVED';
152
- else if (reqData.status === 'REJECTED') updateFields.status = 'REJECTED';
153
- else if (reqData.status === 'PENDING') updateFields.status = 'PENDING';
154
- }
155
- } catch (err) {
156
- logger.warn(`Could not fetch approval status for template ${twilioTemplate.sid}:`, err.message);
157
- }
158
-
159
- const onInsertFields = {
160
- sid: twilioTemplate.sid,
161
- name: (twilioTemplate.friendlyName || `template_${twilioTemplate.sid}`).replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(),
162
- dateCreated: new Date(twilioTemplate.dateCreated),
163
- body: '',
164
- footer: '',
165
- variables: [],
166
- components: []
167
- };
116
+ const { status: queryStatus, type, limit: queryLimit = 50, showFlows: queryShowFlows } = req.query;
117
+ const page = Math.max(parseInt(req.query.page) || 1, 1);
118
+ const limit = Math.min(Math.max(parseInt(queryLimit) || 50, 1), 100);
119
+ const skip = (page - 1) * limit;
168
120
 
169
- await TemplateModel.updateOne(
170
- { sid: twilioTemplate.sid },
171
- {
172
- $set: updateFields,
173
- $setOnInsert: onInsertFields
174
- },
175
- { upsert: true }
176
- );
177
- }
178
-
179
- const findCriteria = { sid: { $in: validTwilioSids } };
121
+ const findCriteria = {};
122
+
180
123
  if (queryStatus) {
181
124
  findCriteria.status = queryStatus;
182
125
  }
126
+
127
+ const showFlows = type === 'flow' || queryShowFlows === 'true';
128
+ if (showFlows) {
129
+ findCriteria.type = 'twilio/flex';
130
+ } else {
131
+ findCriteria.type = { $ne: 'twilio/flex' };
132
+ }
183
133
 
184
- let finalTemplates = await TemplateModel.find(findCriteria)
134
+ const total = await TemplateModel.countDocuments(findCriteria);
135
+ const totalPages = Math.ceil(total / limit);
136
+
137
+ const templates = await TemplateModel.find(findCriteria)
185
138
  .sort({ dateCreated: -1 })
186
- .limit(parseInt(limit, 10))
139
+ .skip(skip)
140
+ .limit(limit)
187
141
  .lean();
188
142
 
189
- const formattedTemplates = finalTemplates.map(_formatTemplate);
143
+ const provider = requireProvider();
144
+
145
+ const approvalPromises = templates
146
+ .filter(template => template.status !== 'APPROVED' && template.status !== 'REJECTED')
147
+ .map(async (template) => {
148
+ try {
149
+ const approvalInfo = await provider.checkApprovalStatus(template.sid);
150
+ if (approvalInfo && approvalInfo.approvalRequest) {
151
+ const reqData = approvalInfo.approvalRequest;
152
+ const updateFields = {
153
+ approvalRequest: {
154
+ sid: reqData.sid,
155
+ status: reqData.status,
156
+ dateSubmitted: reqData.dateCreated ? new Date(reqData.dateCreated) : new Date(),
157
+ dateUpdated: reqData.dateUpdated ? new Date(reqData.dateUpdated) : new Date(),
158
+ rejectionReason: reqData.rejectionReason || ''
159
+ },
160
+ lastUpdated: new Date(),
161
+ status: reqData.status
162
+ };
163
+
164
+ await TemplateModel.updateOne(
165
+ { sid: template.sid },
166
+ { $set: updateFields }
167
+ );
168
+
169
+ Object.assign(template, {
170
+ approvalRequest: updateFields.approvalRequest,
171
+ status: updateFields.status,
172
+ lastUpdated: updateFields.lastUpdated
173
+ });
174
+ }
175
+ } catch (err) {
176
+ logger.warn(`Could not fetch approval status for template ${template.sid}:`, err.message);
177
+ }
178
+ });
179
+
180
+ await Promise.all(approvalPromises);
190
181
 
191
182
  return res.status(200).json({
192
183
  success: true,
193
- templates: formattedTemplates,
194
- count: formattedTemplates.length
184
+ templates: templates.map(_formatTemplate),
185
+ count: templates.length,
186
+ pagination: {
187
+ page,
188
+ limit,
189
+ total,
190
+ totalPages,
191
+ hasNext: page < totalPages,
192
+ hasPrev: page > 1
193
+ }
195
194
  });
196
-
195
+
197
196
  } catch (error) {
198
197
  logger.error('Error in listTemplates:', { error: error.message });
199
198
  return handleApiError(res, error, 'Failed to list templates');
@@ -0,0 +1,23 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const { getDb, getModelDatabase } = require('../config/mongoConfig');
4
+ const { logger } = require('../utils/logger');
5
+
6
+ const VALID_SEVERITIES = ['low', 'medium', 'high', 'critical'];
7
+
8
+ const bugSchema = new mongoose.Schema({
9
+ reporter: { type: String, required: true, index: true },
10
+ whatsapp_id: { type: String, required: true, index: true },
11
+ description: { type: String, required: true },
12
+ severity: { type: String, required: true, enum: VALID_SEVERITIES },
13
+ messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }]
14
+ }, { timestamps: true });
15
+
16
+ const getBug = () => {
17
+ const dbName = getModelDatabase('Bug');
18
+ const db = dbName ? getDb(dbName) : mongoose;
19
+ logger.debug('[Bug] Using database', { dbName: dbName || 'default' });
20
+ return db.models.Bug || db.model('Bug', bugSchema);
21
+ };
22
+
23
+ module.exports = { getBug, bugSchema, VALID_SEVERITIES };
@@ -9,11 +9,12 @@ const TemplateSchema = new mongoose.Schema({
9
9
  default: 'UTILITY',
10
10
  enum: ['UTILITY', 'MARKETING', 'AUTHENTICATION']
11
11
  },
12
+ type: String,
12
13
  language: { type: String, default: 'es' },
13
14
  status: {
14
15
  type: String,
15
- default: 'DRAFT',
16
- enum: ['DRAFT', 'PENDING', 'APPROVED', 'REJECTED', 'UNKNOWN']
16
+ default: 'UNSUBMITTED',
17
+ enum: ['UNSUBMITTED', 'RECEIVED', 'PENDING', 'APPROVED', 'REJECTED', 'PAUSED', 'DISABLED']
17
18
  },
18
19
  body: String,
19
20
  footer: String,
@@ -22,8 +22,12 @@ const conversationRouteDefinitions = {
22
22
  'POST /reply': 'getConversationReplyController',
23
23
  'POST /send-template': 'sendTemplateToNewNumberController',
24
24
  'POST /:phoneNumber/read': 'markMessagesAsReadController',
25
+ 'POST /case-documentation': 'caseDocumentationController',
25
26
  'POST /report-bug': 'reportBugController',
26
- 'POST /case-documentation': 'caseDocumentationController'
27
+ 'POST /bug': 'reportBugController',
28
+ 'GET /bug/:whatsapp_id': 'getBugByWhatsappIdController',
29
+ 'POST /interaction': 'addInteractionController',
30
+ 'GET /interaction/:whatsapp_id': 'getInteractionsByWhatsappIdController'
27
31
  };
28
32
 
29
33
  const mediaRouteDefinitions = {
@@ -130,6 +134,7 @@ const builtInControllers = {
130
134
  markMessagesAsReadController: conversationController.markMessagesAsReadController,
131
135
  searchMessagesByNumberController: conversationController.searchMessagesByNumberController,
132
136
  reportBugController: bugReportController.reportBugController,
137
+ getBugByWhatsappIdController: bugReportController.getBugByWhatsappIdController,
133
138
  caseDocumentationController: caseDocumentationController.caseDocumentationController,
134
139
 
135
140
  // Interaction controllers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.5.2",
3
+ "version": "3.5.4",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",