@peopl-health/nexus 5.68.3 → 5.68.5

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.
@@ -1,12 +1,18 @@
1
- const { getCurrentMexicoDateTime, getMexicoDayLabels } = require('../../utils/dateUtils');
1
+ const { getCurrentMexicoDateTime, getMexicoDayLabels, getMexicoUpcomingDayLabels } = require('../../utils/dateUtils');
2
2
 
3
3
  const ANCHOR_SPAN_DAYS = 7;
4
+ const UPCOMING_SPAN_DAYS = 14;
4
5
 
5
6
  function buildDateAnchor() {
6
7
  const at = Date.now();
7
8
  const labels = getMexicoDayLabels({ days: ANCHOR_SPAN_DAYS, at });
8
9
  const week = labels.map((label, i) => (i === labels.length - 1 ? `${label} (hoy)` : label));
9
- return `${getCurrentMexicoDateTime(at)}\nLos últimos siete días: ${week.join('; ')}.`;
10
+ const upcoming = getMexicoUpcomingDayLabels({ days: UPCOMING_SPAN_DAYS, at });
11
+ return [
12
+ getCurrentMexicoDateTime(at),
13
+ `Los últimos siete días: ${week.join('; ')}.`,
14
+ `Los próximos días: ${upcoming.join('; ')}.`,
15
+ ].join('\n');
10
16
  }
11
17
 
12
18
  module.exports = { buildDateAnchor };
@@ -19,6 +19,7 @@ const { logger } = require('../../utils/logger');
19
19
  const CONFIG_TABLE = 'registry';
20
20
  const CACHE_TTL = 5 * 60 * 1000;
21
21
  const CONFIG_CACHE_KEY = 'clinicalExtractorConfig';
22
+ const ENDING_STATUSES = new Set(['paused', 'completed']);
22
23
 
23
24
  const extractorConfigCache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
24
25
 
@@ -37,6 +38,12 @@ function namesNothingSpecific(canonicalTerm, vagueWords) {
37
38
  return tokens.length > 0 && tokens.every((token) => vagueWords.has(token));
38
39
  }
39
40
 
41
+ function endsNamedLine(raw, fact) {
42
+ return fact.category === 'treatment'
43
+ && Boolean(raw.treatment_line)
44
+ && ENDING_STATUSES.has(fact.observedFields?.status);
45
+ }
46
+
40
47
  async function loadPatientContext(code) {
41
48
  try {
42
49
  const { clinicalContext, contextAsk } = await getClinicalContext(code);
@@ -115,7 +122,8 @@ async function extractClinicalMentions({ text, turnId, code, provider, detectedT
115
122
  unanchored += 1;
116
123
  return;
117
124
  }
118
- if (!mention.provenance.needsConfirmation && namesNothingSpecific(mention.fact.canonicalTerm, vagueWords)) {
125
+ if (!mention.provenance.needsConfirmation && !endsNamedLine(raw, mention.fact)
126
+ && namesNothingSpecific(mention.fact.canonicalTerm, vagueWords)) {
119
127
  forcedPending += 1;
120
128
  mention = toClinicalMention(
121
129
  { ...raw, needs_confirmation: true, missing: raw.missing?.length ? raw.missing : ['name'] },
@@ -5,6 +5,7 @@ const { Symptoms_ID } = require('../../config/airtableConfig');
5
5
  const { getMessages } = require('../../services/messageService');
6
6
  const { DEFAULT_TIMEZONE, isWithinHourWindow } = require('../../utils/scheduleUtils');
7
7
  const { logger } = require('../../utils/logger');
8
+ const { botIsPaused } = require('../../helpers/pausedThreadHelper');
8
9
  const { PATIENT_CONTACT_WINDOW } = require('./contingencyDispatchService');
9
10
  const { hasPendingReminderForPatient } = require('./clinicalAirtableService');
10
11
 
@@ -48,6 +49,16 @@ const AUDIT = {
48
49
  turnId: 'seguimiento_turn_id',
49
50
  };
50
51
 
52
+ const UNDELIVERED_STATUSES = ['failed', 'undelivered'];
53
+ const CONTACT_SENT = {
54
+ from_me: true,
55
+ origin: { $nin: ['instruction', 'system'] },
56
+ $or: [
57
+ { 'statusInfo.status': { $nin: UNDELIVERED_STATUSES } },
58
+ { 'statusInfo.recoveryTemplateSid': { $ne: null }, 'statusInfo.recoverySentAt': null },
59
+ ],
60
+ };
61
+
51
62
  const RECENT_OUTBOUND_HOURS = 24;
52
63
  const COLLIDING_REMINDER_MINUTES = 24 * 60;
53
64
  const INACTIVITY_DAYS = 7;
@@ -226,6 +237,7 @@ async function rejectionReason(candidate, {
226
237
  } = {}) {
227
238
  const opsReason = opsOptedOut(candidate, { now });
228
239
  if (opsReason) return opsReason;
240
+ if (await botIsPaused(candidate.code, { label: '[outreachSweep]' })) return 'bot_paused';
229
241
  if (!includePreviouslySwept) {
230
242
  const sweptReason = priorSweepReason(candidate);
231
243
  if (sweptReason) return sweptReason;
@@ -236,7 +248,7 @@ async function rejectionReason(candidate, {
236
248
  const recentOutbound = await getMessages(
237
249
  {
238
250
  numero: candidate.code,
239
- from_me: true,
251
+ ...CONTACT_SENT,
240
252
  createdAt: { $gt: since },
241
253
  ...(mode === 'monday_triage_recovery' && candidate.triageContentSid
242
254
  ? { content_sid: { $ne: candidate.triageContentSid } }
@@ -271,7 +283,7 @@ async function rejectionReason(candidate, {
271
283
  const recentOutreach = await getMessages(
272
284
  {
273
285
  numero: candidate.code,
274
- from_me: true,
286
+ ...CONTACT_SENT,
275
287
  triggeredBy: { $regex: '^(?:seguimiento:|followup_)' },
276
288
  createdAt: { $gte: inactiveSince },
277
289
  },
@@ -283,7 +295,7 @@ async function rejectionReason(candidate, {
283
295
  const lastTwoOutreaches = await getMessages(
284
296
  {
285
297
  numero: candidate.code,
286
- from_me: true,
298
+ ...CONTACT_SENT,
287
299
  triggeredBy: { $regex: '^(?:seguimiento:|followup_)' },
288
300
  createdAt: { $gte: unansweredSince },
289
301
  },
@@ -320,7 +332,7 @@ async function routineOutreachCollisionReason(code, { now = new Date() } = {}) {
320
332
  { sort: { createdAt: -1 }, limit: 1 },
321
333
  ),
322
334
  getMessages(
323
- { numero: code, from_me: true, createdAt: { $gte: today } },
335
+ { numero: code, ...CONTACT_SENT, createdAt: { $gte: today } },
324
336
  { sort: { createdAt: -1 }, limit: 1 },
325
337
  ),
326
338
  ]);
@@ -331,7 +343,7 @@ async function routineOutreachCollisionReason(code, { now = new Date() } = {}) {
331
343
 
332
344
  async function enrichBrief(candidate) {
333
345
  const [lastInbound] = await getMessages({ numero: candidate.code, from_me: false }, { sort: { createdAt: -1 }, limit: 1 });
334
- const [lastOutbound] = await getMessages({ numero: candidate.code, from_me: true }, { sort: { createdAt: -1 }, limit: 1 });
346
+ const [lastOutbound] = await getMessages({ numero: candidate.code, ...CONTACT_SENT }, { sort: { createdAt: -1 }, limit: 1 });
335
347
  const fmt = (doc) => (doc?.createdAt ? moment.tz(doc.createdAt, DEFAULT_TIMEZONE).format('YYYY-MM-DD HH:mm') : null);
336
348
  return {
337
349
  ...candidate.brief,
@@ -20,6 +20,7 @@ const { isWithin24HourWindow } = require('../helpers/conversationWindowHelper');
20
20
  const { maskCaregiverLabel, remapCaregiverSender, replyContactFor } = require('../helpers/caregiverHelper');
21
21
  const { triggerTemplateRecovery } = require('../helpers/templateRecoveryHelper');
22
22
  const { prepareMessageMedia } = require('../helpers/mediaHelper');
23
+ const { botIsPaused } = require('../helpers/pausedThreadHelper');
23
24
  const { acquireSendClaim, completeSendClaim, releaseSendClaim } = require('../helpers/sendClaimHelper');
24
25
 
25
26
  const { createMessagingProvider } = require('../adapters/registry');
@@ -490,6 +491,10 @@ class NexusMessaging {
490
491
  logger.info('[NexusMessaging] proactive send cancelled — patient became active', { code: messageData.code });
491
492
  return { success: false, messageId: null, status: 'patient_active', delivered: false, deferred: false };
492
493
  }
494
+ if (await botIsPaused(messageData.code, { label: '[NexusMessaging]' })) {
495
+ logger.info('[NexusMessaging] proactive send cancelled — bot paused by the team', { code: messageData.code });
496
+ return { success: false, messageId: null, status: 'bot_paused', delivered: false, deferred: false };
497
+ }
493
498
  }
494
499
  if (!this.provider) throw new Error('No provider initialized');
495
500
 
@@ -1027,6 +1032,9 @@ class NexusMessaging {
1027
1032
  if (delivery?.status === 'patient_active') {
1028
1033
  return { decision: 'skipped', rationale: 'la paciente escribió durante la decisión', holdReason: 'patient_active', nextCheckAt: null, draft: null, accepted: false, delivered: false, deferred: false, deliveryStatus: 'patient_active', turnId: runResult.turnId || null, traceId: runResult.traceId || null };
1029
1034
  }
1035
+ if (delivery?.status === 'bot_paused') {
1036
+ return { decision: 'skipped', rationale: 'el equipo pausó el bot durante la decisión', holdReason: 'bot_paused', nextCheckAt: null, draft: null, accepted: false, delivered: false, deferred: false, deliveryStatus: 'bot_paused', turnId: runResult.turnId || null, traceId: runResult.traceId || null };
1037
+ }
1030
1038
  const accepted = Boolean(runResult.output) && delivery?.success === true && delivery?.deferred !== true;
1031
1039
  const delivered = ['delivered', 'read'].includes(String(delivery?.status || '').toLowerCase());
1032
1040
  if (!dryRun && (decision.decision === 'write') !== accepted) {
@@ -6,13 +6,20 @@ const nowInMexico = (at) => moment(at).tz(MEXICO_TZ).locale('es');
6
6
 
7
7
  const getCurrentMexicoDateTime = (at) => nowInMexico(at).format(MEXICO_STAMP);
8
8
 
9
+ const dayLabel = (day, now) => (day.year() === now.year() ? day.format('dddd D [de] MMMM') : day.format('dddd D [de] MMMM [de] YYYY'));
10
+
9
11
  const getMexicoDayLabels = ({ days, at }) => {
10
12
  const now = nowInMexico(at);
11
13
  const labels = [];
12
- for (let back = days - 1; back >= 0; back -= 1) {
13
- const at = now.clone().subtract(back, 'days');
14
- labels.push(at.year() === now.year() ? at.format('dddd D [de] MMMM') : at.format('dddd D [de] MMMM [de] YYYY'));
15
- }
14
+ for (let back = days - 1; back >= 0; back -= 1) labels.push(dayLabel(now.clone().subtract(back, 'days'), now));
15
+ return labels;
16
+ };
17
+
18
+ // The days after today, nearest first; today itself is not included.
19
+ const getMexicoUpcomingDayLabels = ({ days, at }) => {
20
+ const now = nowInMexico(at);
21
+ const labels = [];
22
+ for (let ahead = 1; ahead <= days; ahead += 1) labels.push(dayLabel(now.clone().add(ahead, 'days'), now));
16
23
  return labels;
17
24
  };
18
25
 
@@ -83,6 +90,7 @@ module.exports = {
83
90
  addWorkingMinutes,
84
91
  getCurrentMexicoDateTime,
85
92
  getMexicoDayLabels,
93
+ getMexicoUpcomingDayLabels,
86
94
  parseDate,
87
95
  toIsoInstant,
88
96
  ageInYears,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.68.3",
3
+ "version": "5.68.5",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",