@peopl-health/nexus 5.68.0-dev.12085 → 5.68.0-dev.12126
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/lib/clinical/services/clinicalAirtableService.js +0 -13
- package/lib/clinical/services/pausedSafetyNetService.js +0 -1
- package/lib/core/NexusMessaging.js +0 -5
- package/lib/models/escalationReviewModel.js +1 -0
- package/lib/services/escalationReviewService.js +2 -1
- package/lib/services/reviewDraftSweepService.js +9 -1
- package/package.json +1 -1
|
@@ -49,7 +49,6 @@ const REMINDER_TYPE_TO_AIRTABLE = {
|
|
|
49
49
|
const PENDING_ROW_FILTER = 'IF({is_active}, "true", "false") = "true", IF({is_sent}, "true", "false") = "false"';
|
|
50
50
|
const ROUTINE_OUTREACH_FILTER = 'OR({type} = "seguimiento", LEFT({schedule_id}, 9) = "recovery_")';
|
|
51
51
|
const RECOVERY_FILTER = 'LEFT({schedule_id}, 9) = "recovery_"';
|
|
52
|
-
// The fire trigger opens 5 min either side of reminder_date; a row still pending after that was not sent.
|
|
53
52
|
const FIRE_TRIGGER_GRACE_MS = 10 * 60 * 1000;
|
|
54
53
|
|
|
55
54
|
// intake.source only offers self|doctor; team_relay carries "según médico" authority.
|
|
@@ -435,8 +434,6 @@ async function hasPendingReminderForPatient({ code, withinMinutes }) {
|
|
|
435
434
|
return Array.isArray(rows) && rows.length > 0;
|
|
436
435
|
}
|
|
437
436
|
|
|
438
|
-
// A row whose fire time passed unsent was skipped and is never re-triggered, and one past its expiry
|
|
439
|
-
// can never fire; neither is a plan the agent or the create guard should see.
|
|
440
437
|
async function readPendingFollowUpsForPatient({ code, now = new Date() }) {
|
|
441
438
|
if (!code) return [];
|
|
442
439
|
const rows = await airtableService.getRecordByFilter(
|
|
@@ -460,13 +457,6 @@ function isAtOrBefore(when, now) {
|
|
|
460
457
|
return Number.isFinite(at) && at <= new Date(now).getTime();
|
|
461
458
|
}
|
|
462
459
|
|
|
463
|
-
/*
|
|
464
|
-
* Another check-in close to this one: a net's patient check-in or another routine follow-up that
|
|
465
|
-
* was sent in the last `withinMinutes`, or is still pending and due within them. The pending lower
|
|
466
|
-
* bound is the trigger's own 5-minute lead, so two rows firing together see each other; an unsent
|
|
467
|
-
* row older than that, or expired, was missed and is not coming. Patient-requested reminders are
|
|
468
|
-
* left out; they are what she asked for.
|
|
469
|
-
*/
|
|
470
460
|
async function hasCheckInNear({ code, withinMinutes, excludeReminderId, fireAt = null }) {
|
|
471
461
|
const window = Number(withinMinutes);
|
|
472
462
|
if (!code || !Number.isFinite(window) || window <= 0) return false;
|
|
@@ -479,8 +469,6 @@ async function hasCheckInNear({ code, withinMinutes, excludeReminderId, fireAt =
|
|
|
479
469
|
null,
|
|
480
470
|
['schedule_id', 'reminder_date', 'is_sent'],
|
|
481
471
|
);
|
|
482
|
-
// A sent row or a net check-in always wins. Between two pending routine rows the earlier one,
|
|
483
|
-
// then the lower id, wins, so two jobs firing together on separate instances never both stand down.
|
|
484
472
|
const ownAt = new Date(fireAt).getTime() || 0;
|
|
485
473
|
return (rows || []).some((row) => {
|
|
486
474
|
const id = String(row.schedule_id || '');
|
|
@@ -510,7 +498,6 @@ async function readPendingReminderForPatient({ code, reminderId }) {
|
|
|
510
498
|
} : null;
|
|
511
499
|
}
|
|
512
500
|
|
|
513
|
-
// Patient activity retires only turn-loss recovery rows; a seguimiento stays for the agent to keep, update or cancel.
|
|
514
501
|
async function cancelPendingFollowUps({ code, trace = null, reason = 'patient_replied', recoveryOnly = false }) {
|
|
515
502
|
return standDownRow({
|
|
516
503
|
code,
|
|
@@ -219,7 +219,6 @@ async function applyChoice(code, choice) {
|
|
|
219
219
|
const queued = await fileUrgentRequest(code, choice);
|
|
220
220
|
if (!queued) return { queued, pledge: null };
|
|
221
221
|
|
|
222
|
-
// The team now owns this patient's next contact; no turn ran to reassess a pending seguimiento.
|
|
223
222
|
await cancelPendingFollowUps({ code, reason: 'safety_net_request' }).catch((error) =>
|
|
224
223
|
logger.error('[PausedSafetyNet] Pending follow-ups were not stood down', { code, error: error.message })
|
|
225
224
|
);
|
|
@@ -608,7 +608,6 @@ class NexusMessaging {
|
|
|
608
608
|
if (chatId) {
|
|
609
609
|
// This is the common ingress for text, media, buttons, lists, and flows. Retire turn-loss
|
|
610
610
|
// recovery here, before specialized handlers split off, so all patient activity counts.
|
|
611
|
-
// A seguimiento survives: the next turn shows it to the agent, who keeps, updates or cancels it.
|
|
612
611
|
await cancelPendingFollowUps({ code: chatId, recoveryOnly: true }).catch(error =>
|
|
613
612
|
logger.error('[processIncomingMessage] Follow-up stand-down failed', { code: chatId, error: error.message })
|
|
614
613
|
);
|
|
@@ -1088,10 +1087,6 @@ class NexusMessaging {
|
|
|
1088
1087
|
return result;
|
|
1089
1088
|
}
|
|
1090
1089
|
|
|
1091
|
-
// An open net alone does not hold a seguimiento: the two ask different things. Hold only when
|
|
1092
|
-
// another check-in is about to reach the patient. Same-day activity does not hold an agent
|
|
1093
|
-
// seguimiento: the agent saw it on every turn since and kept it. The fire trigger does not come
|
|
1094
|
-
// back for a skipped row, so a skip retires it with its reason instead of leaving it armed.
|
|
1095
1090
|
const holdReason = await hasCheckInNear({ code, withinMinutes: FOLLOW_UP_SPACING_MIN, excludeReminderId: reminderId, fireAt: plan.fireAt })
|
|
1096
1091
|
? 'check_in_near'
|
|
1097
1092
|
: await followUpCollisionReason(code, reminderId, startedAt);
|
|
@@ -194,6 +194,7 @@ const escalationReviewSchema = new mongoose.Schema({
|
|
|
194
194
|
followUpDecision: { type: Number, default: null },
|
|
195
195
|
draftDueAt: { type: Date, default: null },
|
|
196
196
|
draftAttempts: { type: Number, default: 0 },
|
|
197
|
+
draftFailedAt: { type: Date, default: null },
|
|
197
198
|
doctorAlerts: { type: [doctorAlertSchema], default: [] },
|
|
198
199
|
doctorAlertDueAt: { type: Date, default: null },
|
|
199
200
|
doctorAlertKind: { type: String, enum: ['initial', 'realert', null], default: null },
|
|
@@ -1118,7 +1118,7 @@ async function generateBotDraft({ id, lease = null } = {}) {
|
|
|
1118
1118
|
};
|
|
1119
1119
|
const saved = await getEscalationReview().findOneAndUpdate(
|
|
1120
1120
|
{ _id: review._id, status: { $in: DOCUMENTING_STATUSES }, draftDueAt: lease, drafts: { $size: (review.drafts ?? []).length } },
|
|
1121
|
-
{ $push: { drafts: entry }, $set: { status: 'draft_ready', draftDueAt: null }, $unset: { identityBlock: '' } },
|
|
1121
|
+
{ $push: { drafts: entry }, $set: { status: 'draft_ready', draftDueAt: null, draftFailedAt: null }, $unset: { identityBlock: '' } },
|
|
1122
1122
|
{ new: true },
|
|
1123
1123
|
).lean();
|
|
1124
1124
|
if (saved) await audit(saved, DRAFT_OPERATOR, null, { action: 'draft_saved', draftNumber: saved.drafts.length });
|
|
@@ -1811,6 +1811,7 @@ async function syncEscalationReviewCase({ id, syncedBy, requestId = null } = {})
|
|
|
1811
1811
|
|
|
1812
1812
|
module.exports = {
|
|
1813
1813
|
BOOKKEEPING,
|
|
1814
|
+
DOCUMENTING_STATUSES,
|
|
1814
1815
|
PRE_DECISION_STATUSES,
|
|
1815
1816
|
_resetEscalationReviewIndexes,
|
|
1816
1817
|
addCallLog,
|
|
@@ -3,7 +3,7 @@ const { createIntervalSweeper } = require('../utils/intervalSweeper');
|
|
|
3
3
|
const { withTimeout } = require('../utils/timeoutUtils');
|
|
4
4
|
const { getEscalationReview } = require('../models/escalationReviewModel');
|
|
5
5
|
|
|
6
|
-
const { BOOKKEEPING, generateBotDraft } = require('./escalationReviewService');
|
|
6
|
+
const { BOOKKEEPING, DOCUMENTING_STATUSES, generateBotDraft } = require('./escalationReviewService');
|
|
7
7
|
|
|
8
8
|
const SWEEP_LIMIT = 1;
|
|
9
9
|
const DEFAULT_SWEEP_INTERVAL_MS = 30 * 1000;
|
|
@@ -34,6 +34,14 @@ async function draftReview(EscalationReview, { _id, draftDueAt, draftAttempts =
|
|
|
34
34
|
escalationReviewId: String(_id), attempts: draftAttempts + 1,
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
if (retry && !willRetry) {
|
|
38
|
+
const marked = await EscalationReview.updateOne(
|
|
39
|
+
{ _id, draftDueAt: lease, closedAt: null, status: { $in: DOCUMENTING_STATUSES } },
|
|
40
|
+
{ $set: { draftDueAt: null, draftFailedAt: new Date() } },
|
|
41
|
+
BOOKKEEPING,
|
|
42
|
+
);
|
|
43
|
+
if (marked.matchedCount) return false;
|
|
44
|
+
}
|
|
37
45
|
await EscalationReview.updateOne(
|
|
38
46
|
{ _id, draftDueAt: lease },
|
|
39
47
|
willRetry
|