@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 +0 -15
- package/lib/adapters/TwilioProvider.js +4 -2
- package/lib/controllers/bugReportController.js +22 -1
- package/lib/controllers/messageController.js +15 -34
- package/lib/jobs/BaseJob.js +29 -0
- package/lib/jobs/ScheduledMessageJob.js +56 -0
- package/lib/jobs/TemplateApprovalJob.js +45 -0
- package/lib/routes/index.js +1 -0
- package/package.json +1 -1
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
|
-
|
|
99
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
17
|
+
if (!ScheduledMessage) {
|
|
36
18
|
res.status(500).json({ success: false, error: 'ScheduledMessage model not configured' });
|
|
37
19
|
return false;
|
|
38
20
|
}
|
|
39
|
-
if (!
|
|
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 (
|
|
25
|
+
if (!getDefaultInstance()) {
|
|
44
26
|
res.status(500).json({ success: false, error: 'NexusMessaging not initialized' });
|
|
45
27
|
return false;
|
|
46
28
|
}
|
|
47
|
-
if (requireAirtable && !
|
|
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
|
|
41
|
+
const saved = await ScheduledMessage.create(payload);
|
|
60
42
|
|
|
61
|
-
const result =
|
|
62
|
-
? await
|
|
63
|
-
: await
|
|
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
|
|
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 (!
|
|
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
|
|
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 };
|
package/lib/routes/index.js
CHANGED
|
@@ -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'
|