@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
|
-
|
|
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
|
-
|
|
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
|
|
261
|
-
|
|
262
|
-
{
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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) {
|
package/lib/utils/dateUtils.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
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,
|