@peopl-health/nexus 4.1.2 → 4.1.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.
@@ -121,13 +121,14 @@ class TwilioProvider extends MessageProvider {
121
121
  ? this._splitMessageAtWordBoundaries(messageParams.body) : null;
122
122
  const sends = chunks ? chunks.map(body => ({ ...messageParams, body })) : [messageParams];
123
123
 
124
- let pending = null;
125
- if (!messageData._skipStorage && this.messageStorage?.savePendingMessage) {
126
- pending = await this.messageStorage.savePendingMessage({
124
+ let parentId = messageData.parentMessageId || null;
125
+ if (!parentId && this.messageStorage?.savePendingMessage) {
126
+ const pending = await this.messageStorage.savePendingMessage({
127
127
  ...messageData, code: formattedCode, from: formattedFrom,
128
128
  provider: 'twilio', timestamp: new Date(), fromMe: true,
129
129
  processed: messageData.processed ?? false
130
130
  });
131
+ parentId = pending?._id || null;
131
132
  }
132
133
 
133
134
  let result;
@@ -136,18 +137,18 @@ class TwilioProvider extends MessageProvider {
136
137
  result = await this.twilioClient.messages.create(sends[i]);
137
138
  } catch (twilioErr) {
138
139
  await recordDeliveryAttempt({
139
- messageData, messageId: pending?._id, kind, body: sends[i].body || null,
140
+ messageData, messageId: parentId, kind, body: sends[i].body || null,
140
141
  errorSource: 'twilio_sync',
141
142
  errorCode: twilioErr.code, errorMessage: twilioErr.message
142
143
  });
143
144
  throw twilioErr;
144
145
  }
145
- await recordDeliveryAttempt({ messageData, messageId: pending?._id, twilioResult: result, kind, body: sends[i].body || null });
146
+ await recordDeliveryAttempt({ messageData, messageId: parentId, twilioResult: result, kind, body: sends[i].body || null });
146
147
  if (i < sends.length - 1) await new Promise(r => setTimeout(r, 100));
147
148
  }
148
149
 
149
- if (pending) {
150
- await this.messageStorage.finalizePendingMessage(pending._id, chunks ? null : result.sid, {
150
+ if (parentId && this.messageStorage?.finalizePendingMessage) {
151
+ await this.messageStorage.finalizePendingMessage(parentId, chunks ? null : result.sid, {
151
152
  status: result.status?.toLowerCase() || null,
152
153
  updatedAt: new Date()
153
154
  });
@@ -82,7 +82,8 @@ class NexusMessaging {
82
82
 
83
83
  this.scheduledMessageJob = new ScheduledMessageJob({
84
84
  queueAdapter: this.queueAdapter,
85
- sendMessage: this.sendMessage.bind(this)
85
+ sendMessage: this.sendMessage.bind(this),
86
+ requireMessageStorage: () => this.messageStorage
86
87
  });
87
88
 
88
89
  this.templateApprovalJob = new TemplateApprovalJob({
@@ -12,7 +12,6 @@ async function recordDeliveryAttempt({
12
12
 
13
13
  let targetId = messageId;
14
14
  if (!targetId) {
15
- if (messageData?._skipStorage) return null;
16
15
  if (!sid) return null;
17
16
  const msgDoc = await Message.findOne({ message_id: sid }, '_id').lean();
18
17
  if (!msgDoc) {
@@ -1,4 +1,5 @@
1
1
  const { logger } = require('../utils/logger');
2
+ const { isRetryableError } = require('../utils/retryUtils');
2
3
  const { ScheduledMessage } = require('../models/agendaMessageModel');
3
4
  const { BaseJob } = require('./BaseJob');
4
5
 
@@ -15,12 +16,13 @@ const TERMINAL_STATUSES = ['sent', 'cancelled'];
15
16
  const STALE_SENDING_MS = 5 * 60 * 1000;
16
17
 
17
18
  class ScheduledMessageJob extends BaseJob {
18
- constructor({ queueAdapter, sendMessage } = {}) {
19
+ constructor({ queueAdapter, sendMessage, requireMessageStorage = null } = {}) {
19
20
  super({ queueAdapter, queueName: QUEUE_NAME });
20
21
  if (typeof sendMessage !== 'function') {
21
22
  throw new Error('ScheduledMessageJob requires a sendMessage function');
22
23
  }
23
24
  this.sendMessage = sendMessage;
25
+ this.requireMessageStorage = requireMessageStorage;
24
26
  }
25
27
 
26
28
  async schedule({ scheduledMessageId, sendTime }) {
@@ -90,6 +92,23 @@ class ScheduledMessageJob extends BaseJob {
90
92
  });
91
93
  }
92
94
 
95
+ let parentMessageId = msg.parentMessageId || null;
96
+ if (!parentMessageId) {
97
+ const storage = this.requireMessageStorage?.();
98
+ if (storage?.savePendingMessage) {
99
+ const parent = await storage.savePendingMessage({
100
+ code: msg.code, body: msg.message, fileUrl: msg.fileUrl, fileType: msg.fileType,
101
+ contentSid: msg.contentSid, variables: msg.variables,
102
+ fromMe: true, processed: true,
103
+ frontendId: msg.frontendId, triggeredBy: msg.triggeredBy
104
+ });
105
+ parentMessageId = parent?._id || null;
106
+ if (parentMessageId) {
107
+ await ScheduledMessage.updateOne({ _id: scheduledMessageId }, { $set: { parentMessageId } });
108
+ }
109
+ }
110
+ }
111
+
93
112
  try {
94
113
  const result = await this.sendMessage({
95
114
  code: msg.code,
@@ -98,7 +117,8 @@ class ScheduledMessageJob extends BaseJob {
98
117
  fileType: msg.fileType,
99
118
  contentSid: msg.contentSid,
100
119
  variables: msg.variables,
101
- hidePreview: msg.hidePreview
120
+ hidePreview: msg.hidePreview,
121
+ parentMessageId
102
122
  });
103
123
 
104
124
  const messageId = result?.messageId || result?.sid || null;
@@ -112,21 +132,23 @@ class ScheduledMessageJob extends BaseJob {
112
132
 
113
133
  return { success: true, messageId };
114
134
  } catch (error) {
135
+ const retryable = isRetryableError(error);
115
136
  await ScheduledMessage.updateOne(
116
137
  { _id: scheduledMessageId },
117
138
  {
118
139
  $set: {
119
- status: 'failed',
120
- failedAt: new Date(),
140
+ status: retryable ? 'pending' : 'failed',
141
+ failedAt: retryable ? null : new Date(),
121
142
  errorCode: error?.code || null,
122
143
  errorMessage: error?.message || null
123
144
  }
124
145
  }
125
146
  );
126
147
 
127
- logger.error('[ScheduledMessageJob] Send failed', { scheduledMessageId, error: error.message });
148
+ logger.error('[ScheduledMessageJob] Send failed', { scheduledMessageId, retryable, error: error.message });
128
149
 
129
- throw error;
150
+ if (retryable) throw error;
151
+ return { success: false, reason: 'non_retryable', error: error.message };
130
152
  }
131
153
  }
132
154
  }
@@ -117,7 +117,7 @@ class TemplateApprovalJob extends BaseJob {
117
117
  code: message.numero,
118
118
  contentSid: templateSid,
119
119
  variables: {},
120
- _skipStorage: true
120
+ parentMessageId: message._id
121
121
  });
122
122
  recoveryMessageId = sendResult?.messageId;
123
123
  if (!recoveryMessageId) {
@@ -17,6 +17,7 @@ const scheduledMessageSchema = new mongoose.Schema({
17
17
  frontendId: { type: String, default: null },
18
18
  flowActionPayload: { type: mongoose.Schema.Types.Mixed, default: null },
19
19
  triggeredBy: { type: String, default: null },
20
+ parentMessageId: { type: mongoose.Schema.Types.ObjectId, ref: 'Message', default: null },
20
21
  createdAt: { type: Date, default: Date.now }
21
22
  });
22
23
 
@@ -55,9 +55,18 @@ class MongoStorage {
55
55
  }
56
56
 
57
57
  async finalizePendingMessage(docId, sid, statusInfo = null) {
58
- const update = { ...(sid && { message_id: sid }), ...(statusInfo && { statusInfo }) };
59
- if (Object.keys(update).length) {
60
- await Message.updateOne({ _id: docId }, { $set: update });
58
+ const set = {};
59
+ if (statusInfo) {
60
+ for (const [k, v] of Object.entries(statusInfo)) set[`statusInfo.${k}`] = v;
61
+ }
62
+ if (Object.keys(set).length) {
63
+ await Message.updateOne({ _id: docId }, { $set: set });
64
+ }
65
+ if (sid) {
66
+ await Message.updateOne(
67
+ { _id: docId, message_id: null },
68
+ { $set: { message_id: sid } }
69
+ );
61
70
  }
62
71
  safeEmit(getStatusEventBus(), 'message:status', createEvent('message:status', {
63
72
  messageId: String(docId),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",