@peopl-health/nexus 5.69.0-dev.12621 → 5.69.0-dev.12623

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.
@@ -1,11 +1,12 @@
1
1
  const moment = require('moment-timezone');
2
2
 
3
3
  const airtableService = require('../../services/airtableService');
4
- const { Monitoreo_ID, Symptoms_ID } = require('../../config/airtableConfig');
4
+ const { Symptoms_ID } = require('../../config/airtableConfig');
5
5
  const { getMessages } = require('../../services/messageService');
6
6
  const { DEFAULT_TIMEZONE, isWithinHourWindow } = require('../../utils/scheduleUtils');
7
7
  const { logger } = require('../../utils/logger');
8
8
  const { botIsPaused } = require('../../helpers/pausedThreadHelper');
9
+ const { Thread } = require('../../models/threadModel');
9
10
  const { PATIENT_CONTACT_WINDOW } = require('./contingencyDispatchService');
10
11
  const { hasPendingReminderForPatient } = require('./clinicalAirtableService');
11
12
  const { WriteClaimStore } = require('./writeClaimStore');
@@ -51,27 +52,9 @@ const AUDIT = {
51
52
  };
52
53
 
53
54
  const WEEKLY_SOURCE = 'weekly';
54
- const MONITOR_SOURCE = 'message_monitor';
55
- const SOURCES = [WEEKLY_SOURCE, MONITOR_SOURCE];
55
+ const SCOPE_SOURCE = 'patient_scope';
56
56
 
57
- const MONITOR_TABLE = 'message_monitor';
58
- const MONITOR_F = {
59
- whatsapp: 'whatsapp_id',
60
- lastInbound: 'last_message_patient_time',
61
- };
62
-
63
- const PATIENT_TABLE = 'estado_general';
64
- const PATIENT_F = {
65
- whatsapp: 'whatsapp_id',
66
- firstName: 'first_name',
67
- disableFollowUp: 'desactivar_seguimiento_48hrs',
68
- };
69
-
70
- const CANONICAL_CODE = /^whatsapp:\+\d{6,}$/;
71
-
72
- // The monitor lane has no questionnaire row to stamp, so its weekly claim lives in Mongo, where a
73
- // unique key makes it atomic and an unavailable store fails closed.
74
- const sweepClaims = new WriteClaimStore({ namespace: 'outreach_sweep', ttlMs: 14 * 24 * 60 * 60 * 1000 });
57
+ const sweepClaims = new WriteClaimStore({ namespace: 'outreach_sweep', ttlMs: 8 * 24 * 60 * 60 * 1000 });
75
58
 
76
59
  const UNDELIVERED_STATUSES = ['failed', 'undelivered'];
77
60
  const CONTACT_SENT = {
@@ -227,64 +210,18 @@ function buildCandidates(rows, { now, cadence = 'daily', mode = 'continuity' })
227
210
  return { candidates, targetDays, weekday, cadence, mode };
228
211
  }
229
212
 
230
- function airtableInstant(date) {
231
- return `DATETIME_PARSE("${date.toISOString()}")`;
232
- }
233
-
234
- function monitorPrefilter(now) {
235
- const oldest = moment.tz(now, DEFAULT_TIMEZONE).subtract(MAX_SILENCE_DAYS + 1, 'days').toDate();
236
- const newest = moment.tz(now, DEFAULT_TIMEZONE).subtract(INACTIVITY_DAYS - 1, 'days').toDate();
237
- return `AND(IS_AFTER({${MONITOR_F.lastInbound}}, ${airtableInstant(oldest)}), `
238
- + `IS_BEFORE({${MONITOR_F.lastInbound}}, ${airtableInstant(newest)}))`;
239
- }
240
-
241
- // A number can carry several monitor rows; any one of them outside prod, outside the programme or
242
- // paused rules the number out, so a stale duplicate cannot admit it.
243
- const MONITOR_DENY = 'OR({tipo} != "prod", NOT({is_program}), {stopped})';
244
-
245
- /* Patients the weekly lane does not own: anyone in the questionnaire view, or with a questionnaire
246
- * row this week, belongs to that lane and its stamps. Names come from estado_general because the
247
- * monitor's own name is a formula that falls back to "Desconocido". A number carrying two names is
248
- * a shared handset, and a message addressed to one of them is skipped rather than guessed. */
249
- function buildMonitorCandidates({ monitorRows, patientRows, excludedCodes }) {
250
- const newestByCode = new Map();
251
- for (const row of monitorRows) {
252
- const code = String(first(row[MONITOR_F.whatsapp]) || '').trim();
253
- if (!CANONICAL_CODE.test(code) || excludedCodes.has(code)) continue;
254
- const seen = newestByCode.get(code);
255
- if (!seen || new Date(row[MONITOR_F.lastInbound] || 0) > new Date(seen[MONITOR_F.lastInbound] || 0)) {
256
- newestByCode.set(code, row);
257
- }
258
- }
259
-
260
- const patientsByCode = new Map();
261
- for (const row of patientRows) {
262
- const code = String(first(row[PATIENT_F.whatsapp]) || '').trim();
263
- if (!newestByCode.has(code)) continue;
264
- if (!patientsByCode.has(code)) patientsByCode.set(code, { names: new Map(), followUpDisabled: false });
265
- const patient = patientsByCode.get(code);
266
- const name = String(first(row[PATIENT_F.firstName]) || '').trim();
267
- if (name) patient.names.set(name.toLowerCase().replace(/\s+/g, ' '), name);
268
- if (row[PATIENT_F.disableFollowUp] === true) patient.followUpDisabled = true;
269
- }
270
-
213
+ function buildScopeCandidates({ threads, excludedCodes }) {
271
214
  const candidates = [];
272
- for (const [code, row] of newestByCode) {
273
- const patient = patientsByCode.get(code);
274
- if (!patient || patient.names.size !== 1) {
275
- logger.info('[outreachSweep] monitor candidate skipped — no single patient name', {
276
- recordId: row._recordId, names: patient?.names.size || 0,
277
- });
278
- continue;
279
- }
215
+ for (const thread of new Map(threads.map((t) => [t.code, t])).values()) {
216
+ const name = String(thread.nombre || '').trim().split(/\s+/)[0];
217
+ if (excludedCodes.has(thread.code) || !name) continue;
280
218
  candidates.push({
281
- source: MONITOR_SOURCE,
282
- patientId: null,
283
- recordId: row._recordId,
284
- code,
285
- name: [...patient.names.values()][0],
219
+ source: SCOPE_SOURCE,
220
+ patientId: thread.patient_id || null,
221
+ recordId: String(thread._id),
222
+ code: thread.code,
223
+ name,
286
224
  reason: 'no_triage_history',
287
- followUpDisabled: patient.followUpDisabled,
288
225
  rows: [],
289
226
  brief: {
290
227
  reason: 'no hay un triaje contestado que sirva como ancla clínica',
@@ -297,36 +234,24 @@ function buildMonitorCandidates({ monitorRows, patientRows, excludedCodes }) {
297
234
  return candidates;
298
235
  }
299
236
 
300
- async function loadMonitorCandidates({ now, viewRows }) {
301
- const weekStart = startOfLocalWeek(now).toDate();
302
- const [monitorRows, deniedRows, patientRows, weekRows] = await Promise.all([
237
+ async function loadScopeCandidates({ now, viewRows, patientScope }) {
238
+ const weekStart = startOfLocalWeek(now).toISOString();
239
+ const [threads, weekRows] = await Promise.all([
240
+ Thread.find({ code: { $in: patientScope } }, { code: 1, nombre: 1, patient_id: 1 }).lean(),
303
241
  airtableService.getRecordByFilter(
304
- Monitoreo_ID, MONITOR_TABLE, monitorPrefilter(now), null, Object.values(MONITOR_F), true,
305
- ),
306
- airtableService.getRecordByFilter(Monitoreo_ID, MONITOR_TABLE, MONITOR_DENY, null, [MONITOR_F.whatsapp]),
307
- airtableService.getRecordByFilter(Symptoms_ID, PATIENT_TABLE, 'TRUE()', null, Object.values(PATIENT_F)),
308
- airtableService.getRecordByFilter(
309
- Symptoms_ID, TABLE, `NOT(IS_BEFORE({${F.createdAt}}, ${airtableInstant(weekStart)}))`, null, [F.whatsapp],
242
+ Symptoms_ID, TABLE, `NOT(IS_BEFORE({${F.createdAt}}, DATETIME_PARSE("${weekStart}")))`, null, [F.whatsapp],
310
243
  ),
311
244
  ]);
312
- const excludedCodes = new Set([
313
- ...[...viewRows, ...(weekRows || [])].map((row) => first(row[F.whatsapp])),
314
- ...(deniedRows || []).map((row) => first(row[MONITOR_F.whatsapp])),
315
- ].map((code) => String(code || '').trim()).filter(Boolean));
316
- return buildMonitorCandidates({ monitorRows: monitorRows || [], patientRows: patientRows || [], excludedCodes });
245
+ const excludedCodes = new Set([...viewRows, ...(weekRows || [])]
246
+ .map((row) => String(first(row[F.whatsapp]) || '').trim()));
247
+ return buildScopeCandidates({ threads: threads || [], excludedCodes });
317
248
  }
318
249
 
319
250
  function sweepClaimKey(candidate, now) {
320
251
  return `${candidate.code}:${startOfLocalWeek(now).format('YYYY-MM-DD')}`;
321
252
  }
322
253
 
323
- function sweepReasonFromStamps(stamps) {
324
- if (stamps.some((stamp) => stamp.endsWith('_en curso'))) return 'stuck_in_progress';
325
- return stamps.length ? 'already_swept_this_week' : null;
326
- }
327
-
328
254
  function opsOptedOut(candidate, { now }) {
329
- if (candidate.followUpDisabled) return 'patient_follow_up_disabled';
330
255
  const weekStart = startOfLocalWeek(now);
331
256
  for (const row of candidate.rows) {
332
257
  if (first(row[F.disableFollowUp]) === true) return 'patient_follow_up_disabled';
@@ -340,12 +265,9 @@ function opsOptedOut(candidate, { now }) {
340
265
  // Patient-scoped, not row-scoped: a patient with two questionnaires this week is one person, and
341
266
  // the old automation's per-row stamp is what let her be contacted twice.
342
267
  function priorSweepReason(candidate) {
343
- return sweepReasonFromStamps(candidate.rows.map((row) => String(row[F.review] || '').trim()).filter(Boolean));
344
- }
345
-
346
- async function priorMonitorSweepReason(candidate, { now }) {
347
- const claim = await sweepClaims.get(sweepClaimKey(candidate, now));
348
- return sweepReasonFromStamps(claim?.stamp ? [claim.stamp] : []);
268
+ const stamps = candidate.rows.map((row) => String(row[F.review] || '').trim()).filter(Boolean);
269
+ if (stamps.some((stamp) => stamp.endsWith('_en curso'))) return 'stuck_in_progress';
270
+ return stamps.length ? 'already_swept_this_week' : null;
349
271
  }
350
272
 
351
273
  async function mondayTriageStillPending(candidate) {
@@ -364,10 +286,11 @@ async function rejectionReason(candidate, {
364
286
  } = {}) {
365
287
  const opsReason = opsOptedOut(candidate, { now });
366
288
  if (opsReason) return opsReason;
289
+ if (await Thread.exists({ code: candidate.code, outreachOptOut: true })) return 'patient_follow_up_disabled';
367
290
  if (await botIsPaused(candidate.code, { label: '[outreachSweep]' })) return 'bot_paused';
368
291
  if (!includePreviouslySwept) {
369
- const sweptReason = candidate.source === MONITOR_SOURCE
370
- ? await priorMonitorSweepReason(candidate, { now })
292
+ const sweptReason = candidate.source === SCOPE_SOURCE
293
+ ? (await sweepClaims.has(sweepClaimKey(candidate, now)) ? 'already_swept_this_week' : null)
371
294
  : priorSweepReason(candidate);
372
295
  if (sweptReason) return sweptReason;
373
296
 
@@ -488,14 +411,9 @@ async function enrichBrief(candidate) {
488
411
  // patient uncontacted for the week, which is the safe direction: a free-text send carries no
489
412
  // contentSid and no idempotency key, so nothing downstream would catch a duplicate.
490
413
  async function claimCandidate({ candidate, weekdayLabel, dryRun, now }) {
491
- if (dryRun) return null;
492
- if (candidate.source === MONITOR_SOURCE) {
493
- const claim = await sweepClaims.acquire(sweepClaimKey(candidate, now), {
494
- stamp: `${weekdayLabel}_en curso`, recordId: candidate.recordId,
495
- });
496
- if (claim === 'acquired') return null;
497
- logger.info('[outreachSweep] monitor claim not acquired', { recordId: candidate.recordId, claim });
498
- return claim === 'taken' ? 'claim_lost' : 'claim_unavailable';
414
+ if (dryRun) return true;
415
+ if (candidate.source === SCOPE_SOURCE) {
416
+ return (await sweepClaims.acquire(sweepClaimKey(candidate, now))) === 'acquired';
499
417
  }
500
418
  try {
501
419
  const claimed = await airtableService.updateRecordByFilter(
@@ -507,39 +425,27 @@ async function claimCandidate({ candidate, weekdayLabel, dryRun, now }) {
507
425
  );
508
426
  if (!claimed?.length) {
509
427
  logger.info('[outreachSweep] candidate already claimed by another run', { recordId: candidate.recordId });
510
- return 'claim_lost';
428
+ return false;
511
429
  }
512
- return null;
430
+ return true;
513
431
  } catch (error) {
514
432
  logger.warn('[outreachSweep] claim failed; skipping rather than risking a double send', {
515
433
  recordId: candidate.recordId, error: error.message,
516
434
  });
517
- return 'claim_lost';
435
+ return false;
518
436
  }
519
437
  }
520
438
 
521
- // Three outcomes, not two: a `write` whose delivery never happened is neither "escrito" nor a
522
- // deliberate hold, and collapsing it into either would hide the failure from the ops queue.
523
- function outcomeStamp(outcome, weekdayLabel) {
524
- if (outcome.error) return `${weekdayLabel}_error`;
525
- if (outcome.delivered) return `${weekdayLabel}_entregado`;
526
- if (outcome.accepted) return `${weekdayLabel}_enviado`;
527
- if (outcome.decision === 'write') return `${weekdayLabel}_no entregado`;
528
- return `${weekdayLabel}_omitido`;
529
- }
530
-
531
- async function recordOutcome({ candidate, outcome, weekdayLabel, dryRun, now }) {
532
- if (dryRun) return;
533
- const stamp = outcomeStamp(outcome, weekdayLabel);
534
- if (candidate.source === MONITOR_SOURCE) {
535
- await sweepClaims.set(sweepClaimKey(candidate, now), {
536
- stamp,
537
- recordId: candidate.recordId,
538
- rationale: outcome.rationale || outcome.error || '',
539
- turnId: outcome.turnId || '',
540
- });
541
- return;
542
- }
439
+ async function recordOutcome({ candidate, outcome, weekdayLabel, dryRun }) {
440
+ if (dryRun || candidate.source === SCOPE_SOURCE) return;
441
+ // Three outcomes, not two: a `write` whose delivery never happened is neither "escrito" nor a
442
+ // deliberate hold, and collapsing it into either would hide the failure from the ops queue.
443
+ let stamp;
444
+ if (outcome.error) stamp = `${weekdayLabel}_error`;
445
+ else if (outcome.delivered) stamp = `${weekdayLabel}_entregado`;
446
+ else if (outcome.accepted) stamp = `${weekdayLabel}_enviado`;
447
+ else if (outcome.decision === 'write') stamp = `${weekdayLabel}_no entregado`;
448
+ else stamp = `${weekdayLabel}_omitido`;
543
449
  const where = `RECORD_ID() = "${candidate.recordId}"`;
544
450
  await airtableService.updateRecordByFilter(Symptoms_ID, TABLE, where, {
545
451
  [F.review]: stamp,
@@ -592,19 +498,16 @@ async function runOutreachSweep({
592
498
  if (!['continuity', 'monday_triage_recovery'].includes(mode)) {
593
499
  throw new Error('mode must be continuity or monday_triage_recovery');
594
500
  }
595
- if (!SOURCES.includes(source)) {
596
- throw new Error('source must be weekly or message_monitor');
501
+ if (![WEEKLY_SOURCE, SCOPE_SOURCE].includes(source)) {
502
+ throw new Error('source must be weekly or patient_scope');
597
503
  }
598
- // message_monitor also holds AMI and inactive patients; only the consumer's scope says who is in
599
- // its programme and active, so this lane never runs unscoped.
600
- if (source === MONITOR_SOURCE
601
- && (mode !== 'continuity' || cadence !== 'daily' || !Array.isArray(patientScope))) {
602
- throw new Error('the message_monitor source requires continuity mode, daily cadence and a patientScope');
504
+ if (source === SCOPE_SOURCE && (mode !== 'continuity' || cadence !== 'daily' || !Array.isArray(patientScope))) {
505
+ throw new Error('the patient_scope source requires continuity mode, daily cadence and a patientScope');
603
506
  }
604
507
 
605
508
  const localWeekday = moment.tz(now, DEFAULT_TIMEZONE).isoWeekday();
606
509
  if (mode === 'monday_triage_recovery' && localWeekday !== 1) {
607
- return { ran: false, weekday: localWeekday, source, reason: 'not_monday', counts: {} };
510
+ return { ran: false, weekday: localWeekday, reason: 'not_monday', counts: {} };
608
511
  }
609
512
 
610
513
  // Triage owns the morning. This is deliberately enforced below the cron so a manual invocation
@@ -612,25 +515,25 @@ async function runOutreachSweep({
612
515
  if (isWithinTriageProtectionWindow(now)) {
613
516
  const weekday = moment.tz(now, DEFAULT_TIMEZONE).isoWeekday();
614
517
  logger.info('[outreachSweep] triage protection window — nothing sent', { weekday });
615
- return { ran: false, weekday, source, reason: 'triage_protection_window', counts: {} };
518
+ return { ran: false, weekday, reason: 'triage_protection_window', counts: {} };
616
519
  }
617
520
 
618
521
  const rows = await airtableService.getRecordByFilter(
619
522
  Symptoms_ID, TABLE, 'TRUE()', mode === 'monday_triage_recovery' ? null : VIEW, Object.values(F), true,
620
523
  );
621
- const { candidates, targetDays, weekday } = source === MONITOR_SOURCE
622
- ? { candidates: await loadMonitorCandidates({ now, viewRows: rows || [] }), targetDays: [], weekday: localWeekday }
524
+ const { candidates, targetDays, weekday } = source === SCOPE_SOURCE
525
+ ? { candidates: await loadScopeCandidates({ now, viewRows: rows || [], patientScope }), targetDays: [], weekday: localWeekday }
623
526
  : buildCandidates(rows || [], { now, cadence, mode });
624
527
  if (cadence === 'legacy' && !targetDays.length) {
625
528
  logger.info('[outreachSweep] not a sweep day — nothing to do', { weekday });
626
- return { ran: false, weekday, source, reason: 'not_a_sweep_day', counts: {} };
529
+ return { ran: false, weekday, reason: 'not_a_sweep_day', counts: {} };
627
530
  }
628
531
 
629
532
  // The scheduler is not the guard. An operator running the sweep by hand at 03:00 must not wake
630
533
  // every candidate, so the window is checked here as well as by whatever invoked us.
631
534
  if (!isWithinHourWindow(now, PATIENT_CONTACT_WINDOW)) {
632
535
  logger.info('[outreachSweep] outside patient contact hours — nothing sent', { weekday });
633
- return { ran: false, weekday, source, reason: 'quiet_hours', counts: {} };
536
+ return { ran: false, weekday, reason: 'quiet_hours', counts: {} };
634
537
  }
635
538
 
636
539
  // An explicit empty scope means "nobody", not "scope omitted". This matters for a canary
@@ -658,11 +561,10 @@ async function runOutreachSweep({
658
561
  const weekdayLabel = moment.tz(now, DEFAULT_TIMEZONE).locale('es').format('dddd');
659
562
  const decisions = await mapWithConcurrency(admitted, concurrency, async (candidate) => {
660
563
  const brief = await enrichBrief(candidate);
661
- const claimLost = await claimCandidate({ candidate, weekdayLabel, dryRun, now });
662
- if (claimLost) {
564
+ if (!dryRun && !await claimCandidate({ candidate, weekdayLabel, dryRun, now })) {
663
565
  counts.rejected++;
664
- rejectedBy[claimLost] = (rejectedBy[claimLost] || 0) + 1;
665
- return { code: candidate.code, recordId: candidate.recordId, decision: 'skipped', holdReason: claimLost };
566
+ rejectedBy.claim_lost = (rejectedBy.claim_lost || 0) + 1;
567
+ return { code: candidate.code, recordId: candidate.recordId, decision: 'skipped', holdReason: 'claim_lost' };
666
568
  }
667
569
  try {
668
570
  const outcome = await runOutreachTurn(candidate.code, {
@@ -672,9 +574,7 @@ async function runOutreachSweep({
672
574
  dryRun,
673
575
  reminderType: 'seguimiento',
674
576
  reminderPrefix: mode === 'monday_triage_recovery' ? 'recovery' : null,
675
- triggeredBy: candidate.source === MONITOR_SOURCE
676
- ? `seguimiento:mm:${candidate.recordId}`
677
- : `seguimiento:${candidate.recordId}`,
577
+ triggeredBy: `seguimiento:${candidate.recordId}`,
678
578
  });
679
579
  counts.run++;
680
580
  if (outcome.decision === 'skipped') {
@@ -684,20 +584,19 @@ async function runOutreachSweep({
684
584
  outcome: { ...outcome, decision: 'hold', rationale: outcome.rationale || outcome.holdReason || 'turn skipped' },
685
585
  weekdayLabel,
686
586
  dryRun,
687
- now,
688
587
  });
689
588
  return { code: candidate.code, recordId: candidate.recordId, ...outcome };
690
589
  }
691
590
  counts[outcome.decision === 'write' ? 'write' : 'hold']++;
692
591
  if (outcome.accepted) counts.accepted++;
693
592
  if (outcome.delivered) counts.delivered++;
694
- await recordOutcome({ candidate, outcome, weekdayLabel, dryRun, now });
593
+ await recordOutcome({ candidate, outcome, weekdayLabel, dryRun });
695
594
  return { code: candidate.code, recordId: candidate.recordId, dryRun, brief, ...outcome };
696
595
  } catch (error) {
697
596
  counts.error++;
698
597
  logger.error('[outreachSweep] outreach turn failed', { code: candidate.code, error: error.message });
699
598
  try {
700
- await recordOutcome({ candidate, outcome: { error: error.message }, weekdayLabel, dryRun, now });
599
+ await recordOutcome({ candidate, outcome: { error: error.message }, weekdayLabel, dryRun });
701
600
  } catch (auditError) {
702
601
  // An audit-schema outage must be loud, but it must not abort the other patients in this
703
602
  // bounded batch. The pre-send claim remains `_en curso`, which the next sweep surfaces as
@@ -719,7 +618,7 @@ async function runOutreachSweep({
719
618
  module.exports = {
720
619
  runOutreachSweep,
721
620
  buildCandidates,
722
- buildMonitorCandidates,
621
+ buildScopeCandidates,
723
622
  rejectionReason,
724
623
  priorSweepReason,
725
624
  TARGET_DAYS_BY_WEEKDAY,
@@ -730,8 +629,6 @@ module.exports = {
730
629
  mondayTriageStillPending,
731
630
  VIEW,
732
631
  F,
733
- MONITOR_F,
734
- PATIENT_F,
735
632
  AUDIT,
736
633
  sweepClaims,
737
634
  };
package/lib/index.d.ts CHANGED
@@ -501,7 +501,7 @@ declare module '@peopl-health/nexus' {
501
501
  ran: boolean;
502
502
  weekday: number;
503
503
  mode?: 'continuity' | 'monday_triage_recovery';
504
- source?: 'weekly' | 'message_monitor';
504
+ source?: 'weekly' | 'patient_scope';
505
505
  targetDays?: string[];
506
506
  dryRun?: boolean;
507
507
  counts: Record<string, number>;
@@ -521,8 +521,8 @@ declare module '@peopl-health/nexus' {
521
521
  mode?: 'continuity' | 'monday_triage_recovery';
522
522
  /** daily: patients inactive for 7+ days. legacy: the Thursday/Saturday post-triage cohort (Mon/Tue and Wed/Thu triages). */
523
523
  cadence?: 'daily' | 'legacy';
524
- /** weekly: the triage questionnaire view. message_monitor: active-program patients outside that view, read from message_monitor; requires continuity, daily and a patientScope. */
525
- source?: 'weekly' | 'message_monitor';
524
+ /** weekly: the triage questionnaire view. patient_scope: the scoped patients outside that view; requires continuity, daily and a patientScope. */
525
+ source?: 'weekly' | 'patient_scope';
526
526
  }): Promise<OutreachSweepResult>;
527
527
 
528
528
  export const routes: any;
@@ -10,6 +10,7 @@ const threadSchema = new mongoose.Schema({
10
10
  run_id: { type: String, default: null },
11
11
  nombre: { type: String, default: null },
12
12
  stopped: { type: Boolean, default: false },
13
+ outreachOptOut: { type: Boolean, default: false },
13
14
  pausedAt: { type: Date, default: null },
14
15
  doNotAlertThrough: { type: Date, default: null },
15
16
  review: { type: Boolean, default: false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.69.0-dev.12621",
3
+ "version": "5.69.0-dev.12623",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",