@peopl-health/nexus 5.68.7 → 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;
|
|
@@ -1098,6 +1098,7 @@ async function settleFollowUp(snapshot, { background = true } = {}) {
|
|
|
1098
1098
|
}
|
|
1099
1099
|
|
|
1100
1100
|
async function generateBotDraft({ id, lease = null } = {}) {
|
|
1101
|
+
const startedAt = new Date();
|
|
1101
1102
|
const review = await loadReview(reviewIdOf(id));
|
|
1102
1103
|
if (review.closedAt || !DOCUMENTING_STATUSES.includes(review.status)) return { review: null, retry: false };
|
|
1103
1104
|
const latest = (review.drafts ?? []).at(-1);
|
|
@@ -1127,6 +1128,7 @@ async function generateBotDraft({ id, lease = null } = {}) {
|
|
|
1127
1128
|
}
|
|
1128
1129
|
|
|
1129
1130
|
const entry = {
|
|
1131
|
+
snapshotAt: startedAt,
|
|
1130
1132
|
doctorText: withIdentity({ identity, review, doctorText: draft.doctorText }),
|
|
1131
1133
|
patientMessage: draft.patientMessage,
|
|
1132
1134
|
writtenBy: DRAFT_OPERATOR,
|
|
@@ -1137,13 +1139,16 @@ async function generateBotDraft({ id, lease = null } = {}) {
|
|
|
1137
1139
|
...(draft.suggestCall && { suggestCall: draft.suggestCall }),
|
|
1138
1140
|
...(draft.sectionsUnavailable?.length && { sectionsUnavailable: draft.sectionsUnavailable }),
|
|
1139
1141
|
};
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
{
|
|
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: '' } },
|
|
1143
1146
|
{ new: true },
|
|
1144
1147
|
).lean();
|
|
1145
|
-
if (saved)
|
|
1146
|
-
|
|
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 };
|
|
1147
1152
|
}
|
|
1148
1153
|
|
|
1149
1154
|
async function noteFactsContext(review) {
|
|
@@ -1361,7 +1366,7 @@ async function handOffToDoctor({ id, mode, physicianEmail = null, sentBy, reques
|
|
|
1361
1366
|
if (!approver) throw httpError(403, 'The physician is not an escalation review approver');
|
|
1362
1367
|
}
|
|
1363
1368
|
|
|
1364
|
-
const draftedAt = draft
|
|
1369
|
+
const draftedAt = draftTimeOf(draft) ? new Date(draftTimeOf(draft)).getTime() : null;
|
|
1365
1370
|
if (draftedAt && (review.alarms ?? []).some((alarm) => new Date(alarm.raisedAt).getTime() > draftedAt)) {
|
|
1366
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');
|
|
1367
1372
|
}
|
|
@@ -1457,7 +1462,7 @@ async function recordDoctorDecision({
|
|
|
1457
1462
|
// Truthiness, not != null: an empty textarea is a decline with nothing to say.
|
|
1458
1463
|
if (decision === 'needs_intervention' && finalText) approvedText = requiredText(finalText, 'finalText');
|
|
1459
1464
|
|
|
1460
|
-
if (APPROVALS.includes(decision) && arrivedAfter(review, draft
|
|
1465
|
+
if (APPROVALS.includes(decision) && arrivedAfter(review, draftTimeOf(draft))) {
|
|
1461
1466
|
if (review.status === 'awaiting_doctor') {
|
|
1462
1467
|
const redraft = draft.writtenBy === DRAFT_OPERATOR ? { draftDueAt: new Date(), draftAttempts: 0 } : {};
|
|
1463
1468
|
const returned = await updateReview(reviewId, ['awaiting_doctor'], { $set: { status: 'documenting', ...redraft } }, {
|
|
@@ -1572,6 +1577,8 @@ const latestUpdateGuard = (review) => {
|
|
|
1572
1577
|
return { updates: { $size: updates.length }, [`updates.${updates.length - 1}.turnId`]: updates.at(-1).turnId };
|
|
1573
1578
|
};
|
|
1574
1579
|
|
|
1580
|
+
const draftTimeOf = (draft) => draft.snapshotAt ?? draft.createdAt;
|
|
1581
|
+
|
|
1575
1582
|
const arrivedAfter = (review, since) => Boolean(since) && [
|
|
1576
1583
|
...(review.alarms ?? []).map((alarm) => alarm.raisedAt),
|
|
1577
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;
|