@peopl-health/nexus 5.68.6 → 5.68.8
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.
|
@@ -98,6 +98,7 @@ const draftSchema = new mongoose.Schema({
|
|
|
98
98
|
missingCharacterization: { type: [String], default: undefined },
|
|
99
99
|
suggestCall: { type: suggestCallSchema, default: undefined },
|
|
100
100
|
sectionsUnavailable: { type: [String], default: undefined },
|
|
101
|
+
snapshotAt: { type: Date, default: undefined },
|
|
101
102
|
}, { timestamps: { createdAt: true, updatedAt: false } });
|
|
102
103
|
|
|
103
104
|
const doctorHandoffSchema = new mongoose.Schema({
|
|
@@ -44,7 +44,7 @@ const AUDIT_SOURCE = 'escalation_review';
|
|
|
44
44
|
const RECENT_REVIEW_LIMIT = 20;
|
|
45
45
|
const TIMELINE_LIMIT = 500;
|
|
46
46
|
const MAX_REVIEW_UPDATES = 50;
|
|
47
|
-
const DRAFT_DEBOUNCE_MS =
|
|
47
|
+
const DRAFT_DEBOUNCE_MS = 20 * 1000;
|
|
48
48
|
const DRAFT_OPERATOR = 'bot:escalationDraft';
|
|
49
49
|
const DIRECT_CHAT_ATTEMPTS = 3;
|
|
50
50
|
const RECHECK_DELAY_MS = 60 * 1000;
|
|
@@ -507,6 +507,25 @@ async function recordReviewUpdate({ code, update, now = new Date(), settleInBack
|
|
|
507
507
|
}
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
const STALE_INTERRUPTION_NOTES = 'Cerrada automáticamente al llegar una alarma nueva; nadie verificó si la operación interrumpida se completó';
|
|
511
|
+
|
|
512
|
+
async function closeStaleInterruption(EscalationReview, patientCode, now) {
|
|
513
|
+
try {
|
|
514
|
+
const stale = await EscalationReview.findOne({
|
|
515
|
+
patientCode,
|
|
516
|
+
closedAt: null,
|
|
517
|
+
status: { $in: INTERRUPTED_STATUSES },
|
|
518
|
+
updatedAt: { $lte: new Date(now.getTime() - INTERRUPTION_GRACE_MS) },
|
|
519
|
+
}, { updatedAt: 1 }).lean();
|
|
520
|
+
if (!stale) return;
|
|
521
|
+
await closeEscalationReview({
|
|
522
|
+
id: String(stale._id), closedBy: BOT_OPERATOR, interruptionNotes: STALE_INTERRUPTION_NOTES, expectedUpdatedAt: stale.updatedAt,
|
|
523
|
+
});
|
|
524
|
+
} catch (error) {
|
|
525
|
+
logger[error?.statusCode === 409 ? 'warn' : 'error']('[EscalationReview] The interrupted review was not closed before the new alarm', { error: redactedError(error) });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
510
529
|
async function recordEscalationAlarm({ code, alarm, now = new Date() } = {}) {
|
|
511
530
|
try {
|
|
512
531
|
const patientCode = patientCodeOf(code);
|
|
@@ -518,6 +537,7 @@ async function recordEscalationAlarm({ code, alarm, now = new Date() } = {}) {
|
|
|
518
537
|
.lean();
|
|
519
538
|
|
|
520
539
|
await ensureActiveReviewIndex(EscalationReview);
|
|
540
|
+
await closeStaleInterruption(EscalationReview, patientCode, now);
|
|
521
541
|
const joined = await join();
|
|
522
542
|
if (joined) {
|
|
523
543
|
await scheduleDoctorAlert(joined, entry, now);
|
|
@@ -1078,6 +1098,7 @@ async function settleFollowUp(snapshot, { background = true } = {}) {
|
|
|
1078
1098
|
}
|
|
1079
1099
|
|
|
1080
1100
|
async function generateBotDraft({ id, lease = null } = {}) {
|
|
1101
|
+
const startedAt = new Date();
|
|
1081
1102
|
const review = await loadReview(reviewIdOf(id));
|
|
1082
1103
|
if (review.closedAt || !DOCUMENTING_STATUSES.includes(review.status)) return { review: null, retry: false };
|
|
1083
1104
|
const latest = (review.drafts ?? []).at(-1);
|
|
@@ -1107,6 +1128,7 @@ async function generateBotDraft({ id, lease = null } = {}) {
|
|
|
1107
1128
|
}
|
|
1108
1129
|
|
|
1109
1130
|
const entry = {
|
|
1131
|
+
snapshotAt: startedAt,
|
|
1110
1132
|
doctorText: withIdentity({ identity, review, doctorText: draft.doctorText }),
|
|
1111
1133
|
patientMessage: draft.patientMessage,
|
|
1112
1134
|
writtenBy: DRAFT_OPERATOR,
|
|
@@ -1117,13 +1139,16 @@ async function generateBotDraft({ id, lease = null } = {}) {
|
|
|
1117
1139
|
...(draft.suggestCall && { suggestCall: draft.suggestCall }),
|
|
1118
1140
|
...(draft.sectionsUnavailable?.length && { sectionsUnavailable: draft.sectionsUnavailable }),
|
|
1119
1141
|
};
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
{
|
|
1142
|
+
const EscalationReview = getEscalationReview();
|
|
1143
|
+
const saved = await EscalationReview.findOneAndUpdate(
|
|
1144
|
+
{ _id: review._id, status: { $in: DOCUMENTING_STATUSES }, draftDueAt: { $lte: lease }, drafts: { $size: (review.drafts ?? []).length } },
|
|
1145
|
+
{ $push: { drafts: entry }, $set: { status: 'draft_ready', draftFailedAt: null }, $unset: { identityBlock: '' } },
|
|
1123
1146
|
{ new: true },
|
|
1124
1147
|
).lean();
|
|
1125
|
-
if (saved)
|
|
1126
|
-
|
|
1148
|
+
if (!saved) return { review: null, retry: false };
|
|
1149
|
+
const released = await EscalationReview.updateOne({ _id: review._id, draftDueAt: lease }, { $set: { draftDueAt: null } }, BOOKKEEPING);
|
|
1150
|
+
await audit(saved, DRAFT_OPERATOR, null, { action: 'draft_saved', draftNumber: saved.drafts.length });
|
|
1151
|
+
return { review: released.matchedCount ? { ...saved, draftDueAt: null } : saved, retry: false };
|
|
1127
1152
|
}
|
|
1128
1153
|
|
|
1129
1154
|
async function noteFactsContext(review) {
|
|
@@ -1341,7 +1366,7 @@ async function handOffToDoctor({ id, mode, physicianEmail = null, sentBy, reques
|
|
|
1341
1366
|
if (!approver) throw httpError(403, 'The physician is not an escalation review approver');
|
|
1342
1367
|
}
|
|
1343
1368
|
|
|
1344
|
-
const draftedAt = draft
|
|
1369
|
+
const draftedAt = draftTimeOf(draft) ? new Date(draftTimeOf(draft)).getTime() : null;
|
|
1345
1370
|
if (draftedAt && (review.alarms ?? []).some((alarm) => new Date(alarm.raisedAt).getTime() > draftedAt)) {
|
|
1346
1371
|
throw httpError(409, 'A new alarm arrived after this draft was written; review it and save a new draft before sending it to the doctor');
|
|
1347
1372
|
}
|
|
@@ -1437,7 +1462,7 @@ async function recordDoctorDecision({
|
|
|
1437
1462
|
// Truthiness, not != null: an empty textarea is a decline with nothing to say.
|
|
1438
1463
|
if (decision === 'needs_intervention' && finalText) approvedText = requiredText(finalText, 'finalText');
|
|
1439
1464
|
|
|
1440
|
-
if (APPROVALS.includes(decision) && arrivedAfter(review, draft
|
|
1465
|
+
if (APPROVALS.includes(decision) && arrivedAfter(review, draftTimeOf(draft))) {
|
|
1441
1466
|
if (review.status === 'awaiting_doctor') {
|
|
1442
1467
|
const redraft = draft.writtenBy === DRAFT_OPERATOR ? { draftDueAt: new Date(), draftAttempts: 0 } : {};
|
|
1443
1468
|
const returned = await updateReview(reviewId, ['awaiting_doctor'], { $set: { status: 'documenting', ...redraft } }, {
|
|
@@ -1552,6 +1577,8 @@ const latestUpdateGuard = (review) => {
|
|
|
1552
1577
|
return { updates: { $size: updates.length }, [`updates.${updates.length - 1}.turnId`]: updates.at(-1).turnId };
|
|
1553
1578
|
};
|
|
1554
1579
|
|
|
1580
|
+
const draftTimeOf = (draft) => draft.snapshotAt ?? draft.createdAt;
|
|
1581
|
+
|
|
1555
1582
|
const arrivedAfter = (review, since) => Boolean(since) && [
|
|
1556
1583
|
...(review.alarms ?? []).map((alarm) => alarm.raisedAt),
|
|
1557
1584
|
...(review.updates ?? []).map((update) => update.at),
|
|
@@ -6,7 +6,7 @@ const { getEscalationReview } = require('../models/escalationReviewModel');
|
|
|
6
6
|
const { BOOKKEEPING, DOCUMENTING_STATUSES, generateBotDraft } = require('./escalationReviewService');
|
|
7
7
|
|
|
8
8
|
const SWEEP_LIMIT = 1;
|
|
9
|
-
const DEFAULT_SWEEP_INTERVAL_MS =
|
|
9
|
+
const DEFAULT_SWEEP_INTERVAL_MS = 10 * 1000;
|
|
10
10
|
const RETRY_DELAY_MS = 5 * 60 * 1000;
|
|
11
11
|
const MAX_DRAFT_ATTEMPTS = 3;
|
|
12
12
|
const LEASE_MS = 10 * 60 * 1000;
|