@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.
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.68.0-dev.12085",
3
+ "version": "5.68.0-dev.12126",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",