@peopl-health/nexus 4.4.4 → 4.5.21

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.
Files changed (45) hide show
  1. package/README.md +4 -9
  2. package/lib/adapters/BaileysProvider.js +4 -2
  3. package/lib/adapters/MessageProvider.js +2 -2
  4. package/lib/adapters/TwilioProvider.js +7 -3
  5. package/lib/controllers/assistantController.js +2 -6
  6. package/lib/controllers/bugReportController.js +2 -2
  7. package/lib/controllers/conversationController.js +13 -13
  8. package/lib/controllers/interactionController.js +2 -2
  9. package/lib/controllers/messageController.js +6 -5
  10. package/lib/controllers/qualityMessageController.js +3 -2
  11. package/lib/core/AssistantProcessor.js +3 -3
  12. package/lib/core/BatchingManager.js +6 -5
  13. package/lib/core/NexusMessaging.js +230 -155
  14. package/lib/core/PhiProcessor.js +113 -0
  15. package/lib/eval/EvalProvider.js +6 -1
  16. package/lib/helpers/baileysHelper.js +3 -1
  17. package/lib/helpers/conversationWindowHelper.js +4 -4
  18. package/lib/helpers/deliveryAttemptHelper.js +3 -1
  19. package/lib/helpers/filesHelper.js +2 -5
  20. package/lib/helpers/messageHelper.js +10 -71
  21. package/lib/helpers/messageStatusHelper.js +3 -3
  22. package/lib/helpers/nerHelper.js +64 -0
  23. package/lib/helpers/templateRecoveryHelper.js +2 -0
  24. package/lib/index.d.ts +16 -1
  25. package/lib/jobs/ScheduledMessageJob.js +15 -23
  26. package/lib/jobs/TemplateApprovalJob.js +4 -1
  27. package/lib/memory/DefaultMemoryManager.js +5 -5
  28. package/lib/memory/SessionManager.js +3 -6
  29. package/lib/models/deliveryAttemptModel.js +1 -1
  30. package/lib/models/globalEntityMapModel.js +27 -0
  31. package/lib/models/messageModel.js +0 -94
  32. package/lib/models/tokenMapModel.js +28 -0
  33. package/lib/providers/NerClient.js +43 -0
  34. package/lib/providers/OpenAIResponsesProvider.js +9 -7
  35. package/lib/providers/OpenAIResponsesProviderTools.js +26 -9
  36. package/lib/services/assistantService.js +20 -11
  37. package/lib/services/dashboardService.js +4 -4
  38. package/lib/services/globalEntityService.js +59 -0
  39. package/lib/services/messageService.js +107 -0
  40. package/lib/services/metaService.js +5 -13
  41. package/lib/services/patientService.js +3 -2
  42. package/lib/services/tokenMapService.js +100 -0
  43. package/lib/storage/MongoStorage.js +9 -14
  44. package/lib/utils/tokenMapUtils.js +12 -0
  45. package/package.json +1 -1
@@ -11,6 +11,7 @@ const { handleFunctionCalls } = require('../providers/OpenAIResponsesProviderToo
11
11
  const { composePrompt, resolveTools } = require('../services/promptComposerService');
12
12
  const { getToolSchemas: getRegistrySchemas } = require('../services/toolRegistryService');
13
13
  const { getAssistantById } = require('../services/assistantResolver');
14
+ const { PhiProcessor } = require('../core/PhiProcessor');
14
15
 
15
16
  const MAX_FUNCTION_ROUNDS = parseInt(process.env.MAX_FUNCTION_ROUNDS || '5', 10);
16
17
 
@@ -47,6 +48,10 @@ class EvalProvider {
47
48
  client: this.client,
48
49
  defaultModels: { responseModel: this.model },
49
50
  });
51
+ this.phiProcessor = new PhiProcessor({
52
+ encode: config.phi?.encode || false,
53
+ ner: config.phi?.ner || null,
54
+ });
50
55
  }
51
56
 
52
57
  id() {
@@ -254,7 +259,7 @@ class EvalProvider {
254
259
  const calls = finalResponse.output.filter(item => item.type === 'function_call');
255
260
  if (!calls.length) break;
256
261
 
257
- const { outputs, toolsExecuted } = await handleFunctionCalls(calls, assistant);
262
+ const { outputs, toolsExecuted } = await handleFunctionCalls(calls, assistant, this.phiProcessor);
258
263
  currentInput.push(...finalResponse.output, ...outputs);
259
264
  allToolsExecuted.push(...toolsExecuted);
260
265
 
@@ -2,10 +2,12 @@ const { downloadMediaMessage } = require('baileys');
2
2
 
3
3
  const { logger } = require('../utils/logger');
4
4
 
5
- const { insertMessage, getMessageValues } = require('../models/messageModel.js');
5
+ const { getMessageValues } = require('../models/messageModel.js');
6
6
 
7
7
  const { uploadMediaToS3 } = require('../helpers/mediaHelper.js');
8
8
 
9
+ const { insertMessage } = require('../services/messageService');
10
+
9
11
  async function processMessage(message, messageType) {
10
12
  try {
11
13
  const { content } = extractMessageContent(message, messageType);
@@ -1,14 +1,14 @@
1
- const { Message } = require('../models/messageModel');
1
+ const { getMessages } = require('../services/messageService');
2
2
 
3
3
  const WINDOW_MS = 24 * 60 * 60 * 1000;
4
4
 
5
5
  async function isWithin24HourWindow(code) {
6
6
  if (!code) return false;
7
7
  const cutoff = new Date(Date.now() - WINDOW_MS);
8
- const recent = await Message.findOne(
8
+ const [recent] = await getMessages(
9
9
  { numero: code, from_me: false, createdAt: { $gte: cutoff } },
10
- '_id'
11
- ).lean();
10
+ { select: '_id', limit: 1 }
11
+ );
12
12
  return !!recent;
13
13
  }
14
14
 
@@ -3,6 +3,8 @@ const { logger } = require('../utils/logger');
3
3
  const { Message } = require('../models/messageModel');
4
4
  const { DeliveryAttempt, DELIVERY_ATTEMPT_TERMINAL_STATUSES } = require('../models/deliveryAttemptModel');
5
5
 
6
+ const { getMessages } = require('../services/messageService');
7
+
6
8
  async function recordDeliveryAttempt({
7
9
  messageData = null, messageId = null, twilioResult = null, kind, body = null,
8
10
  errorSource = null, errorCode = null, errorMessage = null
@@ -13,7 +15,7 @@ async function recordDeliveryAttempt({
13
15
  let targetId = messageId;
14
16
  if (!targetId) {
15
17
  if (!sid) return null;
16
- const msgDoc = await Message.findOne({ message_id: sid }, '_id').lean();
18
+ const [msgDoc] = await getMessages({ message_id: sid }, { select: '_id', limit: 1 });
17
19
  if (!msgDoc) {
18
20
  logger.warn('[deliveryAttemptHelper] Message not found for SID; skipping attempt record', { twilioSid: sid });
19
21
  return null;
@@ -8,7 +8,7 @@ const { downloadFileFromS3 } = require('../config/awsConfig.js');
8
8
  const { sanitizeFilename } = require('../utils/sanitizerUtils.js');
9
9
  const { logger } = require('../utils/logger');
10
10
 
11
- const { Message } = require('../models/messageModel.js');
11
+ const { getMessages } = require('../services/messageService');
12
12
 
13
13
  async function convertPdfToImages(pdfName, existingPdfPath = null) {
14
14
  const outputDir = path.join(__dirname, 'assets', 'tmp');
@@ -89,10 +89,7 @@ const cleanupFiles = async (files) => {
89
89
 
90
90
  async function downloadMediaAndCreateFile(code, reply) {
91
91
  try {
92
- const resultMedia = await Message.findOne({
93
- message_id: reply.message_id,
94
- media: { $ne: null }
95
- });
92
+ const [resultMedia] = await getMessages({ message_id: reply.message_id, media: { $ne: null } }, { limit: 1 });
96
93
 
97
94
  const { bucketName, key } = resultMedia?.media || {};
98
95
  if (!bucketName || !key) return [];
@@ -1,67 +1,8 @@
1
1
  const moment = require('moment-timezone');
2
2
 
3
3
  const { logger } = require('../utils/logger');
4
- const { maskSensitiveValue } = require('../utils/sanitizerUtils');
5
4
 
6
- const { Message } = require('../models/messageModel.js');
7
-
8
- const storeProcessedContent = async (reply, thread, processedContent) => {
9
- if (!processedContent || !reply.media) return;
10
-
11
- await Message.updateOne(
12
- { message_id: reply.message_id },
13
- { $set: {
14
- assistant_id: thread.getAssistantId(),
15
- thread_id: thread.getConversationId(),
16
- media: {
17
- ...reply.media,
18
- metadata: {
19
- ...(reply.media.metadata || {}),
20
- processedContent
21
- }
22
- }
23
- }}
24
- );
25
- };
26
-
27
- async function getLastMessages(code) {
28
- try {
29
- const messages = await Message.find({
30
- processed: false, numero: code,
31
- $or: [{ origin: 'patient' }, { origin: 'whatsapp_platform' }]
32
- }).sort({ createdAt: 1 });
33
-
34
- if (!messages?.length) {
35
- logger.info('[getLastMessages] No unprocessed messages', { code: maskSensitiveValue(code) });
36
- return null;
37
- }
38
-
39
- logger.info('[getLastMessages] Found messages', { count: messages.length, code: maskSensitiveValue(code) });
40
- return messages;
41
- } catch (error) {
42
- logger.error('[getLastMessages] Error', { code: maskSensitiveValue(code), error: error.message });
43
- return null;
44
- }
45
- }
46
-
47
- async function getLastNMessages(code, n, before = null, options = {}) {
48
- try {
49
- const query = { numero: code, ...options.query };
50
- if (before) query.createdAt = { [options.beforeOperator || '$lte']: before };
51
-
52
- const messages = await Message.find(query)
53
- .select(options.select || null)
54
- .sort(options.sort || { createdAt: -1 })
55
- .limit(n)
56
- .lean();
57
-
58
- logger.debug('[getLastNMessages] Retrieved', { count: messages?.length || 0, code: maskSensitiveValue(code), n });
59
- return messages || [];
60
- } catch (error) {
61
- logger.error('[getLastNMessages] Error', { code: maskSensitiveValue(code), n, error: error.message });
62
- return [];
63
- }
64
- }
5
+ const { getMessages } = require('../services/messageService');
65
6
 
66
7
  function formatMessage(reply) {
67
8
  try {
@@ -142,20 +83,18 @@ function getMessageTools(reply) {
142
83
  }
143
84
  }
144
85
 
145
- async function isRecentMessage(chatId) {
146
- const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
147
- const recent = await Message.findOne({
148
- $or: [{ group_id: chatId }, { numero: chatId }],
149
- createdAt: { $gte: fiveMinutesAgo }
150
- }).sort({ createdAt: -1 });
151
- return !!recent;
86
+ async function getLastNMessages(code, n, anchor = null, opts = {}) {
87
+ const { beforeOperator = '$lte', query: extraQuery = {} } = opts;
88
+ const query = {
89
+ ...extraQuery,
90
+ numero: code,
91
+ ...(anchor ? { createdAt: { [beforeOperator]: anchor } } : {}),
92
+ };
93
+ return getMessages(query, { sort: { createdAt: -1 }, limit: n });
152
94
  }
153
95
 
154
96
  module.exports = {
155
- storeProcessedContent,
156
- getLastMessages,
157
- getLastNMessages,
158
97
  formatMessage,
159
98
  getMessageTools,
160
- isRecentMessage
99
+ getLastNMessages,
161
100
  };
@@ -10,6 +10,7 @@ const { handle24HourWindowError } = require('../helpers/templateRecoveryHelper')
10
10
  const { updateDeliveryAttemptByTwilioSid } = require('../helpers/deliveryAttemptHelper');
11
11
 
12
12
  const { addLinkedRecord, updateRecordByFilter } = require('../services/airtableService');
13
+ const { getMessages } = require('../services/messageService');
13
14
 
14
15
  async function updateMessageStatus(messageSid, status, errorCode = null, errorMessage = null) {
15
16
  try {
@@ -114,9 +115,8 @@ async function handleStatusCallback(twilioStatusData, { eventBus } = {}) {
114
115
 
115
116
  async function getMessageStatus(messageSid) {
116
117
  try {
117
- return await Message.findOne({ message_id: messageSid })
118
- .select('statusInfo message_id numero body')
119
- .lean();
118
+ const [msg] = await getMessages({ message_id: messageSid }, { select: 'statusInfo message_id numero body', limit: 1 });
119
+ return msg ?? null;
120
120
  } catch (error) {
121
121
  logger.error('[MessageStatus] Error fetching status', { messageSid, error: error.message });
122
122
  return null;
@@ -0,0 +1,64 @@
1
+ const STRUCTURED_PATTERNS = [
2
+ {
3
+ label: 'TELEFONO',
4
+ regex: /(?<!\d)(?:(?:\+?52[\s-]?)?(?:\d{2,3}[\s-]\d{3,4}[\s-]\d{4}|\d{10})|(?:\+?51[\s-]?)?9\d{2}[\s-]?\d{3}[\s-]?\d{3})(?!\d)/g,
5
+ },
6
+ {
7
+ label: 'EMAIL',
8
+ regex: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g,
9
+ },
10
+ {
11
+ label: 'ID',
12
+ regex: /(?<!\w)(?:[A-Z]{4}\d{6}[HM][A-Z]{5}[A-Z\d]\d|[A-ZÑ&]{3,4}\d{6}[A-Z\d]{3}|\d{8})\b/g,
13
+ },
14
+ ];
15
+
16
+ const WORD_CHARS = '0-9A-Za-z_ÁÉÍÓÚÑáéíóúñ';
17
+ const NAME_REGEX = new RegExp(`(?<![${WORD_CHARS}])([A-ZÁÉÍÓÚÑ][a-záéíóúñ]{1,}(?:\\s+[A-ZÁÉÍÓÚÑ][a-záéíóúñ]{1,}){1,3})(?![${WORD_CHARS}])`, 'g');
18
+
19
+ const detectStructured = (text) => {
20
+ const entities = [];
21
+ for (const { label, regex } of STRUCTURED_PATTERNS) {
22
+ regex.lastIndex = 0;
23
+ let match;
24
+ while ((match = regex.exec(text)) !== null) {
25
+ entities.push({
26
+ start: match.index,
27
+ end: match.index + match[0].length,
28
+ plaintext: match[0],
29
+ label,
30
+ score: null,
31
+ });
32
+ }
33
+ }
34
+ return entities;
35
+ };
36
+
37
+ const personaFallback = (text) => {
38
+ const entities = [];
39
+ NAME_REGEX.lastIndex = 0;
40
+ let match;
41
+ while ((match = NAME_REGEX.exec(text)) !== null) {
42
+ entities.push({
43
+ start: match.index,
44
+ end: match.index + match[0].length,
45
+ plaintext: match[0],
46
+ label: 'PERSONA',
47
+ score: null,
48
+ });
49
+ }
50
+ return entities;
51
+ };
52
+
53
+ const mergeSpans = (bert, structured) => {
54
+ const filtered = bert.filter((b) =>
55
+ !structured.some((s) => b.start < s.end && b.end > s.start)
56
+ );
57
+ return [...filtered, ...structured].sort((a, b) => a.start - b.start);
58
+ };
59
+
60
+ module.exports = {
61
+ detectStructured,
62
+ personaFallback,
63
+ mergeSpans,
64
+ };
@@ -103,6 +103,8 @@ async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messag
103
103
  messageId: messageDocId,
104
104
  originalMessageSid: messageSid
105
105
  });
106
+
107
+ return { delivered: false, deferred: true };
106
108
  } catch (error) {
107
109
  logger.error('[TemplateRecovery] Error', { source, messageSid, error: error.message });
108
110
  }
package/lib/index.d.ts CHANGED
@@ -246,7 +246,7 @@ declare module '@peopl-health/nexus' {
246
246
  getPipeline(): ProcessingPipeline;
247
247
  getAssistantProcessor(): AssistantProcessor;
248
248
  processInstruction(code: string, instruction: string, role?: string): Promise<string | null>;
249
- processSystemMessage(code: string, messages: string | string[], role?: string): Promise<string | null>;
249
+ processSystemMessage(code: string, messages: string | string[], role?: string, options?: { triggeredBy?: string; reply?: boolean }): Promise<string | null>;
250
250
  isConnected(): boolean;
251
251
  disconnect(): Promise<void>;
252
252
  }
@@ -289,6 +289,12 @@ declare module '@peopl-health/nexus' {
289
289
  messaging?: any;
290
290
  }
291
291
 
292
+ export interface NerOptions {
293
+ nerUrl?: string;
294
+ nerApiKey?: string;
295
+ nerTimeoutMs?: number;
296
+ }
297
+
292
298
  export interface InitializeOptions {
293
299
  provider?: 'twilio' | 'baileys';
294
300
  providerConfig?: TwilioConfig | BaileysConfig;
@@ -297,6 +303,15 @@ declare module '@peopl-health/nexus' {
297
303
  assistant?: AssistantConfig;
298
304
  parser?: boolean;
299
305
  parserConfig?: ParserConfig;
306
+ ner?: NerOptions;
307
+ }
308
+
309
+ export interface NerEntity {
310
+ start: number;
311
+ end: number;
312
+ plaintext: string;
313
+ label: string;
314
+ score: number | null;
300
315
  }
301
316
 
302
317
  export class Nexus {
@@ -16,13 +16,12 @@ const TERMINAL_STATUSES = ['sent', 'cancelled'];
16
16
  const STALE_SENDING_MS = 5 * 60 * 1000;
17
17
 
18
18
  class ScheduledMessageJob extends BaseJob {
19
- constructor({ queueAdapter, sendMessage, requireMessageStorage = null, requireProvider = null } = {}) {
19
+ constructor({ queueAdapter, sendMessage, requireProvider = null } = {}) {
20
20
  super({ queueAdapter, queueName: QUEUE_NAME });
21
21
  if (typeof sendMessage !== 'function') {
22
22
  throw new Error('ScheduledMessageJob requires a sendMessage function');
23
23
  }
24
24
  this.sendMessage = sendMessage;
25
- this.requireMessageStorage = requireMessageStorage;
26
25
  this.requireProvider = requireProvider;
27
26
  }
28
27
 
@@ -94,26 +93,11 @@ class ScheduledMessageJob extends BaseJob {
94
93
  }
95
94
 
96
95
  try {
97
- let parentMessageId = msg.parentMessageId || null;
98
- if (!parentMessageId) {
99
- if (msg.contentSid && !msg.message) {
100
- const provider = this.requireProvider?.();
101
- const rendered = await provider?.renderTemplate?.(msg.contentSid, msg.variables);
102
- if (rendered) msg.message = rendered;
103
- }
104
- const storage = this.requireMessageStorage?.();
105
- if (storage?.savePendingMessage) {
106
- const parent = await storage.savePendingMessage({
107
- code: msg.code, body: msg.message, fileUrl: msg.fileUrl, fileType: msg.fileType,
108
- contentSid: msg.contentSid, variables: msg.variables,
109
- fromMe: true, processed: true,
110
- frontendId: msg.frontendId, triggeredBy: msg.triggeredBy
111
- });
112
- parentMessageId = parent?._id || null;
113
- if (parentMessageId) {
114
- await ScheduledMessage.updateOne({ _id: scheduledMessageId }, { $set: { parentMessageId } });
115
- }
116
- }
96
+ const parentMessageId = msg.parentMessageId || null;
97
+ if (!parentMessageId && msg.contentSid && !msg.message) {
98
+ const provider = this.requireProvider?.();
99
+ const rendered = await provider?.renderTemplate?.(msg.contentSid, msg.variables);
100
+ if (rendered) msg.message = rendered;
117
101
  }
118
102
 
119
103
  const result = await this.sendMessage({
@@ -124,7 +108,15 @@ class ScheduledMessageJob extends BaseJob {
124
108
  contentSid: msg.contentSid,
125
109
  variables: msg.variables,
126
110
  hidePreview: msg.hidePreview,
127
- parentMessageId
111
+ frontendId: msg.frontendId,
112
+ triggeredBy: msg.triggeredBy,
113
+ processed: true,
114
+ ...(parentMessageId ? { parentMessageId } : {
115
+ onParentMessageId: async (id) => {
116
+ if (!id) return;
117
+ await ScheduledMessage.updateOne({ _id: scheduledMessageId }, { $set: { parentMessageId: String(id) } });
118
+ },
119
+ }),
128
120
  });
129
121
 
130
122
  const messageId = result?.messageId || result?.sid || null;
@@ -1,6 +1,9 @@
1
1
  const { logger } = require('../utils/logger');
2
2
  const { Message } = require('../models/messageModel');
3
3
  const { DeliveryAttempt } = require('../models/deliveryAttemptModel');
4
+
5
+ const { getMessages } = require('../services/messageService');
6
+
4
7
  const { BaseJob } = require('./BaseJob');
5
8
 
6
9
  const QUEUE_NAME = 'template-approval';
@@ -48,7 +51,7 @@ class TemplateApprovalJob extends BaseJob {
48
51
  }
49
52
 
50
53
  async _process({ templateSid, messageId, originalMessageSid, attempt = 0 }) {
51
- const message = await Message.findById(messageId).lean();
54
+ const [message] = await getMessages({ _id: messageId }, { limit: 1 });
52
55
 
53
56
  if (!message) {
54
57
  logger.warn('[TemplateApprovalJob] Message not found', { messageId, templateSid });
@@ -4,11 +4,12 @@ const { logger } = require('../utils/logger');
4
4
 
5
5
  const { MemoryManager } = require('../memory/MemoryManager');
6
6
 
7
- const { getLastNMessages, getMessageTools, formatMessage } = require('../helpers/messageHelper');
7
+ const { getMessageTools, formatMessage } = require('../helpers/messageHelper');
8
8
 
9
9
  const { handlePendingFunctionCalls: handlePendingFunctionCallsUtil } = require('../providers/OpenAIResponsesProviderTools');
10
10
 
11
11
  const { getRecordByFilter } = require('../services/airtableService');
12
+ const { getMessages } = require('../services/messageService');
12
13
 
13
14
  class DefaultMemoryManager extends MemoryManager {
14
15
  constructor(options = {}) {
@@ -21,10 +22,9 @@ class DefaultMemoryManager extends MemoryManager {
21
22
 
22
23
  try {
23
24
  const beforeCheckpoint = config.beforeCheckpoint ?? message?.createdAt ?? null;
24
- const allMessages = await getLastNMessages(thread.code, this.maxHistoricalMessages, beforeCheckpoint, {
25
- query: { origin: { $ne: 'instruction' } },
26
- beforeOperator: config.beforeOperator || '$lte',
27
- });
25
+ const query = { numero: thread.code, origin: { $ne: 'instruction' } };
26
+ if (beforeCheckpoint) query.createdAt = { [config.beforeOperator || '$lte']: beforeCheckpoint };
27
+ const allMessages = await getMessages(query, { sort: { createdAt: -1 }, limit: this.maxHistoricalMessages });
28
28
  const additional = config.additionalMessages || [];
29
29
 
30
30
  if (!allMessages?.length) {
@@ -1,7 +1,7 @@
1
1
  const { logger } = require('../utils/logger');
2
2
  const { isBenchMode } = require('../utils/benchModeHelper');
3
3
 
4
- const { Message } = require('../models/messageModel');
4
+ const { getMessages } = require('../services/messageService');
5
5
 
6
6
  const { MemoryExtractor } = require('./MemoryExtractor');
7
7
 
@@ -77,11 +77,8 @@ class SessionManager {
77
77
  }
78
78
 
79
79
  _fetchSessionMessages(numero, sessionStart) {
80
- return Message.find({
81
- numero,
82
- origin: { $ne: 'instruction' },
83
- createdAt: { $gte: new Date(sessionStart) },
84
- }).sort({ createdAt: 1 }).lean();
80
+ const query = { numero, origin: { $ne: 'instruction' }, createdAt: { $gte: new Date(sessionStart) } };
81
+ return getMessages(query, { sort: { createdAt: 1 } });
85
82
  }
86
83
 
87
84
  _generateSessionId(numero) {
@@ -2,7 +2,7 @@ const mongoose = require('mongoose');
2
2
 
3
3
  const KIND_VALUES = ['freeform', 'template', 'recovery_template', 'recovery_template_setup'];
4
4
  const STATUS_VALUES = [null, 'queued', 'sending', 'sent', 'delivered', 'undelivered', 'failed', 'read'];
5
- const TERMINAL_STATUS_VALUES = ['delivered', 'read', 'failed', 'undelivered'];
5
+ const TERMINAL_STATUS_VALUES = ['read', 'failed', 'undelivered'];
6
6
  const ERROR_SOURCE_VALUES = [null, 'twilio_sync', 'twilio_async', 'server'];
7
7
 
8
8
  const deliveryAttemptSchema = new mongoose.Schema({
@@ -0,0 +1,27 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const { getDb, getModelDatabase } = require('../config/mongoConfig');
4
+
5
+ const { logger } = require('../utils/logger');
6
+
7
+ const GLOBAL_ENTITY_CATEGORIES = Object.freeze(['STAFF', 'DOCTOR', 'INTERNAL_EMAIL', 'LOCATION', 'BANK_ACCOUNT']);
8
+
9
+ const globalEntityMapSchema = new mongoose.Schema({
10
+ category: { type: String, enum: GLOBAL_ENTITY_CATEGORIES, required: true },
11
+ encode: { type: Map, of: String, default: () => new Map() },
12
+ decode: { type: Map, of: String, default: () => new Map() },
13
+ }, { timestamps: true });
14
+
15
+ globalEntityMapSchema.index({ category: 1 }, { unique: true });
16
+
17
+ const getGlobalEntityMap = () => {
18
+ const dbName = getModelDatabase('GlobalEntityMap');
19
+ const db = dbName ? getDb(dbName) : mongoose;
20
+ logger.debug('[GlobalEntityMap] Using database', { dbName: dbName || 'default' });
21
+ return db.models.GlobalEntityMap || db.model('GlobalEntityMap', globalEntityMapSchema);
22
+ };
23
+
24
+ module.exports = {
25
+ getGlobalEntityMap,
26
+ GLOBAL_ENTITY_CATEGORIES,
27
+ };
@@ -1,18 +1,7 @@
1
1
  const mongoose = require('mongoose');
2
2
 
3
- const { Monitoreo_ID } = require('../config/airtableConfig');
4
-
5
- const { logger } = require('../utils/logger');
6
-
7
- const { getClinicalContext } = require('../helpers/patientInformationHelper');
8
-
9
- const { updateRecordByFilter } = require('../services/airtableService');
10
3
  const { DELIVERY_ATTEMPT_STATUSES } = require('./deliveryAttemptModel');
11
4
 
12
- const { Thread } = require('./threadModel');
13
-
14
- const INTERNAL_ORIGINS = new Set(['system', 'instruction']);
15
-
16
5
  const messageSchema = new mongoose.Schema({
17
6
  raw: { type: Object, default: null },
18
7
  body: { type: String, default: '' },
@@ -109,67 +98,6 @@ messageSchema.index({ triggeredBy: 1, createdAt: -1 }, { name: 'triggered_by_idx
109
98
 
110
99
  const Message = mongoose.model('Message', messageSchema);
111
100
 
112
- async function insertMessage(values) {
113
- try {
114
- if (!values.clinical_context) {
115
- const { clinicalContext} = await getClinicalContext(values.numero);
116
- values.clinical_context = clinicalContext;
117
- }
118
- const messageData = Object.fromEntries(
119
- Object.entries(values).filter(([, v]) => v !== undefined)
120
- );
121
-
122
- if (!Array.isArray(messageData.tools_executed)) {
123
- messageData.tools_executed = [];
124
- }
125
-
126
- let doc;
127
- let isNew;
128
- if (values.message_id == null) {
129
- doc = await Message.create(messageData);
130
- isNew = true;
131
- } else {
132
- const result = await Message.findOneAndUpdate(
133
- { message_id: values.message_id },
134
- { $setOnInsert: messageData },
135
- { upsert: true, new: true, setDefaultsOnInsert: true, includeResultMetadata: true }
136
- );
137
- doc = result.value;
138
- isNew = !result.lastErrorObject?.updatedExisting;
139
- }
140
-
141
- if (isNew && values.numero && !INTERNAL_ORIGINS.has(values.origin)) {
142
- Thread.findOneAndUpdate(
143
- { code: values.numero },
144
- {
145
- $set: {
146
- lastMessageAt: new Date(),
147
- lastMessageBody: values.body || '',
148
- lastMessageFromMe: !!values.from_me,
149
- lastMessageMedia: values.media || null
150
- },
151
- $inc: {
152
- messageCount: 1,
153
- ...(!values.from_me ? { unreadCount: 1 } : {})
154
- }
155
- },
156
- { upsert: true, setDefaultsOnInsert: true }
157
- ).catch(err => logger.error('[MongoStorage] Failed to denormalize thread', { numero: values.numero, error: err.message }));
158
- }
159
-
160
- updateRecordByFilter(Monitoreo_ID, 'message_monitor', `{whatsapp_id} = "${values.numero}"`, {
161
- ...(values.from_me ? { last_message_bot: values.body } : { last_message_patient: values.body, read: false }),
162
- ...(values.from_me ? { last_message_bot_time: doc.createdAt.toISOString() } : { last_message_patient_time: doc.createdAt.toISOString() })
163
- }, values.numero).catch(err => logger.error('[MongoStorage] Failed to update message_monitor table', { numero: values.numero, error: err.message }));
164
-
165
- logger.info('[MongoStorage] Message inserted or updated successfully');
166
- return { isNew, doc };
167
- } catch (err) {
168
- logger.error('[MongoStorage] Error inserting message', { error: err.message, stack: err.stack });
169
- throw err;
170
- }
171
- }
172
-
173
101
  function getMessageValues(message, content) {
174
102
  return {
175
103
  nombre_whatsapp: message.pushName,
@@ -181,29 +109,7 @@ function getMessageValues(message, content) {
181
109
  };
182
110
  }
183
111
 
184
- async function getContactDisplayName(contactNumber) {
185
- try {
186
- const latestMessage = await Message.findOne({
187
- numero: contactNumber,
188
- from_me: false
189
- })
190
- .sort({ createdAt: -1 })
191
- .select('nombre_whatsapp');
192
-
193
- if (latestMessage && latestMessage.nombre_whatsapp && latestMessage.nombre_whatsapp.trim() !== '') {
194
- return latestMessage.nombre_whatsapp;
195
- } else {
196
- return contactNumber;
197
- }
198
- } catch (error) {
199
- logger.error('[MongoStorage] Error fetching display name for ${contactNumber}:', error);
200
- return contactNumber;
201
- }
202
- }
203
-
204
112
  module.exports = {
205
113
  Message,
206
- insertMessage,
207
114
  getMessageValues,
208
- getContactDisplayName
209
115
  };
@@ -0,0 +1,28 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const { getDb, getModelDatabase } = require('../config/mongoConfig');
4
+
5
+ const { logger } = require('../utils/logger');
6
+
7
+ const PHI_CATEGORIES = Object.freeze(['PERSONA', 'TELEFONO', 'EMAIL', 'ID']);
8
+
9
+ const tokenMapSchema = new mongoose.Schema({
10
+ code: { type: String, required: true },
11
+ encode: { type: Map, of: String, default: () => new Map() },
12
+ decode: { type: Map, of: String, default: () => new Map() }
13
+ }, { timestamps: true });
14
+
15
+ tokenMapSchema.index({ code: 1 }, { unique: true });
16
+
17
+ const getTokenMap = () => {
18
+ const dbName = getModelDatabase('TokenMap');
19
+ const db = dbName ? getDb(dbName) : mongoose;
20
+ logger.debug('[TokenMap] Using database', { dbName: dbName || 'default' });
21
+ return db.models.TokenMap || db.model('TokenMap', tokenMapSchema);
22
+ };
23
+
24
+ module.exports = {
25
+ getTokenMap,
26
+ tokenMapSchema,
27
+ PHI_CATEGORIES,
28
+ };