@peopl-health/nexus 1.3.2 → 1.4.1

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 CHANGED
@@ -24,8 +24,23 @@ await nexus.initialize({
24
24
  authToken: process.env.TWILIO_AUTH_TOKEN,
25
25
  phoneNumber: process.env.TWILIO_PHONE_NUMBER
26
26
  },
27
- storage: 'mongo', // or 'noop', or your custom adapter/instance
28
- storageConfig: { mongoUri: process.env.MONGODB_URI }
27
+ // Storage (MongoStorage) and Mongo convenience
28
+ storage: 'mongo',
29
+ storageConfig: { dbName: 'nexus' }, // other options
30
+ mongoUri: process.env.MONGODB_URI, // convenience: passed into storageConfig.mongoUri
31
+
32
+ // Media convenience (inject only bucket name)
33
+ media: { bucketName: process.env.AWS_S3_BUCKET_NAME },
34
+
35
+ // Airtable convenience (pick default base or pass a Base ID)
36
+ airtable: {
37
+ base: 'calendar', // friendly key or base ID
38
+ apiKey: process.env.AIRTABLE_API_KEY
39
+ },
40
+
41
+ // Optional LLM (OpenAI)
42
+ llm: 'openai',
43
+ llmConfig: { apiKey: process.env.OPENAI_API_KEY }
29
44
  });
30
45
 
31
46
  // Built‑in routes (assistant, conversation, media, message, template)
@@ -130,3 +145,64 @@ See:
130
145
  - examples/basic-usage.js
131
146
  - examples/assistants/
132
147
 
148
+
149
+
150
+ ## Configuration
151
+
152
+ You can configure Nexus via environment variables or at runtime using simple injection helpers. For production apps, prefer passing options or DI, and use envs as a fallback.
153
+
154
+ - Providers/AI
155
+ - TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER
156
+ - OPENAI_API_KEY
157
+
158
+ - Mongo
159
+ - MONGODB_URI (or pass `mongoUri` when calling `initializeMongoDB()`)
160
+
161
+ - Airtable
162
+ - AIRTABLE_API_KEY
163
+ - AIRTABLE_BASE_ID (or specific IDs below)
164
+ - AIRTABLE_CALENDAR_ID, AIRTABLE_CONFIG_ID, AIRTABLE_HISTORIAL_CLINICO_ID
165
+ - AIRTABLE_LOGGING_ID, AIRTABLE_MONITOREO_ID, AIRTABLE_PROGRAMA_JUNTAS_ID
166
+ - AIRTABLE_SYMPTOMS_ID, AIRTABLE_WEBINARS_LEADS_ID
167
+
168
+ - AWS (S3)
169
+ - AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION (default: us-east-1)
170
+ - AWS_S3_BUCKET_NAME (or inject via `configureMediaController`)
171
+
172
+ - Misc
173
+ - NODE_ENV (affects logging and helpers)
174
+ - USER_DB_MONGO (used as author in message controller)
175
+
176
+ Injection points (dependency injection)
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
+ - Media (inject only your bucket name; AWS SDK is loaded by the lib):
194
+ ```js
195
+ const { configureMediaController } = require('@peopl-health/nexus/lib/controllers/mediaController');
196
+ configureMediaController({ bucketName: process.env.AWS_S3_BUCKET_NAME });
197
+ ```
198
+
199
+ - Storage settings (MongoStorage only):
200
+ ```js
201
+ // Store once; Nexus auto-injects media bucket from storage at startup
202
+ await nexus.getStorage().setConfig('media.bucketName', process.env.AWS_S3_BUCKET_NAME);
203
+ ```
204
+
205
+ Tips
206
+ - Connect Mongo before `app.listen()` to avoid Mongoose buffering timeouts.
207
+ - If you initialize OpenAI, pass `llm: 'openai'` and `llmConfig: { apiKey }` to `nexus.initialize`.
208
+ - Use the event bus + middleware for custom routing and transformations without forking the default handlers.
@@ -161,7 +161,7 @@ class TwilioProvider extends MessageProvider {
161
161
  const links = (content && content.links) || {};
162
162
  let approvalsUrl = links.approvals || links.approvalRequests || links.approval_requests;
163
163
  if (!approvalsUrl) {
164
- approvalsUrl = 'https://content.twilio.com/v1/Content/' + sid + '/ApprovalRequests';
164
+ return { content, approvalRequest: null, warning: 'Approval endpoint not provided by Twilio' };
165
165
  }
166
166
  const resp = await axios.get(approvalsUrl, { auth: { username: this.accountSid, password: this.authToken } });
167
167
  const data = resp && resp.data ? resp.data : null;
@@ -188,7 +188,11 @@ class TwilioProvider extends MessageProvider {
188
188
  const links = (content && content.links) || {};
189
189
  let approvalsUrl = links.approvals || links.approvalRequests || links.approval_requests;
190
190
  if (!approvalsUrl) {
191
- approvalsUrl = 'https://content.twilio.com/v1/Content/' + contentSid + '/ApprovalRequests';
191
+ return {
192
+ success: false,
193
+ contentSid,
194
+ warning: 'Twilio account does not expose an approvals endpoint. Approval must be handled manually in the Console.'
195
+ };
192
196
  }
193
197
  const payload = {
194
198
  name,
@@ -200,7 +204,11 @@ class TwilioProvider extends MessageProvider {
200
204
  const resp = await axios.post(approvalsUrl, payload, { auth: { username: this.accountSid, password: this.authToken } });
201
205
  return { success: true, contentSid, approvalRequest: resp.data || null };
202
206
  } catch (error) {
203
- throw new Error(`Failed to submit for approval: ${error.message}`);
207
+ return {
208
+ success: false,
209
+ contentSid,
210
+ warning: `Failed to submit for approval via API: ${error.message}`
211
+ };
204
212
  }
205
213
  }
206
214
 
@@ -1,18 +1,19 @@
1
1
  const Airtable = require('airtable');
2
+ const runtimeConfig = require('./runtimeConfig');
2
3
 
3
4
  const airtableConfig = {
4
- apiKey: process.env.AIRTABLE_API_KEY,
5
+ apiKey: runtimeConfig.get('AIRTABLE_API_KEY'),
5
6
  };
6
7
 
7
8
  // Configurable base IDs - users can override via environment variables
8
- const Calendar_ID = process.env.AIRTABLE_CALENDAR_ID || 'appIjEstWR6972tbF';
9
- const Config_ID = process.env.AIRTABLE_CONFIG_ID || 'app9K4EvGI8McC8jF';
10
- const Historial_Clinico_ID = process.env.AIRTABLE_HISTORIAL_CLINICO_ID || 'appdUpGUS06XIzVnY';
11
- const Logging_ID = process.env.AIRTABLE_LOGGING_ID || 'appQ7YhzfebRDbSPJ';
12
- const Monitoreo_ID = process.env.AIRTABLE_MONITOREO_ID || 'appdvraKSdp0XVn5n';
13
- const Programa_Juntas_ID = process.env.AIRTABLE_PROGRAMA_JUNTAS_ID || 'appKFWzkcDEWlrXBE';
14
- const Symptoms_ID = process.env.AIRTABLE_SYMPTOMS_ID || 'appQRhZlQ9tMfYZWJ';
15
- const Webinars_Leads_ID = process.env.AIRTABLE_WEBINARS_LEADS_ID || 'appzjpVXTI0TgqGPq';
9
+ const Calendar_ID = require('./runtimeConfig').get('AIRTABLE_CALENDAR_ID') || 'appIjEstWR6972tbF';
10
+ const Config_ID = require('./runtimeConfig').get('AIRTABLE_CONFIG_ID') || 'app9K4EvGI8McC8jF';
11
+ const Historial_Clinico_ID = require('./runtimeConfig').get('AIRTABLE_HISTORIAL_CLINICO_ID') || 'appdUpGUS06XIzVnY';
12
+ const Logging_ID = require('./runtimeConfig').get('AIRTABLE_LOGGING_ID') || 'appQ7YhzfebRDbSPJ';
13
+ const Monitoreo_ID = require('./runtimeConfig').get('AIRTABLE_MONITOREO_ID') || 'appdvraKSdp0XVn5n';
14
+ const Programa_Juntas_ID = require('./runtimeConfig').get('AIRTABLE_PROGRAMA_JUNTAS_ID') || 'appKFWzkcDEWlrXBE';
15
+ const Symptoms_ID = require('./runtimeConfig').get('AIRTABLE_SYMPTOMS_ID') || 'appQRhZlQ9tMfYZWJ';
16
+ const Webinars_Leads_ID = require('./runtimeConfig').get('AIRTABLE_WEBINARS_LEADS_ID') || 'appzjpVXTI0TgqGPq';
16
17
 
17
18
  // Initialize Airtable only if API key is provided
18
19
  let airtable = null;
@@ -20,6 +21,17 @@ if (airtableConfig.apiKey) {
20
21
  airtable = new Airtable({ apiKey: airtableConfig.apiKey });
21
22
  }
22
23
 
24
+ const BASE_MAP = {
25
+ calendar: Calendar_ID,
26
+ config: Config_ID,
27
+ historial: Historial_Clinico_ID,
28
+ logging: Logging_ID,
29
+ monitoreo: Monitoreo_ID,
30
+ programa: Programa_Juntas_ID,
31
+ symptoms: Symptoms_ID,
32
+ webinars: Webinars_Leads_ID
33
+ };
34
+
23
35
  module.exports = {
24
36
  airtable,
25
37
  config: airtableConfig,
@@ -33,13 +45,17 @@ module.exports = {
33
45
  Webinars_Leads_ID,
34
46
 
35
47
  // Helper function to get base by ID
36
- getBase: (baseId = process.env.AIRTABLE_BASE_ID) => {
48
+ getBase: (baseKeyOrId = require('./runtimeConfig').get('AIRTABLE_BASE_ID')) => {
37
49
  if (!airtable) {
38
50
  throw new Error('Airtable not configured. Please set AIRTABLE_API_KEY environment variable.');
39
51
  }
40
- if (!baseId) {
41
- throw new Error('Airtable base ID not provided. Please set AIRTABLE_BASE_ID environment variable or pass baseId parameter.');
52
+ let resolved = baseKeyOrId;
53
+ if (resolved && BASE_MAP[String(resolved).toLowerCase()]) {
54
+ resolved = BASE_MAP[String(resolved).toLowerCase()];
55
+ }
56
+ if (!resolved) {
57
+ throw new Error('Airtable base identifier not provided. Pass a base ID or one of: ' + Object.keys(BASE_MAP).join(', ') + '.');
42
58
  }
43
- return airtable.base(baseId);
59
+ return airtable.base(resolved);
44
60
  }
45
61
  };
@@ -0,0 +1,15 @@
1
+ const overrides = new Map();
2
+
3
+ function set(key, value) {
4
+ if (!key) return;
5
+ overrides.set(String(key), value);
6
+ }
7
+
8
+ function get(key, fallback = undefined) {
9
+ const k = String(key);
10
+ if (overrides.has(k)) return overrides.get(k);
11
+ return process.env[k] !== undefined ? process.env[k] : fallback;
12
+ }
13
+
14
+ module.exports = { set, get };
15
+
@@ -1,10 +1,84 @@
1
1
  const { Message } = require('../models/messageModel.js');
2
- const { ScheduledMessage } = require('../models/agendaMessageModel.js');
3
- const { getRecordByFilter } = require('../services/airtableService.js');
4
- const { sendMessage, sendScheduledMessage } = require('../core/NexusMessaging');
5
-
2
+ const { ScheduledMessage: DefaultScheduledMessage } = require('../models/agendaMessageModel.js');
3
+ const {
4
+ sendMessage: defaultSendMessage,
5
+ sendScheduledMessage: defaultSendScheduledMessage
6
+ } = require('../core/NexusMessaging');
7
+ const { getRecordByFilter: defaultGetRecordByFilter } = require('../services/airtableService');
8
+ const runtimeConfig = require('../config/runtimeConfig');
6
9
  const moment = require('moment-timezone');
7
10
 
11
+ const dependencies = {
12
+ ScheduledMessage: DefaultScheduledMessage,
13
+ getRecordByFilter: defaultGetRecordByFilter,
14
+ sendScheduledMessage: defaultSendScheduledMessage,
15
+ sendMessage: defaultSendMessage
16
+ };
17
+
18
+ const configureMessageController = (overrides = {}) => {
19
+ if (overrides.ScheduledMessage) dependencies.ScheduledMessage = overrides.ScheduledMessage;
20
+ if (overrides.getRecordByFilter) dependencies.getRecordByFilter = overrides.getRecordByFilter;
21
+ if (overrides.sendScheduledMessage) dependencies.sendScheduledMessage = overrides.sendScheduledMessage;
22
+ if (overrides.sendMessage) dependencies.sendMessage = overrides.sendMessage;
23
+ return { ...dependencies };
24
+ };
25
+
26
+ const ensureDependency = (res, condition, errorMessage) => {
27
+ if (condition) return true;
28
+ res.status(500).json({ success: false, error: errorMessage });
29
+ return false;
30
+ };
31
+
32
+ const persistScheduledMessage = async (Model, payload) => {
33
+ if (Model && typeof Model.create === 'function') {
34
+ return await Model.create(payload);
35
+ }
36
+ if (typeof Model === 'function') {
37
+ const instance = new Model(payload);
38
+ if (typeof instance.save === 'function') {
39
+ await instance.save();
40
+ return instance;
41
+ }
42
+ }
43
+ throw new Error('ScheduledMessage model must expose create() or constructor+save()');
44
+ };
45
+
46
+ const pickMessageId = (result, fallbackDoc) => {
47
+ if (result) {
48
+ const id = result.sid || result.messageId || result.id || result._id;
49
+ if (id) return id;
50
+ }
51
+
52
+ if (fallbackDoc) {
53
+ const docId = fallbackDoc.wa_id || fallbackDoc.sid || fallbackDoc.id || fallbackDoc._id;
54
+ if (docId) return typeof docId === 'object' && docId.toString ? docId.toString() : docId;
55
+ }
56
+
57
+ return null;
58
+ };
59
+
60
+ const normalizeCode = (code) => {
61
+ if (!code || typeof code !== 'string') return code;
62
+
63
+ const trimmed = code.trim();
64
+ if (trimmed.startsWith('whatsapp:')) {
65
+ return trimmed;
66
+ }
67
+
68
+ if (trimmed.includes('@')) {
69
+ return trimmed;
70
+ }
71
+
72
+ if (trimmed.startsWith('+')) {
73
+ return `whatsapp:${trimmed}`;
74
+ }
75
+
76
+ if (/^\d+$/.test(trimmed)) {
77
+ return `whatsapp:+${trimmed}`;
78
+ }
79
+
80
+ return trimmed;
81
+ };
8
82
 
9
83
  const sendMessageController = async (req, res) => {
10
84
  const {
@@ -18,35 +92,43 @@ const sendMessageController = async (req, res) => {
18
92
  contentSid = null,
19
93
  variables = null
20
94
  } = req.body || {};
21
- const author = (require('../config/runtimeConfig').get('USER_DB_MONGO'));
95
+ const author = runtimeConfig.get('USER_DB_MONGO');
22
96
  const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 2500 : new Date();
23
97
 
98
+ const ScheduledMessageModel = dependencies.ScheduledMessage;
99
+ if (!ensureDependency(res, ScheduledMessageModel, 'ScheduledMessage model not configured. Call configureMessageController() to inject it.')) return;
100
+
101
+ const hasScheduler = typeof dependencies.sendScheduledMessage === 'function';
102
+ const hasDirectSend = typeof dependencies.sendMessage === 'function';
103
+ if (!ensureDependency(res, hasScheduler || hasDirectSend, 'No messaging provider configured. Ensure Nexus.initialize() completed before using the message controllers.')) return;
104
+
24
105
  try {
25
- const messageData = {
106
+ const payload = {
26
107
  fileUrl,
27
108
  message,
28
109
  fileType,
29
110
  timeZone: timeZone === '' ? null : timeZone,
30
111
  sendTime: sendMoment,
31
- contentSid: contentSid,
112
+ contentSid,
32
113
  hidePreview,
33
- code,
114
+ code: normalizeCode(code),
34
115
  author,
35
116
  extraDelay: 0,
36
117
  variables
37
118
  };
38
- await ScheduledMessage.create(messageData);
39
- console.log('Sending message with data:', messageData);
40
-
41
- const result = await sendMessage(messageData);
42
-
119
+
120
+ const scheduledRecord = await persistScheduledMessage(ScheduledMessageModel, payload);
121
+ const result = hasScheduler
122
+ ? await dependencies.sendScheduledMessage(scheduledRecord)
123
+ : await dependencies.sendMessage(payload);
124
+
43
125
  res.status(200).json({
44
- success: true,
45
- message: 'Message sent successfully',
46
- messageId: result?.sid || result?.id
126
+ status: 200,
127
+ response: hasScheduler ? 'Message scheduled to be sent once!' : 'Message sent successfully',
128
+ messageId: pickMessageId(result, scheduledRecord)
47
129
  });
48
130
  } catch (err) {
49
- console.error('Error scheduling individual message:', err.message);
131
+ console.error('Error scheduling individual message:', err);
50
132
  res.status(500).json({ status: false, error: err.message });
51
133
  }
52
134
  };
@@ -56,64 +138,71 @@ const sendBulkMessageController = async (req, res) => {
56
138
  fileUrl,
57
139
  message,
58
140
  fileType,
59
- codes,
141
+ codes = [],
60
142
  sendTime = new Date(),
61
143
  timeZone = 'Etc/GMT',
62
144
  hidePreview = false,
63
145
  contentSid = null,
64
146
  variables = null
65
147
  } = req.body || {};
66
- const author = (require('../config/runtimeConfig').get('USER_DB_MONGO'));
67
- const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20*1000 : new Date();
148
+ const author = runtimeConfig.get('USER_DB_MONGO');
149
+ const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20 * 1000 : new Date();
150
+
151
+ const ScheduledMessageModel = dependencies.ScheduledMessage;
152
+ if (!ensureDependency(res, ScheduledMessageModel, 'ScheduledMessage model not configured. Call configureMessageController() to inject it.')) return;
153
+
154
+ const hasScheduler = typeof dependencies.sendScheduledMessage === 'function';
155
+ const hasDirectSend = typeof dependencies.sendMessage === 'function';
156
+ if (!ensureDependency(res, hasScheduler || hasDirectSend, 'No messaging provider configured. Ensure Nexus.initialize() completed before using the message controllers.')) return;
68
157
 
69
158
  try {
70
159
  let numSend = 0;
71
160
  let extraDelay = 0;
72
- let curMessage = message;
73
- const scheduledMessages = [];
74
- for (const code of codes) {
75
- const scheduledMessage = new ScheduledMessage({
161
+ const scheduledPayloads = [];
162
+
163
+ for (const recipient of codes) {
164
+ const payload = {
76
165
  fileUrl,
77
- message: curMessage,
166
+ message,
78
167
  fileType,
79
168
  timeZone: timeZone === '' ? null : timeZone,
80
169
  sendTime: new Date(sendMoment + extraDelay),
81
- contentSid: contentSid,
170
+ contentSid,
82
171
  hidePreview,
83
- code,
172
+ code: normalizeCode(recipient),
84
173
  author,
85
174
  extraDelay,
86
175
  variables
87
- });
88
-
89
- // Add to scheduledMessages for both saving and scheduling
90
- scheduledMessages.push(scheduledMessage);
176
+ };
91
177
 
92
- // Increment delay and message counter
178
+ scheduledPayloads.push(payload);
93
179
  extraDelay += Math.floor(Math.random() * 5001) + 5000;
94
180
  numSend += 1;
95
181
  }
96
182
 
97
- // Schedule all messages with Agenda in parallel
98
183
  const sentMessages = await Promise.all(
99
- scheduledMessages.map(async (message) => {
100
- const savedMessage = await ScheduledMessage.create(message);
101
- return sendScheduledMessage(savedMessage);
184
+ scheduledPayloads.map(async (payload) => {
185
+ const savedMessage = await persistScheduledMessage(ScheduledMessageModel, payload);
186
+ const result = hasScheduler
187
+ ? await dependencies.sendScheduledMessage(savedMessage)
188
+ : await dependencies.sendMessage(payload);
189
+ return { result, scheduled: savedMessage };
102
190
  })
103
191
  );
104
192
 
105
193
  console.log(`Send bulk of ${numSend} messages`);
106
194
 
107
- // Extract message IDs
108
- const messageIds = sentMessages.map(msg => msg ? msg.sid : null).filter(id => id !== null);
195
+ const messageIds = sentMessages
196
+ .map(({ result, scheduled }) => pickMessageId(result, scheduled))
197
+ .filter((id) => id !== null);
109
198
 
110
- res.status(200).json({
111
- status: true,
199
+ res.status(200).json({
200
+ status: true,
112
201
  response: 'Bulk message sent',
113
- messageIds: messageIds
202
+ messageIds
114
203
  });
115
204
  } catch (err) {
116
- console.log(err.message);
205
+ console.error('Error sending bulk messages:', err);
117
206
  res.status(500).send(err.message);
118
207
  }
119
208
  };
@@ -133,8 +222,18 @@ const sendBulkMessageAirtableController = async (req, res) => {
133
222
  condition = '1',
134
223
  variables = null
135
224
  } = req.body || {};
136
- const author = (require('../config/runtimeConfig').get('USER_DB_MONGO'));
137
- const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20*1000 : new Date();
225
+ const author = runtimeConfig.get('USER_DB_MONGO');
226
+ const sendMoment = sendTime ? moment.tz(sendTime, timeZone) + 20 * 1000 : new Date();
227
+
228
+ const ScheduledMessageModel = dependencies.ScheduledMessage;
229
+ if (!ensureDependency(res, ScheduledMessageModel, 'ScheduledMessage model not configured. Call configureMessageController() to inject it.')) return;
230
+
231
+ const hasScheduler = typeof dependencies.sendScheduledMessage === 'function';
232
+ const hasDirectSend = typeof dependencies.sendMessage === 'function';
233
+ if (!ensureDependency(res, hasScheduler || hasDirectSend, 'No messaging provider configured. Ensure Nexus.initialize() completed before using the message controllers.')) return;
234
+
235
+ const airtableFetcher = dependencies.getRecordByFilter;
236
+ if (!ensureDependency(res, typeof airtableFetcher === 'function', 'Airtable getRecordByFilter not configured. Call configureMessageController() to inject it.')) return;
138
237
 
139
238
  const regex = /\[(.*?)\]/g;
140
239
  const envVariables = [];
@@ -144,15 +243,14 @@ const sendBulkMessageAirtableController = async (req, res) => {
144
243
  }
145
244
 
146
245
  try {
147
- const rows = await getRecordByFilter(baseId, tableName, condition);
246
+ const rows = await airtableFetcher(baseId, tableName, condition);
148
247
  let extraDelay = 0;
149
- let curMessage = message;
150
248
  const sentPhones = new Set();
151
- const scheduledMessages = [];
249
+ const scheduledPayloads = [];
250
+
251
+ for (const row of rows || []) {
252
+ let customMessage = message;
152
253
 
153
- for (const row of rows) {
154
- let customMessage = curMessage;
155
-
156
254
  for (const envVar of envVariables) {
157
255
  let value = row[envVar];
158
256
  if (Array.isArray(value)) value = value[0];
@@ -164,67 +262,71 @@ const sendBulkMessageAirtableController = async (req, res) => {
164
262
  let code = row[columnPhone];
165
263
  if (Array.isArray(code)) code = code[0];
166
264
  if (!code) continue;
167
- if (!code.includes('@g.us')) code = `${code}@s.whatsapp.net`;
265
+ code = normalizeCode(code);
168
266
 
169
267
  if (sentPhones.has(code)) continue;
170
268
  sentPhones.add(code);
171
269
 
172
- // Prepare message object
173
- const scheduledMessage = {
270
+ const payload = {
174
271
  fileUrl,
175
272
  message: customMessage,
176
273
  fileType,
177
274
  timeZone: timeZone === '' ? null : timeZone,
178
275
  sendTime: new Date(sendMoment + extraDelay),
179
- contentSid: contentSid,
276
+ contentSid,
180
277
  hidePreview,
181
278
  code,
182
279
  author,
183
280
  variables
184
281
  };
185
-
186
- scheduledMessages.push(scheduledMessage);
282
+
283
+ scheduledPayloads.push(payload);
187
284
  extraDelay += Math.floor(Math.random() * 5001) + 5000;
188
285
  }
189
286
 
190
287
  const sentMessages = await Promise.all(
191
- scheduledMessages.map(async (message) => {
192
- const savedMessage = await ScheduledMessage.create(message);
193
- return sendScheduledMessage(savedMessage);
288
+ scheduledPayloads.map(async (payload) => {
289
+ const savedMessage = await persistScheduledMessage(ScheduledMessageModel, payload);
290
+ const result = hasScheduler
291
+ ? await dependencies.sendScheduledMessage(savedMessage)
292
+ : await dependencies.sendMessage(payload);
293
+ return { result, scheduled: savedMessage };
194
294
  })
195
295
  );
196
296
 
197
- // Extract message IDs
198
- const messageIds = sentMessages.map(msg => msg ? msg.sid : null).filter(id => id !== null);
297
+ const messageIds = sentMessages
298
+ .map(({ result, scheduled }) => pickMessageId(result, scheduled))
299
+ .filter((id) => id !== null);
199
300
 
200
- console.log(`Iterate over ${rows.length} rows`);
301
+ console.log(`Iterate over ${(rows || []).length} rows`);
201
302
 
202
- res.status(200).json({
203
- status: true,
303
+ res.status(200).json({
304
+ status: true,
204
305
  response: 'Airtable message sent',
205
- messageIds: messageIds
306
+ messageIds
206
307
  });
207
308
  } catch (err) {
208
- console.log(err.message);
309
+ console.error('Error sending Airtable bulk messages:', err);
209
310
  res.status(500).send(err.message);
210
311
  }
211
312
  };
212
313
 
213
314
  const getLastInteractionController = async (req, res) => {
214
- const { code } = req.body;
315
+ const { code } = req.query;
316
+ const normalizedCode = normalizeCode(code);
215
317
 
216
318
  try {
217
319
  const lastMessage = await Message.findOne({
218
320
  $or: [
219
- { numero: code },
220
- { group_id: code }
321
+ { numero: normalizedCode },
322
+ { group_id: normalizedCode }
221
323
  ]
222
324
  }).sort({ createdAt: -1 }).exec();
223
325
 
224
326
  if (!lastMessage) {
225
327
  return res.status(404).send({ message: 'No messages found for the provided code.' });
226
328
  }
227
-
329
+
228
330
  const createdAt = new Date(lastMessage.createdAt);
229
331
  const now = new Date();
230
332
  const timeDiffMs = now - createdAt;
@@ -241,5 +343,6 @@ module.exports = {
241
343
  sendMessageController,
242
344
  sendBulkMessageController,
243
345
  sendBulkMessageAirtableController,
244
- getLastInteractionController
245
- };
346
+ getLastInteractionController,
347
+ configureMessageController
348
+ };
@@ -1,11 +1,3 @@
1
- // Nexus provider will be injected - templates only work with Twilio
2
- let nexusProvider = null;
3
-
4
- // Configure Nexus provider
5
- const configureNexusProvider = (provider) => {
6
- nexusProvider = provider;
7
- };
8
-
9
1
  // Check if provider supports templates
10
2
  const checkTemplateSupport = () => {
11
3
  if (!nexusProvider) {
@@ -20,10 +12,29 @@ const checkTemplateSupport = () => {
20
12
 
21
13
  const { handleApiError } = require('../utils/errorHandler');
22
14
 
23
- const { Template } = require('../templates/templateStructure');
15
+ const {
16
+ Template,
17
+ configureNexusProvider: configureTemplateProvider
18
+ } = require('../templates/templateStructure');
24
19
  const predefinedTemplates = require('../templates/predefinedTemplates');
25
20
 
26
21
 
22
+ // Nexus provider will be injected - templates only work with Twilio
23
+ let nexusProvider = null;
24
+
25
+ // Configure Nexus provider
26
+ const configureNexusProvider = (provider) => {
27
+ nexusProvider = provider;
28
+ if (typeof configureTemplateProvider === 'function') {
29
+ try {
30
+ configureTemplateProvider(provider);
31
+ } catch (err) {
32
+ console.warn('[templateController] Failed to propagate provider to template structure:', err?.message || err);
33
+ }
34
+ }
35
+ };
36
+
37
+
27
38
  const getTemplateModel = () => {
28
39
  // Require the concrete model; enforce presence instead of stubbing
29
40
  try {
@@ -441,7 +441,25 @@ class NexusMessaging {
441
441
  }
442
442
  }
443
443
 
444
- const defaultInstance = new NexusMessaging();
444
+ let defaultInstance = new NexusMessaging();
445
+
446
+ const setDefaultInstance = (instance) => {
447
+ if (!instance) {
448
+ throw new Error('setDefaultInstance requires a NexusMessaging instance');
449
+ }
450
+
451
+ const isCompatible = typeof instance.sendMessage === 'function' &&
452
+ typeof instance.sendScheduledMessage === 'function' &&
453
+ typeof instance.processIncomingMessage === 'function';
454
+
455
+ if (!isCompatible) {
456
+ throw new Error('setDefaultInstance received an incompatible object');
457
+ }
458
+
459
+ defaultInstance = instance;
460
+ };
461
+
462
+ const getDefaultInstance = () => defaultInstance;
445
463
 
446
464
  const sendMessage = async (messageData) => {
447
465
  return await defaultInstance.sendMessage(messageData);
@@ -454,5 +472,7 @@ const sendScheduledMessage = async (scheduledMessage) => {
454
472
  module.exports = {
455
473
  NexusMessaging,
456
474
  sendMessage,
457
- sendScheduledMessage
475
+ sendScheduledMessage,
476
+ setDefaultInstance,
477
+ getDefaultInstance
458
478
  };
@@ -1,5 +1,5 @@
1
1
  const { downloadFileFromS3, generatePresignedUrl } = require('../config/awsConfig.js');
2
- const { openaiClient } = require('../config/llmConfig.js');
2
+ const llmConfig = require('../config/llmConfig.js');
3
3
 
4
4
  const { Message } = require('../models/messageModel.js');
5
5
 
@@ -16,7 +16,9 @@ const mode = process.env.NODE_ENV || 'dev';
16
16
 
17
17
  async function checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxRetries = 30) {
18
18
  try {
19
- const run = await openaiClient.beta.threads.runs.retrieve(thread_id, run_id);
19
+ const client = llmConfig.openaiClient;
20
+ if (!client) throw new Error('OpenAI client not configured');
21
+ const run = await client.beta.threads.runs.retrieve(thread_id, run_id);
20
22
  console.log(`Status: ${run.status} ${thread_id} ${run_id} (attempt ${retryCount + 1})`);
21
23
 
22
24
  if (run.status === 'failed' || run.status === 'expired' || run.status === 'incomplete') {
@@ -47,7 +49,9 @@ async function checkRunStatus(assistant, thread_id, run_id, retryCount = 0, maxR
47
49
 
48
50
  async function checkIfFinished(text) {
49
51
  try {
50
- const completion = await openaiClient.chat.completions.create({
52
+ const client = llmConfig.openaiClient;
53
+ if (!client) throw new Error('OpenAI client not configured');
54
+ const completion = await client.chat.completions.create({
51
55
  model: 'gpt-4o-mini',
52
56
  messages: [
53
57
  {
@@ -210,7 +214,9 @@ async function processMessage(code, reply, thread) {
210
214
  const imageAnalysis = await analyzeImage(fileName);
211
215
  console.log(imageAnalysis);
212
216
  const invalidAnalysis = ['NOT_MEDICAL', 'QUALITY_INSUFFICIENT'];
213
- url = await generatePresignedUrl(reply.media.bucketName, reply.media.key);
217
+ if (imageAnalysis.medical_relevance) {
218
+ url = await generatePresignedUrl(reply.media.bucketName, reply.media.key);
219
+ }
214
220
  if (imageAnalysis.has_table) {
215
221
  messagesChat.push({
216
222
  type: 'text',
@@ -223,7 +229,9 @@ async function processMessage(code, reply, thread) {
223
229
  });
224
230
  } else {
225
231
  console.log('Add attachment');
226
- const file = await openaiClient.files.create({
232
+ const client = llmConfig.openaiClient;
233
+ if (!client) throw new Error('OpenAI client not configured');
234
+ const file = await client.files.create({
227
235
  file: fs.createReadStream(fileName),
228
236
  purpose: 'vision',
229
237
  });
@@ -233,7 +241,9 @@ async function processMessage(code, reply, thread) {
233
241
  });
234
242
  }
235
243
  } else if (fileName.includes('audio')) {
236
- const audioTranscript = await openaiClient.audio.transcriptions.create({
244
+ const client = llmConfig.openaiClient;
245
+ if (!client) throw new Error('OpenAI client not configured');
246
+ const audioTranscript = await client.audio.transcriptions.create({
237
247
  model: 'whisper-1',
238
248
  file: fs.createReadStream(fileName),
239
249
  response_format: 'text',
@@ -251,7 +261,9 @@ async function processMessage(code, reply, thread) {
251
261
  console.log('messagesChat', messagesChat);
252
262
  console.log('attachments', attachments);
253
263
 
254
- await openaiClient.beta.threads.messages.create(thread.thread_id, {
264
+ const client = llmConfig.openaiClient;
265
+ if (!client) throw new Error('OpenAI client not configured');
266
+ await client.beta.threads.messages.create(thread.thread_id, {
255
267
  role: 'user',
256
268
  content: messagesChat,
257
269
  attachments: attachments
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const { NexusMessaging } = require('./core/NexusMessaging');
1
+ const { NexusMessaging, setDefaultInstance } = require('./core/NexusMessaging');
2
2
  const { MongoStorage } = require('./storage/MongoStorage');
3
3
  const { MessageParser } = require('./utils/messageParser');
4
4
  const { DefaultLLMProvider } = require('./utils/defaultLLMProvider');
@@ -6,6 +6,8 @@ const { loadNexusConfig } = require('./config/configLoader');
6
6
  const templateController = require('./controllers/templateController');
7
7
  const templateFlowController = require('./controllers/templateFlowController');
8
8
  const interactive = require('./interactive');
9
+ const runtimeConfig = require('./config/runtimeConfig');
10
+ const llmConfigModule = require('./config/llmConfig');
9
11
  const {
10
12
  configureLLMProvider: configureAssistantsLLM,
11
13
  registerAssistant,
@@ -22,6 +24,11 @@ class Nexus {
22
24
  constructor(config = {}) {
23
25
  this.config = config;
24
26
  this.messaging = new NexusMessaging(config.messaging || {});
27
+ try {
28
+ setDefaultInstance(this.messaging);
29
+ } catch (err) {
30
+ console.warn('[Nexus] Failed to set default messaging instance:', err?.message || err);
31
+ }
25
32
  this.storage = null;
26
33
  this.messageParser = null;
27
34
  this.llmProvider = null;
@@ -75,6 +82,23 @@ class Nexus {
75
82
  console.warn('Warning: failed to auto-configure template providers:', e?.message || e);
76
83
  }
77
84
 
85
+
86
+ // Convenience: handle mongoUri early (before storage connect)
87
+ try {
88
+ if (options.mongoUri) {
89
+ if (storage === 'mongo') {
90
+ if (typeof storageConfig === 'object' && !storageConfig.mongoUri) {
91
+ storageConfig.mongoUri = options.mongoUri;
92
+ }
93
+ } else {
94
+ // If not using MongoStorage but a URI is provided, initialize default mongoose connection
95
+ await this.messaging.initializeMongoDB(options.mongoUri);
96
+ }
97
+ }
98
+ } catch (dbErr) {
99
+ console.warn('[Nexus] mongo convenience warning:', dbErr?.message || dbErr);
100
+ }
101
+
78
102
  // Initialize storage if provided
79
103
  if (storage) {
80
104
  try {
@@ -104,6 +128,39 @@ class Nexus {
104
128
  // Initialize default LLM provider if requested
105
129
  if (llm === 'openai') {
106
130
  this.llmProvider = new DefaultLLMProvider(llmConfig);
131
+ try {
132
+ if (this.llmProvider && typeof this.llmProvider.getClient === 'function') {
133
+ llmConfigModule.openaiClient = this.llmProvider.getClient();
134
+ }
135
+ } catch (err) {
136
+ console.warn('[Nexus] Failed to expose OpenAI client:', err?.message || err);
137
+ }
138
+
139
+ // Convenience: handle common top-level config for mongo, media bucket, airtable
140
+ try {
141
+ // Mongo URI passthrough for storage=mongo
142
+ if (options.mongoUri && storage === 'mongo') {
143
+ if (typeof storageConfig === 'object') {
144
+ storageConfig.mongoUri = storageConfig.mongoUri || options.mongoUri;
145
+ }
146
+ }
147
+
148
+ // Media bucket (overrides storage setting)
149
+ if (options.media && options.media.bucketName) {
150
+ runtimeConfig.set('AWS_S3_BUCKET_NAME', options.media.bucketName);
151
+ }
152
+
153
+ // Airtable base default (accepts alias like 'calendar' or an ID)
154
+ if (options.airtable && options.airtable.base) {
155
+ runtimeConfig.set('AIRTABLE_BASE_ID', options.airtable.base);
156
+ }
157
+ if (options.airtable && options.airtable.apiKey) {
158
+ runtimeConfig.set('AIRTABLE_API_KEY', options.airtable.apiKey);
159
+ }
160
+ } catch (cfgErr) {
161
+ console.warn('[Nexus] convenience config warning:', cfgErr?.message || cfgErr);
162
+ }
163
+
107
164
  }
108
165
 
109
166
 
@@ -25,6 +25,7 @@ function toTwilioContent(spec) {
25
25
 
26
26
  if (type === 'flow') {
27
27
  content.types['twilio/flows'] = {
28
+ type: flow?.type || spec.flowType || 'FLOW',
28
29
  body: flow?.body || body || '',
29
30
  button_text: flow?.buttonText || spec.buttonText || undefined,
30
31
  subtitle: flow?.subtitle || spec.subtitle || undefined,
@@ -1,6 +1,7 @@
1
1
  const mongoose = require('mongoose');
2
2
  const moment = require('moment-timezone');
3
3
 
4
+
4
5
  const messageSchema = new mongoose.Schema({
5
6
  nombre_whatsapp: { type: String, required: true },
6
7
  numero: { type: String, required: true },
@@ -51,12 +52,39 @@ messageSchema.pre('save', function (next) {
51
52
 
52
53
  const Message = mongoose.model('Message', messageSchema);
53
54
 
55
+
54
56
  async function insertMessage(values) {
55
- const msg = new Message(values);
56
- await msg.save();
57
- return msg;
57
+ try {
58
+ const skipNumbers = ['5215592261426@s.whatsapp.net', '5215547411345@s.whatsapp.net', '51985959446@s.whatsapp.net'];
59
+ const messageData = {
60
+ nombre_whatsapp: values.nombre_whatsapp,
61
+ numero: values.numero,
62
+ body: values.body,
63
+ timestamp: values.timestamp,
64
+ message_id: values.message_id,
65
+ is_group: values.is_group,
66
+ is_media: values.is_media,
67
+ group_id: values.group_id,
68
+ reply_id: values.reply_id,
69
+ from_me: values.from_me,
70
+ processed: skipNumbers.includes(values.numero),
71
+ media: values.media ? values.media : null
72
+ };
73
+
74
+ await Message.findOneAndUpdate(
75
+ { message_id: values.message_id, body: values.body },
76
+ { $setOnInsert: messageData },
77
+ { upsert: true, new: true }
78
+ );
79
+
80
+ console.log('Message inserted or updated successfully');
81
+ } catch (err) {
82
+ console.error('Error inserting message:', err);
83
+ throw err;
84
+ }
58
85
  }
59
86
 
87
+
60
88
  function formatTimestamp(unixTimestamp) {
61
89
  const date = new Date(unixTimestamp * 1000);
62
90
  return date.toLocaleString('sv-MX', {
@@ -65,6 +93,7 @@ function formatTimestamp(unixTimestamp) {
65
93
  }).replace(' ', 'T').slice(0, 19);
66
94
  }
67
95
 
96
+
68
97
  function getMessageValues(message, content, reply, is_media) {
69
98
  const nombre_whatsapp = message.pushName;
70
99
  const numero = message.key.participant || message.key.remoteJid;
@@ -90,10 +119,27 @@ function getMessageValues(message, content, reply, is_media) {
90
119
  };
91
120
  }
92
121
 
122
+ async function getContactDisplayName(contactNumber) {
123
+ try {
124
+ const latestMessage = await Message.findOne({ numero: contactNumber })
125
+ .sort({ createdAt: -1 })
126
+ .select('nombre_whatsapp');
127
+
128
+ if (latestMessage && latestMessage.nombre_whatsapp && latestMessage.nombre_whatsapp.trim() !== '') {
129
+ return latestMessage.nombre_whatsapp;
130
+ } else {
131
+ return contactNumber;
132
+ }
133
+ } catch (error) {
134
+ console.error(`Error fetching display name for ${contactNumber}:`, error);
135
+ return contactNumber;
136
+ }
137
+ }
138
+
93
139
  module.exports = {
94
140
  Message,
95
- // Backward-compatible helper used by helpers
96
141
  insertMessage,
97
142
  getMessageValues,
98
- formatTimestamp
99
- };
143
+ formatTimestamp,
144
+ getContactDisplayName
145
+ };
@@ -32,7 +32,7 @@ const messageRouteDefinitions = {
32
32
  'POST /send': 'sendMessageController',
33
33
  'POST /send-bulk': 'sendBulkMessageController',
34
34
  'POST /send-bulk-airtable': 'sendBulkMessageAirtableController',
35
- 'POST /get-last': 'getLastInteractionController'
35
+ 'GET /last': 'getLastInteractionController'
36
36
  };
37
37
 
38
38
  const templateRouteDefinitions = {
@@ -1,4 +1,8 @@
1
- const { Historial_Clinico_ID } = require('../config/airtableConfig.js');
1
+ const { Historial_Clinico_ID, Monitoreo_ID } = require('../config/airtableConfig.js');
2
+ const AWS = require('../config/awsConfig.js');
3
+ const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
4
+ const { addRecord } = require('../services/airtableService.js');
5
+ const runtimeConfig = require('../config/runtimeConfig');
2
6
 
3
7
  let llmProvider = null;
4
8
  const configureLLMProvider = (provider) => {
@@ -243,6 +247,25 @@ const replyAssistant = async function (code, message_ = null, thread_ = null, ru
243
247
 
244
248
  if (urls.length > 0) {
245
249
  console.log('urls', urls);
250
+ const { pdfBuffer, processedFiles } = await combineImagesToPDF({ code });
251
+ console.log('AFTER COMBINED IN BUFFER', processedFiles);
252
+ const key = `${code}-${Date.now()}-combined.pdf`;
253
+ const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
254
+ if (bucket && pdfBuffer) {
255
+ await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
256
+ const url = await AWS.generatePresignedUrl(bucket, key);
257
+ const curRow = await getCurRow(Monitoreo_ID, code);
258
+ const customer_id = curRow?.[0]?.recordID || curRow?.[0]?.record_id || curRow?.[0]?.id || null;
259
+ console.log('customer_id:', customer_id);
260
+ try {
261
+ await addRecord(Monitoreo_ID, 'estudios', [{ fields: { estudios: urls, combined_estudios: [{ url }], patient_id: customer_id ? [customer_id] : [] } }]);
262
+ } catch (e) {
263
+ console.warn('Failed to add Airtable estudios record:', e?.message || e);
264
+ }
265
+ }
266
+ if (processedFiles && processedFiles.length) {
267
+ await cleanupFiles(processedFiles);
268
+ }
246
269
  }
247
270
 
248
271
  thread = await getThread(code);
@@ -1,4 +1,5 @@
1
1
  const mongoose = require('mongoose');
2
+ const runtimeConfig = require('../config/runtimeConfig');
2
3
 
3
4
  /**
4
5
  * MongoDB storage interface for messages and interactions
@@ -16,21 +17,7 @@ class MongoStorage {
16
17
  }
17
18
 
18
19
  createSchemas() {
19
- const messageSchema = new mongoose.Schema({
20
- messageId: String,
21
- numero: String,
22
- body: String,
23
- timestamp: String,
24
- isGroup: { type: Boolean, default: false },
25
- isMedia: { type: Boolean, default: false },
26
- fromMe: { type: Boolean, default: false },
27
- contentSid: String,
28
- isTemplate: { type: Boolean, default: false },
29
- templateVariables: String,
30
- provider: String,
31
- createdAt: { type: Date, default: Date.now }
32
- });
33
-
20
+ const { Message } = require('../models/messageModel');
34
21
  const interactionSchema = new mongoose.Schema({
35
22
  messageId: String,
36
23
  numero: String,
@@ -54,7 +41,7 @@ class MongoStorage {
54
41
  });
55
42
 
56
43
  return {
57
- Message: mongoose.models.Message || mongoose.model('Message', messageSchema),
44
+ Message,
58
45
  Interaction: mongoose.models.Interaction || mongoose.model('Interaction', interactionSchema),
59
46
  Thread: mongoose.models.Thread || mongoose.model('Thread', threadSchema)
60
47
  };
@@ -74,28 +61,76 @@ class MongoStorage {
74
61
 
75
62
  async saveMessage(messageData) {
76
63
  try {
77
- const message = new this.schemas.Message({
78
- messageId: messageData.messageId,
79
- numero: messageData.to || messageData.from,
80
- body: messageData.message || messageData.body,
81
- timestamp: this.formatTimestamp(messageData.timestamp),
82
- isGroup: messageData.isGroup || false,
83
- isMedia: messageData.fileType && messageData.fileType !== 'text',
84
- fromMe: messageData.fromMe || false,
85
- contentSid: messageData.contentSid,
86
- isTemplate: !!messageData.contentSid,
87
- templateVariables: messageData.variables ? JSON.stringify(messageData.variables) : null,
88
- provider: messageData.provider
89
- });
90
-
91
- await message.save();
92
- return message;
64
+ const values = this.buildLegacyMessageValues(messageData);
65
+ const { insertMessage } = require('../models/messageModel');
66
+ await insertMessage(values);
67
+ return values;
93
68
  } catch (error) {
94
69
  console.error('Error saving message:', error);
95
70
  throw error;
96
71
  }
97
72
  }
98
73
 
74
+ normalizeNumero(numero) {
75
+ if (!numero || typeof numero !== 'string') return numero;
76
+
77
+ const trimmed = numero.trim();
78
+ if (trimmed.startsWith('whatsapp:')) {
79
+ return trimmed;
80
+ }
81
+
82
+ if (trimmed.includes('@')) {
83
+ return trimmed;
84
+ }
85
+
86
+ if (trimmed.startsWith('+')) {
87
+ return `whatsapp:${trimmed}`;
88
+ }
89
+
90
+ if (/^\d+$/.test(trimmed)) {
91
+ return `whatsapp:+${trimmed}`;
92
+ }
93
+
94
+ return trimmed;
95
+ }
96
+
97
+ buildLegacyMessageValues(messageData = {}) {
98
+ const numero = messageData.to || messageData.code || messageData.numero || messageData.from;
99
+ const rawNumero = typeof numero === 'string' ? numero : '';
100
+ const normalizedNumero = this.normalizeNumero(rawNumero);
101
+ const isGroup = normalizedNumero.includes('@g.us');
102
+ const isMedia = messageData.isMedia === true || (messageData.fileType && messageData.fileType !== 'text');
103
+ const now = new Date();
104
+ const timestamp = now.toISOString();
105
+ const nombre = messageData.nombre_whatsapp || messageData.author || messageData.fromName || runtimeConfig.get('USER_DB_MONGO') || process.env.USER_DB_MONGO || 'Nexus';
106
+ const textBody = messageData.message || messageData.body || (messageData.contentSid ? `[Template:${messageData.contentSid}]` : isMedia ? `[Media:${messageData.fileType || 'attachment'}]` : '');
107
+ const providerId = messageData.messageId || messageData.sid || messageData.id || messageData._id || `pending-${now.getTime()}-${Math.floor(Math.random()*1000)}`;
108
+
109
+ const media = messageData.media || (messageData.fileUrl ? {
110
+ url: messageData.fileUrl,
111
+ mediaType: messageData.fileType === 'text' ? null : (messageData.fileType || 'document'),
112
+ fileName: messageData.fileName || null,
113
+ contentType: messageData.contentType || null,
114
+ metadata: messageData.mediaMetadata || null
115
+ } : null);
116
+
117
+ return {
118
+ nombre_whatsapp: nombre,
119
+ numero: normalizedNumero,
120
+ body: textBody,
121
+ timestamp,
122
+ message_id: providerId,
123
+ is_group: isGroup,
124
+ is_media: isMedia,
125
+ group_id: isGroup ? normalizedNumero : null,
126
+ reply_id: messageData.reply_id || messageData.replyId || null,
127
+ from_me: messageData.fromMe !== undefined ? messageData.fromMe : true,
128
+ media,
129
+ content_sid: messageData.contentSid || null,
130
+ template_variables: messageData.variables ? JSON.stringify(messageData.variables) : null
131
+ };
132
+ }
133
+
99
134
  async saveInteractive(interactionData) {
100
135
  try {
101
136
  const interaction = new this.schemas.Interaction({
@@ -43,19 +43,21 @@ class Template {
43
43
  }
44
44
 
45
45
  addBodyVariation(text, variableDescriptions = []) {
46
+ const normalizedVariables = Template.normalizeVariables(variableDescriptions);
46
47
  this.variations.push({
47
48
  text: text,
48
- variables: variableDescriptions
49
+ variables: normalizedVariables
49
50
  });
50
51
  return this;
51
52
  }
52
53
 
53
54
  setBody(text, variableDescriptions = []) {
54
- this.addBodyVariation(text, variableDescriptions);
55
+ const normalizedVariables = Template.normalizeVariables(variableDescriptions);
56
+ this.addBodyVariation(text, normalizedVariables);
55
57
 
56
58
  const selectedVariation = this.variations.length > 1 ?
57
59
  this.variations[Math.floor(Math.random() * this.variations.length)] :
58
- { text, variables: variableDescriptions };
60
+ { text, variables: normalizedVariables };
59
61
 
60
62
  const enhancedText = `${selectedVariation.text}`;
61
63
 
@@ -196,6 +198,49 @@ class Template {
196
198
  const createdTemplate = await nexusProvider.createTemplate(twilioFormat);
197
199
  return createdTemplate;
198
200
  }
201
+
202
+ static normalizeVariables(variableDescriptions = []) {
203
+ if (!variableDescriptions) return [];
204
+
205
+ if (Array.isArray(variableDescriptions)) {
206
+ return variableDescriptions;
207
+ }
208
+
209
+ if (typeof variableDescriptions === 'object') {
210
+ return Object.entries(variableDescriptions)
211
+ .sort(([a], [b]) => {
212
+ const na = Number(a);
213
+ const nb = Number(b);
214
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) {
215
+ return na - nb;
216
+ }
217
+ return a.localeCompare(b);
218
+ })
219
+ .map(([key, value], index) => {
220
+ if (typeof value === 'string') {
221
+ return {
222
+ name: `var_${key}`,
223
+ description: value,
224
+ example: `Ejemplo ${key}`
225
+ };
226
+ }
227
+ if (value && typeof value === 'object') {
228
+ return {
229
+ name: value.name || `var_${key}`,
230
+ description: value.description || `Variable ${key}`,
231
+ example: value.example || value.default || `Ejemplo ${key}`
232
+ };
233
+ }
234
+ return {
235
+ name: `var_${key}`,
236
+ description: `Variable ${key}`,
237
+ example: `Ejemplo ${key}`
238
+ };
239
+ });
240
+ }
241
+
242
+ return [];
243
+ }
199
244
  }
200
245
 
201
246
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "publishConfig": {
6
6
  "access": "public"