@peopl-health/nexus 3.14.2 → 3.15.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.
package/README.md CHANGED
@@ -175,21 +175,6 @@ You can configure Nexus via environment variables or at runtime using simple inj
175
175
 
176
176
  Injection points (dependency injection)
177
177
 
178
- - Message scheduling (use your Agenda/Bull model + scheduler):
179
- ```js
180
- const { configureMessageController } = require('@peopl-health/nexus/lib/controllers/messageController');
181
- const { AgendaMessage } = require('./src/models/agendaMessageModel');
182
- const { sendScheduledMessage: appSchedule } = require('./src/messaging/scheduledMessageService');
183
-
184
- const provider = nexus.getMessaging().getProvider(); // e.g., TwilioProvider
185
- configureMessageController({
186
- ScheduledMessage: AgendaMessage, // must expose create/find/findById/deleteOne
187
- sendScheduledMessage: (saved) => appSchedule(provider.twilioClient, saved),
188
- // Optional: only if you use bulk‑airtable
189
- getRecordByFilter: require('@peopl-health/nexus/lib/services/airtableService').getRecordByFilter
190
- });
191
- ```
192
-
193
178
  - Media (inject only your bucket name; AWS SDK is loaded by the lib):
194
179
  ```js
195
180
  const { configureMediaController } = require('@peopl-health/nexus/lib/controllers/mediaController');
@@ -95,8 +95,10 @@ class TwilioProvider extends MessageProvider {
95
95
 
96
96
  if (fileUrl && fileType !== 'text') {
97
97
  const mediaPrep = await this._prepareOutboundMedia(messageData, formattedCode);
98
- const outboundMediaUrl = mediaPrep.mediaUrl || fileUrl;
99
- messageParams.mediaUrl = [outboundMediaUrl];
98
+ if (!contentSid) {
99
+ const outboundMediaUrl = mediaPrep.mediaUrl || fileUrl;
100
+ messageParams.mediaUrl = [outboundMediaUrl];
101
+ }
100
102
  if (!messageParams.body || messageParams.body.trim() === '') {
101
103
  delete messageParams.body;
102
104
  }
@@ -66,6 +66,27 @@ const reportBugController = async (req, res) => {
66
66
  }
67
67
  };
68
68
 
69
+ const updateBugController = async (req, res) => {
70
+ try {
71
+ const { recordId } = req.params;
72
+ if (!recordId) return res.status(400).json({ success: false, error: 'recordId is required' });
73
+
74
+ const { fields } = req.body || {};
75
+ if (!fields || Object.keys(fields).length === 0) {
76
+ return res.status(400).json({ success: false, error: 'No fields provided for update' });
77
+ }
78
+
79
+ const Bug = getBug();
80
+ const updatedBug = await Bug.findOneAndUpdate({ recordId }, fields, { new: true });
81
+ if (!updatedBug) return res.status(404).json({ success: false, error: 'Bug not found' });
82
+
83
+ res.status(200).json({ success: true, bug: updatedBug });
84
+ } catch (error) {
85
+ logger.error('Error updating bug report:', { error: error.message, recordId: req.params?.recordId });
86
+ res.status(500).json({ success: false, error: error.message });
87
+ }
88
+ };
89
+
69
90
  const getBugByWhatsappIdController = async (req, res) => {
70
91
  try {
71
92
  const { whatsapp_id } = req.params;
@@ -113,4 +134,4 @@ const getBugByWhatsappIdController = async (req, res) => {
113
134
  }
114
135
  };
115
136
 
116
- module.exports = { reportBugController, getBugByWhatsappIdController };
137
+ module.exports = { reportBugController, getBugByWhatsappIdController, updateBugController };
@@ -3,48 +3,30 @@ const moment = require('moment-timezone');
3
3
  const { logger } = require('../utils/logger');
4
4
 
5
5
  const { Message } = require('../models/messageModel.js');
6
- const { ScheduledMessage: DefaultScheduledMessage } = require('../models/agendaMessageModel.js');
6
+ const { ScheduledMessage } = require('../models/agendaMessageModel.js');
7
7
  const FlowRouting = require('../models/flowRoutingModel.js');
8
8
 
9
9
  const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
10
10
  const { ensureFlowTokenInVariables } = require('../helpers/templateFlowControllerHelper');
11
11
 
12
- const { getRecordByFilter: defaultGetRecordByFilter } = require('../services/airtableService');
12
+ const { getRecordByFilter } = require('../services/airtableService');
13
13
 
14
- const {
15
- sendMessage: defaultSendMessage,
16
- sendScheduledMessage: defaultSendScheduledMessage,
17
- getDefaultInstance
18
- } = require('../core/NexusMessaging');
19
-
20
- const dependencies = {
21
- ScheduledMessage: DefaultScheduledMessage,
22
- getRecordByFilter: defaultGetRecordByFilter,
23
- sendScheduledMessage: defaultSendScheduledMessage,
24
- sendMessage: defaultSendMessage
25
- };
26
-
27
- const configureMessageController = (overrides = {}) => {
28
- Object.keys(overrides).forEach(key => {
29
- if (overrides[key]) dependencies[key] = overrides[key];
30
- });
31
- return { ...dependencies };
32
- };
14
+ const { sendMessage, sendScheduledMessage, getDefaultInstance } = require('../core/NexusMessaging');
33
15
 
34
16
  const _validateMessagingDeps = (res, requireAirtable = false) => {
35
- if (!dependencies.ScheduledMessage) {
17
+ if (!ScheduledMessage) {
36
18
  res.status(500).json({ success: false, error: 'ScheduledMessage model not configured' });
37
19
  return false;
38
20
  }
39
- if (!dependencies.sendScheduledMessage && !dependencies.sendMessage) {
21
+ if (!sendScheduledMessage && !sendMessage) {
40
22
  res.status(500).json({ success: false, error: 'No messaging provider configured' });
41
23
  return false;
42
24
  }
43
- if ((dependencies.sendMessage === defaultSendMessage || dependencies.sendScheduledMessage === defaultSendScheduledMessage) && !getDefaultInstance()) {
25
+ if (!getDefaultInstance()) {
44
26
  res.status(500).json({ success: false, error: 'NexusMessaging not initialized' });
45
27
  return false;
46
28
  }
47
- if (requireAirtable && !dependencies.getRecordByFilter) {
29
+ if (requireAirtable && !getRecordByFilter) {
48
30
  res.status(500).json({ success: false, error: 'Airtable getRecordByFilter not configured' });
49
31
  return false;
50
32
  }
@@ -56,11 +38,11 @@ const _pickMessageId = (result, fallback) =>
56
38
  fallback?.wa_id || fallback?.sid || fallback?.id || fallback?._id?.toString() || null;
57
39
 
58
40
  const _sendAndPersist = async (payload) => {
59
- const saved = await dependencies.ScheduledMessage.create(payload);
41
+ const saved = await ScheduledMessage.create(payload);
60
42
 
61
- const result = dependencies.sendScheduledMessage
62
- ? await dependencies.sendScheduledMessage(saved)
63
- : await dependencies.sendMessage({ ...payload, body: payload.message });
43
+ const result = sendScheduledMessage
44
+ ? await sendScheduledMessage(saved)
45
+ : await sendMessage({ ...payload, body: payload.message });
64
46
  return { result, saved };
65
47
  };
66
48
 
@@ -168,7 +150,7 @@ const sendBulkMessageAirtableController = async (req, res) => {
168
150
 
169
151
  try {
170
152
  const isFlowTemplate = contentSid && await FlowRouting.findOne({ contentSid }).lean();
171
- const rows = await dependencies.getRecordByFilter(baseId, tableName, condition);
153
+ const rows = await getRecordByFilter(baseId, tableName, condition);
172
154
  let extraDelay = 0;
173
155
  const sentPhones = new Set();
174
156
  const payloads = [];
@@ -235,12 +217,12 @@ const checkScheduledMessageStatusController = async (req, res) => {
235
217
  if (!contentSid || !code) {
236
218
  return res.status(400).json({ success: false, error: 'contentSid and code are required' });
237
219
  }
238
- if (!dependencies.ScheduledMessage) {
220
+ if (!ScheduledMessage) {
239
221
  return res.status(500).json({ success: false, error: 'ScheduledMessage model not configured' });
240
222
  }
241
223
 
242
224
  try {
243
- const msg = await dependencies.ScheduledMessage.findOne({ contentSid, code: ensureWhatsAppFormat(code) });
225
+ const msg = await ScheduledMessage.findOne({ contentSid, code: ensureWhatsAppFormat(code) });
244
226
  if (!msg) {
245
227
  return res.status(404).json({ success: false, error: 'ScheduledMessage not found' });
246
228
  }
@@ -278,6 +260,5 @@ module.exports = {
278
260
  sendBulkMessageAirtableController,
279
261
  getLastInteractionController,
280
262
  checkScheduledMessageStatusController,
281
- checkMessageStatusController,
282
- configureMessageController
263
+ checkMessageStatusController
283
264
  };
@@ -0,0 +1,29 @@
1
+ class BaseJob {
2
+ constructor({ queueAdapter, queueName } = {}) {
3
+ if (!queueAdapter) {
4
+ throw new Error(`${this.constructor.name} requires queueAdapter`);
5
+ }
6
+ if (!queueName) {
7
+ throw new Error(`${this.constructor.name} requires queueName`);
8
+ }
9
+
10
+ this.queueAdapter = queueAdapter;
11
+ this.queueName = queueName;
12
+
13
+ this.queueAdapter.process(queueName, (data) => this._process(data));
14
+ }
15
+
16
+ async _enqueue(payload, options = {}) {
17
+ return await this.queueAdapter.enqueue(this.queueName, payload, options);
18
+ }
19
+
20
+ async _cancelJob(jobId) {
21
+ return await this.queueAdapter.cancelJob(jobId);
22
+ }
23
+
24
+ async _process(_data) {
25
+ throw new Error(`${this.constructor.name}: _process(data) must be implemented`);
26
+ }
27
+ }
28
+
29
+ module.exports = { BaseJob };
@@ -0,0 +1,56 @@
1
+ const { logger } = require('../utils/logger');
2
+ const { BaseJob } = require('./BaseJob');
3
+
4
+ const QUEUE_NAME = 'scheduled-messages';
5
+
6
+ const DEFAULT_JOB_OPTIONS = {
7
+ attempts: 3,
8
+ backoff: { type: 'exponential', delay: 2000 },
9
+ removeOnComplete: true,
10
+ removeOnFail: 50
11
+ };
12
+
13
+ class ScheduledMessageJob extends BaseJob {
14
+ constructor({ queueAdapter } = {}) {
15
+ super({ queueAdapter, queueName: QUEUE_NAME });
16
+ }
17
+
18
+ async schedule({ scheduledMessageId, sendTime }) {
19
+ if (!scheduledMessageId) throw new Error('scheduledMessageId is required');
20
+ if (!sendTime) throw new Error('sendTime is required');
21
+
22
+ const delay = Math.max(0, new Date(sendTime).getTime() - Date.now());
23
+ const jobId = this._buildJobId(scheduledMessageId);
24
+
25
+ const enqueuedJobId = await this._enqueue(
26
+ { scheduledMessageId: String(scheduledMessageId) },
27
+ { ...DEFAULT_JOB_OPTIONS, delay, jobId }
28
+ );
29
+
30
+ logger.info('[ScheduledMessageJob] Enqueued', { scheduledMessageId, jobId: enqueuedJobId, delay, sendTime });
31
+
32
+ return { jobId: enqueuedJobId, delay };
33
+ }
34
+
35
+ async cancel(scheduledMessageId) {
36
+ if (!scheduledMessageId) throw new Error('scheduledMessageId is required');
37
+
38
+ const jobId = this._buildJobId(scheduledMessageId);
39
+ const cancelled = await this._cancelJob(jobId);
40
+
41
+ logger.info('[ScheduledMessageJob] Cancel requested', { scheduledMessageId, jobId, cancelled });
42
+
43
+ return cancelled;
44
+ }
45
+
46
+ _buildJobId(scheduledMessageId) {
47
+ return `scheduled-msg-${scheduledMessageId}`;
48
+ }
49
+
50
+ async _process(data) {
51
+ logger.warn('[ScheduledMessageJob] Processor stub invoked; real implementation pending', data);
52
+ return { success: false, reason: 'not_implemented' };
53
+ }
54
+ }
55
+
56
+ module.exports = { ScheduledMessageJob };
@@ -0,0 +1,45 @@
1
+ const { logger } = require('../utils/logger');
2
+ const { BaseJob } = require('./BaseJob');
3
+
4
+ const QUEUE_NAME = 'template-approval';
5
+ const DEFAULT_DELAY_MS = 15 * 60 * 1000;
6
+
7
+ const DEFAULT_JOB_OPTIONS = {
8
+ attempts: 3,
9
+ backoff: { type: 'exponential', delay: 5000 },
10
+ removeOnComplete: true,
11
+ removeOnFail: 50
12
+ };
13
+
14
+ class TemplateApprovalJob extends BaseJob {
15
+ constructor({ queueAdapter } = {}) {
16
+ super({ queueAdapter, queueName: QUEUE_NAME });
17
+ }
18
+
19
+ async schedule({ templateSid, messageId, originalMessageSid = null, attempt = 0, delayMs = DEFAULT_DELAY_MS }) {
20
+ if (!templateSid) throw new Error('templateSid is required');
21
+ if (!messageId) throw new Error('messageId is required');
22
+
23
+ const jobId = this._buildJobId(templateSid, attempt);
24
+
25
+ const enqueuedJobId = await this._enqueue(
26
+ { templateSid, messageId: String(messageId), originalMessageSid, attempt },
27
+ { ...DEFAULT_JOB_OPTIONS, delay: delayMs, jobId }
28
+ );
29
+
30
+ logger.info('[TemplateApprovalJob] Enqueued', { templateSid, messageId, attempt, jobId: enqueuedJobId, delayMs });
31
+
32
+ return { jobId: enqueuedJobId };
33
+ }
34
+
35
+ _buildJobId(templateSid, attempt) {
36
+ return `template-approval-${templateSid}-${attempt}`;
37
+ }
38
+
39
+ async _process(data) {
40
+ logger.warn('[TemplateApprovalJob] Processor stub invoked; real implementation pending', data);
41
+ return { success: false, reason: 'not_implemented' };
42
+ }
43
+ }
44
+
45
+ module.exports = { TemplateApprovalJob };
@@ -27,6 +27,7 @@ const conversationRouteDefinitions = {
27
27
  'POST /:phoneNumber/read': 'markMessagesAsReadController',
28
28
  'POST /case-documentation': 'caseDocumentationController',
29
29
  'POST /bug': 'reportBugController',
30
+ 'PATCH /bug/:recordId': 'updateBugController',
30
31
  'POST /interaction': 'addInteractionController',
31
32
  'PUT /review/all': 'updateAllReviewStatusController',
32
33
  'PUT /review/:code': 'updateReviewStatusController'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.14.2",
3
+ "version": "3.15.2",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",