@peopl-health/nexus 5.68.3 → 5.69.0-dev.12295

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 };
@@ -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,9 +49,20 @@ 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;
65
+ const MAX_SILENCE_DAYS = 30;
54
66
  const TRIAGE_PROTECTION_START_MINUTE = (7 * 60) + 30;
55
67
  const TRIAGE_PROTECTION_END_MINUTE = 11 * 60;
56
68
 
@@ -226,6 +238,7 @@ async function rejectionReason(candidate, {
226
238
  } = {}) {
227
239
  const opsReason = opsOptedOut(candidate, { now });
228
240
  if (opsReason) return opsReason;
241
+ if (await botIsPaused(candidate.code, { label: '[outreachSweep]' })) return 'bot_paused';
229
242
  if (!includePreviouslySwept) {
230
243
  const sweptReason = priorSweepReason(candidate);
231
244
  if (sweptReason) return sweptReason;
@@ -236,7 +249,7 @@ async function rejectionReason(candidate, {
236
249
  const recentOutbound = await getMessages(
237
250
  {
238
251
  numero: candidate.code,
239
- from_me: true,
252
+ ...CONTACT_SENT,
240
253
  createdAt: { $gt: since },
241
254
  ...(mode === 'monday_triage_recovery' && candidate.triageContentSid
242
255
  ? { content_sid: { $ne: candidate.triageContentSid } }
@@ -257,11 +270,13 @@ async function rejectionReason(candidate, {
257
270
  if (!await mondayTriageStillPending(candidate)) return 'triage_no_longer_pending';
258
271
  } else if (cadence === 'daily') {
259
272
  const inactiveSince = moment.tz(now, DEFAULT_TIMEZONE).subtract(INACTIVITY_DAYS, 'days').toDate();
260
- const recentInbound = await getMessages(
261
- { numero: candidate.code, from_me: false, createdAt: { $gte: inactiveSince } },
262
- { sort: { createdAt: -1 }, limit: 1 },
273
+ const silentSince = moment.tz(now, DEFAULT_TIMEZONE).subtract(MAX_SILENCE_DAYS, 'days').toDate();
274
+ const [lastInbound] = await getMessages(
275
+ { numero: candidate.code, from_me: false, createdAt: { $gte: silentSince } },
276
+ { select: { createdAt: 1 }, sort: { createdAt: -1 }, limit: 1 },
263
277
  );
264
- if (recentInbound?.length) return 'patient_active_within_inactivity_window';
278
+ if (!lastInbound) return 'patient_silent_beyond_max_window';
279
+ if (new Date(lastInbound.createdAt) >= inactiveSince) return 'patient_active_within_inactivity_window';
265
280
 
266
281
  const lastTriageAt = candidate?.brief?.lastTriageAt;
267
282
  if (lastTriageAt && moment.tz(lastTriageAt, DEFAULT_TIMEZONE).isSameOrAfter(inactiveSince)) {
@@ -271,7 +286,7 @@ async function rejectionReason(candidate, {
271
286
  const recentOutreach = await getMessages(
272
287
  {
273
288
  numero: candidate.code,
274
- from_me: true,
289
+ ...CONTACT_SENT,
275
290
  triggeredBy: { $regex: '^(?:seguimiento:|followup_)' },
276
291
  createdAt: { $gte: inactiveSince },
277
292
  },
@@ -283,7 +298,7 @@ async function rejectionReason(candidate, {
283
298
  const lastTwoOutreaches = await getMessages(
284
299
  {
285
300
  numero: candidate.code,
286
- from_me: true,
301
+ ...CONTACT_SENT,
287
302
  triggeredBy: { $regex: '^(?:seguimiento:|followup_)' },
288
303
  createdAt: { $gte: unansweredSince },
289
304
  },
@@ -320,7 +335,7 @@ async function routineOutreachCollisionReason(code, { now = new Date() } = {}) {
320
335
  { sort: { createdAt: -1 }, limit: 1 },
321
336
  ),
322
337
  getMessages(
323
- { numero: code, from_me: true, createdAt: { $gte: today } },
338
+ { numero: code, ...CONTACT_SENT, createdAt: { $gte: today } },
324
339
  { sort: { createdAt: -1 }, limit: 1 },
325
340
  ),
326
341
  ]);
@@ -331,7 +346,7 @@ async function routineOutreachCollisionReason(code, { now = new Date() } = {}) {
331
346
 
332
347
  async function enrichBrief(candidate) {
333
348
  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 });
349
+ const [lastOutbound] = await getMessages({ numero: candidate.code, ...CONTACT_SENT }, { sort: { createdAt: -1 }, limit: 1 });
335
350
  const fmt = (doc) => (doc?.createdAt ? moment.tz(doc.createdAt, DEFAULT_TIMEZONE).format('YYYY-MM-DD HH:mm') : null);
336
351
  return {
337
352
  ...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.69.0-dev.12295",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",