@peopl-health/nexus 4.1.7 → 4.2.0
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.
|
@@ -120,16 +120,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
120
120
|
const chunks = messageParams.body?.length > 1600 && !messageParams.mediaUrl && !messageParams.contentSid
|
|
121
121
|
? this._splitMessageAtWordBoundaries(messageParams.body) : null;
|
|
122
122
|
const sends = chunks ? chunks.map(body => ({ ...messageParams, body })) : [messageParams];
|
|
123
|
-
|
|
124
|
-
let parentId = messageData.parentMessageId || null;
|
|
125
|
-
if (!parentId && this.messageStorage?.savePendingMessage) {
|
|
126
|
-
const pending = await this.messageStorage.savePendingMessage({
|
|
127
|
-
...messageData, code: formattedCode, from: formattedFrom,
|
|
128
|
-
provider: 'twilio', timestamp: new Date(), fromMe: true,
|
|
129
|
-
processed: messageData.processed ?? false
|
|
130
|
-
});
|
|
131
|
-
parentId = pending?._id || null;
|
|
132
|
-
}
|
|
123
|
+
const parentId = messageData.parentMessageId || null;
|
|
133
124
|
|
|
134
125
|
let result;
|
|
135
126
|
for (let i = 0; i < sends.length; i++) {
|
|
@@ -147,19 +138,13 @@ class TwilioProvider extends MessageProvider {
|
|
|
147
138
|
if (i < sends.length - 1) await new Promise(r => setTimeout(r, 100));
|
|
148
139
|
}
|
|
149
140
|
|
|
150
|
-
if (parentId && this.messageStorage?.finalizePendingMessage) {
|
|
151
|
-
await this.messageStorage.finalizePendingMessage(parentId, chunks ? null : result.sid, {
|
|
152
|
-
status: result.status?.toLowerCase() || null,
|
|
153
|
-
updatedAt: new Date()
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
141
|
return {
|
|
158
142
|
success: true,
|
|
159
143
|
messageId: result.sid,
|
|
160
144
|
provider: 'twilio',
|
|
161
145
|
status: result.status,
|
|
162
|
-
result
|
|
146
|
+
result,
|
|
147
|
+
finalize: { sid: chunks ? null : result.sid, status: result.status?.toLowerCase() || null }
|
|
163
148
|
};
|
|
164
149
|
}
|
|
165
150
|
|
|
@@ -11,6 +11,8 @@ const { Thread } = require('../models/threadModel');
|
|
|
11
11
|
|
|
12
12
|
const { ensureThreadExists } = require('../helpers/threadHelper');
|
|
13
13
|
const { storeRunMetrics } = require('../helpers/metricsHelper');
|
|
14
|
+
const { isWithin24HourWindow } = require('../helpers/conversationWindowHelper');
|
|
15
|
+
const { triggerTemplateRecovery } = require('../helpers/templateRecoveryHelper');
|
|
14
16
|
|
|
15
17
|
const { createMessagingProvider } = require('../adapters/registry');
|
|
16
18
|
|
|
@@ -235,17 +237,43 @@ class NexusMessaging {
|
|
|
235
237
|
|
|
236
238
|
if (messageData._fromConversationReply) messageData.processed = true;
|
|
237
239
|
|
|
238
|
-
|
|
240
|
+
if (this._needsTemplateRoute(messageData) && !(await isWithin24HourWindow(messageData.code))) {
|
|
241
|
+
const parent = await this.messageStorage.savePendingMessage({
|
|
242
|
+
...messageData,
|
|
243
|
+
provider: 'twilio', timestamp: new Date(), fromMe: true,
|
|
244
|
+
processed: messageData.processed ?? false
|
|
245
|
+
});
|
|
246
|
+
await triggerTemplateRecovery(parent, { source: 'preemptive' });
|
|
247
|
+
return { success: true, messageId: String(parent._id), status: 'queued', preemptive: true };
|
|
248
|
+
}
|
|
249
|
+
|
|
239
250
|
const providerStores = this.provider.supportsMessageStorage?.() ?? false;
|
|
251
|
+
let parentId = messageData.parentMessageId || null;
|
|
252
|
+
if (!parentId && providerStores && this.messageStorage?.savePendingMessage) {
|
|
253
|
+
const pending = await this.messageStorage.savePendingMessage({
|
|
254
|
+
...messageData,
|
|
255
|
+
provider: 'twilio',
|
|
256
|
+
timestamp: new Date(), fromMe: true,
|
|
257
|
+
processed: messageData.processed ?? false
|
|
258
|
+
});
|
|
259
|
+
parentId = pending?._id || null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const result = await this.provider.sendMessage({ ...messageData, parentMessageId: parentId });
|
|
263
|
+
|
|
264
|
+
if (parentId && result?.finalize && this.messageStorage?.finalizePendingMessage) {
|
|
265
|
+
await this.messageStorage.finalizePendingMessage(parentId, result.finalize.sid, {
|
|
266
|
+
status: result.finalize.status,
|
|
267
|
+
updatedAt: new Date()
|
|
268
|
+
});
|
|
269
|
+
}
|
|
240
270
|
|
|
241
271
|
if (this.messageStorage && !providerStores) {
|
|
242
272
|
await this.messageStorage.saveMessage({
|
|
243
273
|
...messageData,
|
|
244
274
|
messageId: result.messageId,
|
|
245
275
|
provider: result.provider,
|
|
246
|
-
timestamp: new Date(),
|
|
247
|
-
fromMe: true,
|
|
248
|
-
processed: true
|
|
276
|
+
timestamp: new Date(), fromMe: true, processed: true
|
|
249
277
|
});
|
|
250
278
|
}
|
|
251
279
|
|
|
@@ -274,6 +302,14 @@ class NexusMessaging {
|
|
|
274
302
|
return result;
|
|
275
303
|
}
|
|
276
304
|
|
|
305
|
+
_needsTemplateRoute(messageData) {
|
|
306
|
+
if (this.provider?.supportsMessageStorage?.() !== true) return false;
|
|
307
|
+
if (messageData.contentSid) return false;
|
|
308
|
+
if (messageData.parentMessageId) return false;
|
|
309
|
+
if (!messageData.code) return false;
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
277
313
|
async sendScheduledMessage(scheduledMessage) {
|
|
278
314
|
if (!scheduledMessage?._id) {
|
|
279
315
|
throw new Error('sendScheduledMessage requires a persisted ScheduledMessage with _id');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { Message } = require('../models/messageModel');
|
|
2
|
+
|
|
3
|
+
const WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
async function isWithin24HourWindow(code) {
|
|
6
|
+
if (!code) return false;
|
|
7
|
+
const cutoff = new Date(Date.now() - WINDOW_MS);
|
|
8
|
+
const recent = await Message.findOne(
|
|
9
|
+
{ numero: code, from_me: false, createdAt: { $gte: cutoff } },
|
|
10
|
+
'_id'
|
|
11
|
+
).lean();
|
|
12
|
+
return !!recent;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { isWithin24HourWindow };
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
const { logger } = require('../utils/logger');
|
|
2
2
|
const { Message } = require('../models/messageModel');
|
|
3
|
+
const TemplateModel = require('../models/templateModel');
|
|
3
4
|
const { Template } = require('../templates/templateStructure');
|
|
4
5
|
const { recordDeliveryAttempt } = require('./deliveryAttemptHelper');
|
|
5
6
|
|
|
6
7
|
const getMessaging = () => require('../core/NexusMessaging');
|
|
7
8
|
|
|
8
|
-
async function
|
|
9
|
+
async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messageSid = null } = {}) {
|
|
9
10
|
try {
|
|
10
|
-
if (!
|
|
11
|
+
if (!messageDoc?.body || !messageDoc?.numero || !messageDoc?._id) return;
|
|
11
12
|
|
|
12
|
-
const messageDocId =
|
|
13
|
+
const messageDocId = messageDoc._id;
|
|
14
|
+
const logCtx = { source, messageSid, messageDocId: String(messageDocId) };
|
|
13
15
|
|
|
14
|
-
if (
|
|
15
|
-
logger.info('[TemplateRecovery]
|
|
16
|
+
if (messageDoc?.statusInfo?.recoveryTemplateSid || messageDoc?.statusInfo?.recoverySentAt) {
|
|
17
|
+
logger.info('[TemplateRecovery] Already completed or in progress', logCtx);
|
|
16
18
|
return;
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -22,24 +24,24 @@ async function handle24HourWindowError(message, messageSid) {
|
|
|
22
24
|
{ $set: { 'statusInfo.recoveryStartedAt': new Date() } }
|
|
23
25
|
);
|
|
24
26
|
if (!claim.modifiedCount && !claim.nModified) {
|
|
25
|
-
logger.info('[TemplateRecovery]
|
|
27
|
+
logger.info('[TemplateRecovery] Already in progress', logCtx);
|
|
26
28
|
return;
|
|
27
29
|
}
|
|
28
30
|
} catch (claimErr) {
|
|
29
|
-
logger.warn('[TemplateRecovery] Could not set recovery flag', {
|
|
31
|
+
logger.warn('[TemplateRecovery] Could not set recovery flag', { ...logCtx, error: claimErr.message });
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const { requireProvider, scheduleTemplateApproval } = getMessaging();
|
|
34
36
|
const provider = requireProvider();
|
|
35
37
|
if (typeof provider.createTemplate !== 'function') {
|
|
36
|
-
logger.warn('[TemplateRecovery] Provider does not support createTemplate, skipping',
|
|
38
|
+
logger.warn('[TemplateRecovery] Provider does not support createTemplate, skipping', logCtx);
|
|
37
39
|
return;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const templateName = `auto_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
|
41
43
|
const template = new Template(templateName, 'UTILITY', 'es');
|
|
42
|
-
template.setBody(
|
|
44
|
+
template.setBody(messageDoc.body, []);
|
|
43
45
|
|
|
44
46
|
let twilioContent;
|
|
45
47
|
let setupErr = null;
|
|
@@ -54,22 +56,36 @@ async function handle24HourWindowError(message, messageSid) {
|
|
|
54
56
|
await recordDeliveryAttempt({
|
|
55
57
|
messageId: messageDocId,
|
|
56
58
|
kind: 'recovery_template_setup',
|
|
57
|
-
body:
|
|
59
|
+
body: messageDoc.body,
|
|
58
60
|
twilioResult: twilioContent?.sid ? { sid: twilioContent.sid } : null,
|
|
59
61
|
...(setupErr && { errorSource: 'server', errorMessage: setupErr.message })
|
|
60
62
|
});
|
|
61
63
|
|
|
62
64
|
if (setupErr) {
|
|
63
|
-
logger.error('[TemplateRecovery] Setup failed', {
|
|
65
|
+
logger.error('[TemplateRecovery] Setup failed', { ...logCtx, error: setupErr.message });
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
await TemplateModel.create({
|
|
70
|
+
sid: twilioContent.sid,
|
|
71
|
+
name: templateName,
|
|
72
|
+
friendlyName: twilioContent.friendlyName || templateName,
|
|
73
|
+
category: 'UTILITY',
|
|
74
|
+
language: 'es',
|
|
75
|
+
status: 'PENDING',
|
|
76
|
+
body: messageDoc.body,
|
|
77
|
+
variables: [],
|
|
78
|
+
components: twilioContent.components || [],
|
|
79
|
+
dateCreated: new Date(),
|
|
80
|
+
lastUpdated: new Date()
|
|
81
|
+
}).catch(err => logger.warn('[TemplateRecovery] TemplateModel.create failed (non-fatal)', { ...logCtx, error: err.message }));
|
|
82
|
+
|
|
67
83
|
await Message.updateOne(
|
|
68
84
|
{ _id: messageDocId },
|
|
69
85
|
{ $set: { 'statusInfo.recoveryTemplateSid': twilioContent.sid } }
|
|
70
86
|
);
|
|
71
87
|
|
|
72
|
-
logger.info('[TemplateRecovery] Template created', {
|
|
88
|
+
logger.info('[TemplateRecovery] Template created', { ...logCtx, templateSid: twilioContent.sid });
|
|
73
89
|
|
|
74
90
|
await scheduleTemplateApproval({
|
|
75
91
|
templateSid: twilioContent.sid,
|
|
@@ -77,8 +93,12 @@ async function handle24HourWindowError(message, messageSid) {
|
|
|
77
93
|
originalMessageSid: messageSid
|
|
78
94
|
});
|
|
79
95
|
} catch (error) {
|
|
80
|
-
logger.error('[TemplateRecovery] Error', { messageSid, error: error.message });
|
|
96
|
+
logger.error('[TemplateRecovery] Error', { source, messageSid, error: error.message });
|
|
81
97
|
}
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
|
|
100
|
+
async function handle24HourWindowError(message, messageSid) {
|
|
101
|
+
return triggerTemplateRecovery(message, { source: 'reactive', messageSid });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { handle24HourWindowError, triggerTemplateRecovery };
|