@peopl-health/nexus 5.52.0-dev.7695 → 5.52.0-dev.7705

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.
@@ -4,6 +4,7 @@ const { redactDirectIdentifiers } = require('../utils/sanitizerUtils');
4
4
  const {
5
5
  addCallLog,
6
6
  addDraft,
7
+ cancelEscalationReview,
7
8
  closeEscalationReview,
8
9
  getEscalationReviewById,
9
10
  handOffToDoctor,
@@ -135,6 +136,16 @@ const closeEscalationReviewController = async (req, res) => {
135
136
  }
136
137
  };
137
138
 
139
+ const cancelEscalationReviewController = async (req, res) => {
140
+ try {
141
+ const { triggeredBy, reason = null } = req.body || {};
142
+ const review = await cancelEscalationReview({ id: req.params.id, cancelledBy: triggeredBy, reason, requestId: req.requestId });
143
+ res.status(200).json({ success: true, review });
144
+ } catch (error) {
145
+ respondWithError(res, error, 'cancelling escalation review', { reviewId: req.params?.id });
146
+ }
147
+ };
148
+
138
149
  const syncEscalationReviewCaseController = async (req, res) => {
139
150
  try {
140
151
  const { triggeredBy } = req.body || {};
@@ -152,6 +163,7 @@ const syncEscalationReviewCaseController = async (req, res) => {
152
163
  module.exports = {
153
164
  addEscalationReviewCallLogController,
154
165
  addEscalationReviewDraftController,
166
+ cancelEscalationReviewController,
155
167
  closeEscalationReviewController,
156
168
  getEscalationReviewController,
157
169
  handOffEscalationReviewController,
@@ -7,6 +7,7 @@ const { logger } = require('../utils/logger');
7
7
  const CALL_OUTCOMES = ['contacted', 'no_answer', 'voicemail'];
8
8
  const HANDOFF_MODES = ['sent_whatsapp', 'copied'];
9
9
  const INTERRUPTED_STATUSES = ['handing_off', 'sending'];
10
+ const CANCELLABLE_STATUSES = ['open', 'documenting', 'draft_ready'];
10
11
  const DOCTOR_DECISIONS = ['approved', 'edited_approved', 'needs_intervention', 'returned'];
11
12
  const DECISION_SOURCES = ['platform', 'registered_by_navigator'];
12
13
 
@@ -83,6 +84,7 @@ const escalationReviewSchema = new mongoose.Schema({
83
84
  'indications_queued',
84
85
  'indications_sent',
85
86
  'closed',
87
+ 'cancelled',
86
88
  ],
87
89
  default: 'open',
88
90
  },
@@ -100,6 +102,7 @@ const escalationReviewSchema = new mongoose.Schema({
100
102
 
101
103
  escalationReviewSchema.index({ patientCode: 1, createdAt: -1 });
102
104
  escalationReviewSchema.index({ activeCaseKey: 1 }, { unique: true, sparse: true });
105
+ escalationReviewSchema.index({ patientCode: 1 }, { unique: true, partialFilterExpression: { closedAt: null } });
103
106
 
104
107
  const getEscalationReview = () => {
105
108
  const dbName = getModelDatabase('EscalationReview');
@@ -110,6 +113,7 @@ const getEscalationReview = () => {
110
113
 
111
114
  module.exports = {
112
115
  CALL_OUTCOMES,
116
+ CANCELLABLE_STATUSES,
113
117
  DECISION_SOURCES,
114
118
  DOCTOR_DECISIONS,
115
119
  HANDOFF_MODES,
@@ -126,6 +126,7 @@ const escalationReviewRouteDefinitions = {
126
126
  'POST /:id/decisions': 'recordEscalationReviewDecisionController',
127
127
  'POST /:id/patient-deliveries': 'sendEscalationReviewIndicationsController',
128
128
  'POST /:id/close': 'closeEscalationReviewController',
129
+ 'POST /:id/cancel': 'cancelEscalationReviewController',
129
130
  'POST /:id/case-sync': 'syncEscalationReviewCaseController'
130
131
  };
131
132
 
@@ -285,6 +286,7 @@ const builtInControllers = {
285
286
  recordEscalationReviewDecisionController: escalationReviewController.recordEscalationReviewDecisionController,
286
287
  sendEscalationReviewIndicationsController: escalationReviewController.sendEscalationReviewIndicationsController,
287
288
  closeEscalationReviewController: escalationReviewController.closeEscalationReviewController,
289
+ cancelEscalationReviewController: escalationReviewController.cancelEscalationReviewController,
288
290
  syncEscalationReviewCaseController: escalationReviewController.syncEscalationReviewCaseController
289
291
  };
290
292
 
@@ -6,6 +6,7 @@ const { logger } = require('../utils/logger');
6
6
  const { PATIENT_CODE, redactDirectIdentifiers } = require('../utils/sanitizerUtils');
7
7
  const {
8
8
  CALL_OUTCOMES,
9
+ CANCELLABLE_STATUSES,
9
10
  DECISION_SOURCES,
10
11
  DOCTOR_DECISIONS,
11
12
  HANDOFF_MODES,
@@ -248,6 +249,24 @@ const deliveryTransport = (message, result) => {
248
249
  return result.deferred ? 'template_recovery' : 'text';
249
250
  };
250
251
 
252
+ let indexesReady = new WeakMap();
253
+
254
+ function ensureActiveReviewIndex(EscalationReview) {
255
+ const pending = indexesReady.get(EscalationReview);
256
+ if (pending) return pending;
257
+
258
+ const building = EscalationReview.ensureIndexes().catch((error) => {
259
+ indexesReady.delete(EscalationReview);
260
+ if (error?.code !== DUPLICATE_KEY_ERROR) throw error;
261
+ logger.error('[EscalationReview] A uniqueness index could not be built', { error: redactedError(error) });
262
+ throw httpError(409, 'Escalation reviews cannot be opened: a uniqueness index could not be built. The server log names the index and the conflict; resolve it and retry.');
263
+ });
264
+ indexesReady.set(EscalationReview, building);
265
+ return building;
266
+ }
267
+
268
+ const _resetEscalationReviewIndexes = () => { indexesReady = new WeakMap(); };
269
+
251
270
  async function openEscalationReview({ code, patientCaseRecordId = null, openedBy, requestId = null } = {}) {
252
271
  const patientCode = patientCodeOf(code);
253
272
  const operator = requiredText(openedBy, 'openedBy');
@@ -264,14 +283,14 @@ async function openEscalationReview({ code, patientCaseRecordId = null, openedBy
264
283
  }
265
284
 
266
285
  const EscalationReview = getEscalationReview();
267
- await EscalationReview.init();
286
+ await ensureActiveReviewIndex(EscalationReview);
268
287
  let review;
269
288
  try {
270
289
  review = (await EscalationReview.create(fields)).toObject();
271
290
  } catch (error) {
272
- if (error?.code !== DUPLICATE_KEY_ERROR || !fields.activeCaseKey) throw error;
273
- const existing = await EscalationReview.findOne({ activeCaseKey: fields.activeCaseKey }).lean();
274
- if (!existing) throw httpError(409, 'The patient case review changed while opening; try again');
291
+ if (error?.code !== DUPLICATE_KEY_ERROR) throw error;
292
+ const existing = await EscalationReview.findOne({ patientCode, closedAt: null }).lean();
293
+ if (!existing) throw httpError(409, 'A closed review still holds this patient case; try again in a moment');
275
294
  return { review: existing, created: false };
276
295
  }
277
296
  await audit(review, operator, requestId, { action: 'opened', patientCaseRecordId });
@@ -524,6 +543,17 @@ async function closeEscalationReview({ id, closedBy, deliveryConfirmed = false,
524
543
  return syncCase(review, operator, requestId);
525
544
  }
526
545
 
546
+ async function cancelEscalationReview({ id, cancelledBy, reason = null, requestId = null } = {}) {
547
+ const reviewId = reviewIdOf(id);
548
+ const operator = requiredText(cancelledBy, 'cancelledBy');
549
+ const review = await updateReview(reviewId, CANCELLABLE_STATUSES, {
550
+ $set: { status: 'cancelled', closedBy: operator, closedAt: new Date() },
551
+ $unset: { activeCaseKey: '' },
552
+ });
553
+ await audit(review, operator, requestId, { action: 'cancelled', reason: optionalText(reason) });
554
+ return review;
555
+ }
556
+
527
557
  async function syncEscalationReviewCase({ id, syncedBy, requestId = null } = {}) {
528
558
  const reviewId = reviewIdOf(id);
529
559
  const operator = requiredText(syncedBy, 'syncedBy');
@@ -531,8 +561,10 @@ async function syncEscalationReviewCase({ id, syncedBy, requestId = null } = {})
531
561
  }
532
562
 
533
563
  module.exports = {
564
+ _resetEscalationReviewIndexes,
534
565
  addCallLog,
535
566
  addDraft,
567
+ cancelEscalationReview,
536
568
  closeEscalationReview,
537
569
  getEscalationReviewById,
538
570
  handOffToDoctor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.52.0-dev.7695",
3
+ "version": "5.52.0-dev.7705",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",