@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
- const result = await this.provider.sendMessage(messageData);
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 handle24HourWindowError(message, messageSid) {
9
+ async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messageSid = null } = {}) {
9
10
  try {
10
- if (!message?.body || !message?.numero || !message?._id) return;
11
+ if (!messageDoc?.body || !messageDoc?.numero || !messageDoc?._id) return;
11
12
 
12
- const messageDocId = message._id;
13
+ const messageDocId = messageDoc._id;
14
+ const logCtx = { source, messageSid, messageDocId: String(messageDocId) };
13
15
 
14
- if (message?.statusInfo?.recoveryTemplateSid || message?.statusInfo?.recoverySentAt) {
15
- logger.info('[TemplateRecovery] Recovery already completed or in progress', { messageSid });
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] Recovery already in progress', { messageSid });
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', { messageSid, error: claimErr.message });
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', { messageSid });
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(message.body, []);
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: message.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', { messageSid, error: setupErr.message });
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', { messageSid, templateSid: twilioContent.sid });
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
- module.exports = { handle24HourWindowError };
100
+ async function handle24HourWindowError(message, messageSid) {
101
+ return triggerTemplateRecovery(message, { source: 'reactive', messageSid });
102
+ }
103
+
104
+ module.exports = { handle24HourWindowError, triggerTemplateRecovery };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "4.1.7",
3
+ "version": "4.2.0",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",