@peopl-health/nexus 5.11.0-dev.1115 → 5.11.0-dev.1117

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.
@@ -255,7 +255,7 @@ class NexusMessaging {
255
255
  },
256
256
  saveFn,
257
257
  async ({ body, _id }) => {
258
- await triggerTemplateRecovery({ _id, body, numero: messageData.code }, { source: 'preemptive' });
258
+ await triggerTemplateRecovery({ _id, body, numero: messageData.code, media: messageData.media }, { source: 'preemptive' });
259
259
  return { success: true, messageId: String(_id), status: 'queued', preemptive: true, delivered: false, deferred: true };
260
260
  },
261
261
  { onSaved: messageData.onParentMessageId },
@@ -6,7 +6,7 @@ const { createEvent, safeEmit } = require('../utils/eventUtils');
6
6
  const { Message } = require('../models/messageModel');
7
7
  const { DeliveryAttempt, DELIVERY_ATTEMPT_TERMINAL_STATUSES } = require('../models/deliveryAttemptModel');
8
8
 
9
- const { handle24HourWindowError } = require('../helpers/templateRecoveryHelper');
9
+ const { handle24HourWindowError, cleanupRecoveryTemplate } = require('../helpers/templateRecoveryHelper');
10
10
  const { updateDeliveryAttemptByTwilioSid } = require('../helpers/deliveryAttemptHelper');
11
11
 
12
12
  const { addLinkedRecord, updateRecordByFilter } = require('../services/airtableService');
@@ -105,11 +105,19 @@ async function handleStatusCallback(twilioStatusData, { eventBus } = {}) {
105
105
  }
106
106
 
107
107
  if ((ErrorCode === 63016 || ErrorCode === '63016') && updated) {
108
- handle24HourWindowError(updated, MessageSid).catch(err =>
108
+ handle24HourWindowError(updated, MessageSid).catch(err =>
109
109
  logger.error('[MessageStatus] Recovery error', { messageSid: MessageSid, error: err.message })
110
110
  );
111
111
  }
112
112
 
113
+ const deliveryStatus = MessageStatus.toLowerCase();
114
+ if (updated?.statusInfo?.recoverySentAt && updated?.statusInfo?.recoveryTemplateSid &&
115
+ (deliveryStatus === 'sent' || deliveryStatus === 'delivered' || deliveryStatus === 'read')) {
116
+ cleanupRecoveryTemplate(updated).catch(err =>
117
+ logger.warn('[MessageStatus] Recovery template cleanup failed', { messageSid: MessageSid, error: err.message })
118
+ );
119
+ }
120
+
113
121
  return updated;
114
122
  }
115
123
 
@@ -1,3 +1,4 @@
1
+ const { generatePresignedUrl } = require('../config/awsConfig');
1
2
  const { logger } = require('../utils/logger');
2
3
  const { Message } = require('../models/messageModel');
3
4
  const TemplateModel = require('../models/templateModel');
@@ -7,6 +8,17 @@ const { recordDeliveryAttempt } = require('./deliveryAttemptHelper');
7
8
  const getMessaging = () => require('../core/NexusMessaging');
8
9
 
9
10
  const TEMPLATE_BODY_LIMIT = 1024;
11
+ const RECOVERY_MEDIA_URL_TTL_SECONDS = 7 * 24 * 60 * 60;
12
+
13
+ async function resolveRecoveryMediaUrl(media) {
14
+ if (!media?.bucketName || !media?.key) return null;
15
+ try {
16
+ return await generatePresignedUrl(media.bucketName, media.key, RECOVERY_MEDIA_URL_TTL_SECONDS);
17
+ } catch (error) {
18
+ logger.warn('[TemplateRecovery] Could not presign media; sending text-only', { error: error.message });
19
+ return null;
20
+ }
21
+ }
10
22
 
11
23
  async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messageSid = null } = {}) {
12
24
  try {
@@ -45,6 +57,9 @@ async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messag
45
57
  const template = new Template(templateName, 'UTILITY', 'es');
46
58
  template.setBody(messageDoc.body, []);
47
59
 
60
+ const mediaUrl = await resolveRecoveryMediaUrl(messageDoc.media);
61
+ if (mediaUrl) template.setMedia(mediaUrl);
62
+
48
63
  let twilioContent;
49
64
  let setupErr = null;
50
65
  if (messageDoc.body.length > TEMPLATE_BODY_LIMIT) {
@@ -96,7 +111,7 @@ async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messag
96
111
  { $set: { 'statusInfo.recoveryTemplateSid': twilioContent.sid } }
97
112
  );
98
113
 
99
- logger.info('[TemplateRecovery] Template created', { ...logCtx, templateSid: twilioContent.sid });
114
+ logger.info('[TemplateRecovery] Template created', { ...logCtx, templateSid: twilioContent.sid, hasMedia: !!mediaUrl });
100
115
 
101
116
  await scheduleTemplateApproval({
102
117
  templateSid: twilioContent.sid,
@@ -114,4 +129,23 @@ async function handle24HourWindowError(message, messageSid) {
114
129
  return triggerTemplateRecovery(message, { source: 'reactive', messageSid });
115
130
  }
116
131
 
117
- module.exports = { handle24HourWindowError, triggerTemplateRecovery };
132
+ async function cleanupRecoveryTemplate(messageDoc) {
133
+ const templateSid = messageDoc?.statusInfo?.recoveryTemplateSid;
134
+ if (!templateSid || !messageDoc?._id) return;
135
+
136
+ const claim = await Message.updateOne(
137
+ { _id: messageDoc._id, 'statusInfo.recoveryTemplateSid': templateSid },
138
+ { $unset: { 'statusInfo.recoveryTemplateSid': '' } }
139
+ );
140
+ if (!claim.modifiedCount && !claim.nModified) return;
141
+
142
+ try {
143
+ const provider = getMessaging().requireProvider();
144
+ await provider.deleteTemplate(templateSid);
145
+ logger.info('[TemplateRecovery] Template deleted after delivery', { messageDocId: String(messageDoc._id), templateSid });
146
+ } catch (error) {
147
+ logger.warn('[TemplateRecovery] Failed to delete template after delivery', { templateSid, error: error.message });
148
+ }
149
+ }
150
+
151
+ module.exports = { handle24HourWindowError, triggerTemplateRecovery, cleanupRecoveryTemplate };
@@ -84,7 +84,7 @@ class TemplateApprovalJob extends BaseJob {
84
84
  logger.info('[TemplateApprovalJob] Approval status', { templateSid, approvalStatus, attempt, messageId, originalMessageSid });
85
85
 
86
86
  if (approvalStatus === 'APPROVED') {
87
- return await this._handleApproved({ provider, message, templateSid, originalMessageSid });
87
+ return await this._handleApproved({ message, templateSid, originalMessageSid });
88
88
  }
89
89
  if (approvalStatus === 'REJECTED') {
90
90
  return await this._handleRejected({ provider, message, templateSid });
@@ -109,7 +109,7 @@ class TemplateApprovalJob extends BaseJob {
109
109
  return { success: false, reason, nextAttempt };
110
110
  }
111
111
 
112
- async _handleApproved({ provider, message, templateSid, originalMessageSid }) {
112
+ async _handleApproved({ message, templateSid, originalMessageSid }) {
113
113
  const claim = await Message.updateOne(
114
114
  { _id: message._id, 'statusInfo.recoverySentAt': null },
115
115
  { $set: { 'statusInfo.recoverySentAt': new Date() } }
@@ -148,10 +148,6 @@ class TemplateApprovalJob extends BaseJob {
148
148
  { $set: { status: 'sent', completedAt: new Date() } }
149
149
  );
150
150
 
151
- provider.deleteTemplate(templateSid).catch((deleteErr) =>
152
- logger.warn('[TemplateApprovalJob] Failed to delete template after send', { templateSid, error: deleteErr.message })
153
- );
154
-
155
151
  return { success: true, recoveryMessageId: sendResult.messageId };
156
152
  }
157
153
 
@@ -14,7 +14,8 @@ class Template {
14
14
  buttons: []
15
15
  };
16
16
  this.variables = [];
17
-
17
+ this.media = null;
18
+
18
19
  const uniqueId = `${Date.now()}_${uuidv4().substring(0, 6)}`;
19
20
  this.friendlyName = `${name}_${uniqueId}`;
20
21
  this.templateName = this.friendlyName;
@@ -45,6 +46,11 @@ class Template {
45
46
  return this;
46
47
  }
47
48
 
49
+ setMedia(url) {
50
+ this.media = url;
51
+ return this;
52
+ }
53
+
48
54
  addQuickReply(text) {
49
55
  if (this.components.buttons.length >= 3) {
50
56
  throw new Error('Maximum of 3 buttons allowed');
@@ -116,7 +122,14 @@ class Template {
116
122
  if (this.category) {
117
123
  template.categories = [this.category];
118
124
  }
119
-
125
+
126
+ if (this.media) {
127
+ template.types['twilio/media'] = {
128
+ body: this.components.body[0].text,
129
+ media: [this.media]
130
+ };
131
+ }
132
+
120
133
  if (this.components.buttons && this.components.buttons.length > 0) {
121
134
  const actions = this.components.buttons.map((button, index) => {
122
135
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.11.0-dev.1115",
3
+ "version": "5.11.0-dev.1117",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",