@peopl-health/nexus 5.69.0-dev.12621 → 5.69.0-dev.12622
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,7 +1,7 @@
|
|
|
1
1
|
const moment = require('moment-timezone');
|
|
2
2
|
|
|
3
3
|
const airtableService = require('../../services/airtableService');
|
|
4
|
-
const {
|
|
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');
|
|
@@ -51,14 +51,7 @@ const AUDIT = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const WEEKLY_SOURCE = 'weekly';
|
|
54
|
-
const
|
|
55
|
-
const SOURCES = [WEEKLY_SOURCE, MONITOR_SOURCE];
|
|
56
|
-
|
|
57
|
-
const MONITOR_TABLE = 'message_monitor';
|
|
58
|
-
const MONITOR_F = {
|
|
59
|
-
whatsapp: 'whatsapp_id',
|
|
60
|
-
lastInbound: 'last_message_patient_time',
|
|
61
|
-
};
|
|
54
|
+
const SCOPE_SOURCE = 'patient_scope';
|
|
62
55
|
|
|
63
56
|
const PATIENT_TABLE = 'estado_general';
|
|
64
57
|
const PATIENT_F = {
|
|
@@ -67,11 +60,7 @@ const PATIENT_F = {
|
|
|
67
60
|
disableFollowUp: 'desactivar_seguimiento_48hrs',
|
|
68
61
|
};
|
|
69
62
|
|
|
70
|
-
const
|
|
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 });
|
|
63
|
+
const sweepClaims = new WriteClaimStore({ namespace: 'outreach_sweep', ttlMs: 8 * 24 * 60 * 60 * 1000 });
|
|
75
64
|
|
|
76
65
|
const UNDELIVERED_STATUSES = ['failed', 'undelivered'];
|
|
77
66
|
const CONTACT_SENT = {
|
|
@@ -227,60 +216,28 @@ function buildCandidates(rows, { now, cadence = 'daily', mode = 'continuity' })
|
|
|
227
216
|
return { candidates, targetDays, weekday, cadence, mode };
|
|
228
217
|
}
|
|
229
218
|
|
|
230
|
-
function
|
|
231
|
-
|
|
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();
|
|
219
|
+
function buildScopeCandidates({ patientScope, patientRows, excludedCodes }) {
|
|
220
|
+
const patients = new Map(patientScope.filter((code) => !excludedCodes.has(code))
|
|
221
|
+
.map((code) => [code, { names: new Map(), recordId: null, followUpDisabled: false }]));
|
|
261
222
|
for (const row of patientRows) {
|
|
262
|
-
const
|
|
263
|
-
if (!
|
|
264
|
-
if (!patientsByCode.has(code)) patientsByCode.set(code, { names: new Map(), followUpDisabled: false });
|
|
265
|
-
const patient = patientsByCode.get(code);
|
|
223
|
+
const patient = patients.get(String(first(row[PATIENT_F.whatsapp]) || '').trim());
|
|
224
|
+
if (!patient) continue;
|
|
266
225
|
const name = String(first(row[PATIENT_F.firstName]) || '').trim();
|
|
267
226
|
if (name) patient.names.set(name.toLowerCase().replace(/\s+/g, ' '), name);
|
|
227
|
+
patient.recordId = patient.recordId || row._recordId;
|
|
268
228
|
if (row[PATIENT_F.disableFollowUp] === true) patient.followUpDisabled = true;
|
|
269
229
|
}
|
|
270
230
|
|
|
271
231
|
const candidates = [];
|
|
272
|
-
for (const [code,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
logger.info('[outreachSweep] monitor candidate skipped — no single patient name', {
|
|
276
|
-
recordId: row._recordId, names: patient?.names.size || 0,
|
|
277
|
-
});
|
|
232
|
+
for (const [code, patient] of patients) {
|
|
233
|
+
if (patient.names.size !== 1) {
|
|
234
|
+
logger.info('[outreachSweep] scope candidate skipped — no single patient name', { recordId: patient.recordId });
|
|
278
235
|
continue;
|
|
279
236
|
}
|
|
280
237
|
candidates.push({
|
|
281
|
-
source:
|
|
238
|
+
source: SCOPE_SOURCE,
|
|
282
239
|
patientId: null,
|
|
283
|
-
recordId:
|
|
240
|
+
recordId: patient.recordId,
|
|
284
241
|
code,
|
|
285
242
|
name: [...patient.names.values()][0],
|
|
286
243
|
reason: 'no_triage_history',
|
|
@@ -297,34 +254,23 @@ function buildMonitorCandidates({ monitorRows, patientRows, excludedCodes }) {
|
|
|
297
254
|
return candidates;
|
|
298
255
|
}
|
|
299
256
|
|
|
300
|
-
async function
|
|
301
|
-
const weekStart = startOfLocalWeek(now).
|
|
302
|
-
const [
|
|
303
|
-
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)),
|
|
257
|
+
async function loadScopeCandidates({ now, viewRows, patientScope }) {
|
|
258
|
+
const weekStart = startOfLocalWeek(now).toISOString();
|
|
259
|
+
const [patientRows, weekRows] = await Promise.all([
|
|
260
|
+
airtableService.getRecordByFilter(Symptoms_ID, PATIENT_TABLE, 'TRUE()', null, Object.values(PATIENT_F), true),
|
|
308
261
|
airtableService.getRecordByFilter(
|
|
309
|
-
Symptoms_ID, TABLE, `NOT(IS_BEFORE({${F.createdAt}}, ${
|
|
262
|
+
Symptoms_ID, TABLE, `NOT(IS_BEFORE({${F.createdAt}}, DATETIME_PARSE("${weekStart}")))`, null, [F.whatsapp],
|
|
310
263
|
),
|
|
311
264
|
]);
|
|
312
|
-
const excludedCodes = new Set([
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
].map((code) => String(code || '').trim()).filter(Boolean));
|
|
316
|
-
return buildMonitorCandidates({ monitorRows: monitorRows || [], patientRows: patientRows || [], excludedCodes });
|
|
265
|
+
const excludedCodes = new Set([...viewRows, ...(weekRows || [])]
|
|
266
|
+
.map((row) => String(first(row[F.whatsapp]) || '').trim()));
|
|
267
|
+
return buildScopeCandidates({ patientScope, patientRows: patientRows || [], excludedCodes });
|
|
317
268
|
}
|
|
318
269
|
|
|
319
270
|
function sweepClaimKey(candidate, now) {
|
|
320
271
|
return `${candidate.code}:${startOfLocalWeek(now).format('YYYY-MM-DD')}`;
|
|
321
272
|
}
|
|
322
273
|
|
|
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
274
|
function opsOptedOut(candidate, { now }) {
|
|
329
275
|
if (candidate.followUpDisabled) return 'patient_follow_up_disabled';
|
|
330
276
|
const weekStart = startOfLocalWeek(now);
|
|
@@ -340,12 +286,9 @@ function opsOptedOut(candidate, { now }) {
|
|
|
340
286
|
// Patient-scoped, not row-scoped: a patient with two questionnaires this week is one person, and
|
|
341
287
|
// the old automation's per-row stamp is what let her be contacted twice.
|
|
342
288
|
function priorSweepReason(candidate) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
async function priorMonitorSweepReason(candidate, { now }) {
|
|
347
|
-
const claim = await sweepClaims.get(sweepClaimKey(candidate, now));
|
|
348
|
-
return sweepReasonFromStamps(claim?.stamp ? [claim.stamp] : []);
|
|
289
|
+
const stamps = candidate.rows.map((row) => String(row[F.review] || '').trim()).filter(Boolean);
|
|
290
|
+
if (stamps.some((stamp) => stamp.endsWith('_en curso'))) return 'stuck_in_progress';
|
|
291
|
+
return stamps.length ? 'already_swept_this_week' : null;
|
|
349
292
|
}
|
|
350
293
|
|
|
351
294
|
async function mondayTriageStillPending(candidate) {
|
|
@@ -366,8 +309,8 @@ async function rejectionReason(candidate, {
|
|
|
366
309
|
if (opsReason) return opsReason;
|
|
367
310
|
if (await botIsPaused(candidate.code, { label: '[outreachSweep]' })) return 'bot_paused';
|
|
368
311
|
if (!includePreviouslySwept) {
|
|
369
|
-
const sweptReason = candidate.source ===
|
|
370
|
-
? await
|
|
312
|
+
const sweptReason = candidate.source === SCOPE_SOURCE
|
|
313
|
+
? (await sweepClaims.has(sweepClaimKey(candidate, now)) ? 'already_swept_this_week' : null)
|
|
371
314
|
: priorSweepReason(candidate);
|
|
372
315
|
if (sweptReason) return sweptReason;
|
|
373
316
|
|
|
@@ -488,14 +431,9 @@ async function enrichBrief(candidate) {
|
|
|
488
431
|
// patient uncontacted for the week, which is the safe direction: a free-text send carries no
|
|
489
432
|
// contentSid and no idempotency key, so nothing downstream would catch a duplicate.
|
|
490
433
|
async function claimCandidate({ candidate, weekdayLabel, dryRun, now }) {
|
|
491
|
-
if (dryRun) return
|
|
492
|
-
if (candidate.source ===
|
|
493
|
-
|
|
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';
|
|
434
|
+
if (dryRun) return true;
|
|
435
|
+
if (candidate.source === SCOPE_SOURCE) {
|
|
436
|
+
return (await sweepClaims.acquire(sweepClaimKey(candidate, now))) === 'acquired';
|
|
499
437
|
}
|
|
500
438
|
try {
|
|
501
439
|
const claimed = await airtableService.updateRecordByFilter(
|
|
@@ -507,39 +445,27 @@ async function claimCandidate({ candidate, weekdayLabel, dryRun, now }) {
|
|
|
507
445
|
);
|
|
508
446
|
if (!claimed?.length) {
|
|
509
447
|
logger.info('[outreachSweep] candidate already claimed by another run', { recordId: candidate.recordId });
|
|
510
|
-
return
|
|
448
|
+
return false;
|
|
511
449
|
}
|
|
512
|
-
return
|
|
450
|
+
return true;
|
|
513
451
|
} catch (error) {
|
|
514
452
|
logger.warn('[outreachSweep] claim failed; skipping rather than risking a double send', {
|
|
515
453
|
recordId: candidate.recordId, error: error.message,
|
|
516
454
|
});
|
|
517
|
-
return
|
|
455
|
+
return false;
|
|
518
456
|
}
|
|
519
457
|
}
|
|
520
458
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (outcome.
|
|
527
|
-
if (outcome.
|
|
528
|
-
|
|
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
|
-
}
|
|
459
|
+
async function recordOutcome({ candidate, outcome, weekdayLabel, dryRun }) {
|
|
460
|
+
if (dryRun || candidate.source === SCOPE_SOURCE) return;
|
|
461
|
+
// Three outcomes, not two: a `write` whose delivery never happened is neither "escrito" nor a
|
|
462
|
+
// deliberate hold, and collapsing it into either would hide the failure from the ops queue.
|
|
463
|
+
let stamp;
|
|
464
|
+
if (outcome.error) stamp = `${weekdayLabel}_error`;
|
|
465
|
+
else if (outcome.delivered) stamp = `${weekdayLabel}_entregado`;
|
|
466
|
+
else if (outcome.accepted) stamp = `${weekdayLabel}_enviado`;
|
|
467
|
+
else if (outcome.decision === 'write') stamp = `${weekdayLabel}_no entregado`;
|
|
468
|
+
else stamp = `${weekdayLabel}_omitido`;
|
|
543
469
|
const where = `RECORD_ID() = "${candidate.recordId}"`;
|
|
544
470
|
await airtableService.updateRecordByFilter(Symptoms_ID, TABLE, where, {
|
|
545
471
|
[F.review]: stamp,
|
|
@@ -592,19 +518,16 @@ async function runOutreachSweep({
|
|
|
592
518
|
if (!['continuity', 'monday_triage_recovery'].includes(mode)) {
|
|
593
519
|
throw new Error('mode must be continuity or monday_triage_recovery');
|
|
594
520
|
}
|
|
595
|
-
if (!
|
|
596
|
-
throw new Error('source must be weekly or
|
|
521
|
+
if (![WEEKLY_SOURCE, SCOPE_SOURCE].includes(source)) {
|
|
522
|
+
throw new Error('source must be weekly or patient_scope');
|
|
597
523
|
}
|
|
598
|
-
|
|
599
|
-
|
|
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');
|
|
524
|
+
if (source === SCOPE_SOURCE && (mode !== 'continuity' || cadence !== 'daily' || !Array.isArray(patientScope))) {
|
|
525
|
+
throw new Error('the patient_scope source requires continuity mode, daily cadence and a patientScope');
|
|
603
526
|
}
|
|
604
527
|
|
|
605
528
|
const localWeekday = moment.tz(now, DEFAULT_TIMEZONE).isoWeekday();
|
|
606
529
|
if (mode === 'monday_triage_recovery' && localWeekday !== 1) {
|
|
607
|
-
return { ran: false, weekday: localWeekday,
|
|
530
|
+
return { ran: false, weekday: localWeekday, reason: 'not_monday', counts: {} };
|
|
608
531
|
}
|
|
609
532
|
|
|
610
533
|
// Triage owns the morning. This is deliberately enforced below the cron so a manual invocation
|
|
@@ -612,25 +535,25 @@ async function runOutreachSweep({
|
|
|
612
535
|
if (isWithinTriageProtectionWindow(now)) {
|
|
613
536
|
const weekday = moment.tz(now, DEFAULT_TIMEZONE).isoWeekday();
|
|
614
537
|
logger.info('[outreachSweep] triage protection window — nothing sent', { weekday });
|
|
615
|
-
return { ran: false, weekday,
|
|
538
|
+
return { ran: false, weekday, reason: 'triage_protection_window', counts: {} };
|
|
616
539
|
}
|
|
617
540
|
|
|
618
541
|
const rows = await airtableService.getRecordByFilter(
|
|
619
542
|
Symptoms_ID, TABLE, 'TRUE()', mode === 'monday_triage_recovery' ? null : VIEW, Object.values(F), true,
|
|
620
543
|
);
|
|
621
|
-
const { candidates, targetDays, weekday } = source ===
|
|
622
|
-
? { candidates: await
|
|
544
|
+
const { candidates, targetDays, weekday } = source === SCOPE_SOURCE
|
|
545
|
+
? { candidates: await loadScopeCandidates({ now, viewRows: rows || [], patientScope }), targetDays: [], weekday: localWeekday }
|
|
623
546
|
: buildCandidates(rows || [], { now, cadence, mode });
|
|
624
547
|
if (cadence === 'legacy' && !targetDays.length) {
|
|
625
548
|
logger.info('[outreachSweep] not a sweep day — nothing to do', { weekday });
|
|
626
|
-
return { ran: false, weekday,
|
|
549
|
+
return { ran: false, weekday, reason: 'not_a_sweep_day', counts: {} };
|
|
627
550
|
}
|
|
628
551
|
|
|
629
552
|
// The scheduler is not the guard. An operator running the sweep by hand at 03:00 must not wake
|
|
630
553
|
// every candidate, so the window is checked here as well as by whatever invoked us.
|
|
631
554
|
if (!isWithinHourWindow(now, PATIENT_CONTACT_WINDOW)) {
|
|
632
555
|
logger.info('[outreachSweep] outside patient contact hours — nothing sent', { weekday });
|
|
633
|
-
return { ran: false, weekday,
|
|
556
|
+
return { ran: false, weekday, reason: 'quiet_hours', counts: {} };
|
|
634
557
|
}
|
|
635
558
|
|
|
636
559
|
// An explicit empty scope means "nobody", not "scope omitted". This matters for a canary
|
|
@@ -658,11 +581,10 @@ async function runOutreachSweep({
|
|
|
658
581
|
const weekdayLabel = moment.tz(now, DEFAULT_TIMEZONE).locale('es').format('dddd');
|
|
659
582
|
const decisions = await mapWithConcurrency(admitted, concurrency, async (candidate) => {
|
|
660
583
|
const brief = await enrichBrief(candidate);
|
|
661
|
-
|
|
662
|
-
if (claimLost) {
|
|
584
|
+
if (!dryRun && !await claimCandidate({ candidate, weekdayLabel, dryRun, now })) {
|
|
663
585
|
counts.rejected++;
|
|
664
|
-
rejectedBy
|
|
665
|
-
return { code: candidate.code, recordId: candidate.recordId, decision: 'skipped', holdReason:
|
|
586
|
+
rejectedBy.claim_lost = (rejectedBy.claim_lost || 0) + 1;
|
|
587
|
+
return { code: candidate.code, recordId: candidate.recordId, decision: 'skipped', holdReason: 'claim_lost' };
|
|
666
588
|
}
|
|
667
589
|
try {
|
|
668
590
|
const outcome = await runOutreachTurn(candidate.code, {
|
|
@@ -672,9 +594,7 @@ async function runOutreachSweep({
|
|
|
672
594
|
dryRun,
|
|
673
595
|
reminderType: 'seguimiento',
|
|
674
596
|
reminderPrefix: mode === 'monday_triage_recovery' ? 'recovery' : null,
|
|
675
|
-
triggeredBy: candidate.
|
|
676
|
-
? `seguimiento:mm:${candidate.recordId}`
|
|
677
|
-
: `seguimiento:${candidate.recordId}`,
|
|
597
|
+
triggeredBy: `seguimiento:${candidate.recordId}`,
|
|
678
598
|
});
|
|
679
599
|
counts.run++;
|
|
680
600
|
if (outcome.decision === 'skipped') {
|
|
@@ -684,20 +604,19 @@ async function runOutreachSweep({
|
|
|
684
604
|
outcome: { ...outcome, decision: 'hold', rationale: outcome.rationale || outcome.holdReason || 'turn skipped' },
|
|
685
605
|
weekdayLabel,
|
|
686
606
|
dryRun,
|
|
687
|
-
now,
|
|
688
607
|
});
|
|
689
608
|
return { code: candidate.code, recordId: candidate.recordId, ...outcome };
|
|
690
609
|
}
|
|
691
610
|
counts[outcome.decision === 'write' ? 'write' : 'hold']++;
|
|
692
611
|
if (outcome.accepted) counts.accepted++;
|
|
693
612
|
if (outcome.delivered) counts.delivered++;
|
|
694
|
-
await recordOutcome({ candidate, outcome, weekdayLabel, dryRun
|
|
613
|
+
await recordOutcome({ candidate, outcome, weekdayLabel, dryRun });
|
|
695
614
|
return { code: candidate.code, recordId: candidate.recordId, dryRun, brief, ...outcome };
|
|
696
615
|
} catch (error) {
|
|
697
616
|
counts.error++;
|
|
698
617
|
logger.error('[outreachSweep] outreach turn failed', { code: candidate.code, error: error.message });
|
|
699
618
|
try {
|
|
700
|
-
await recordOutcome({ candidate, outcome: { error: error.message }, weekdayLabel, dryRun
|
|
619
|
+
await recordOutcome({ candidate, outcome: { error: error.message }, weekdayLabel, dryRun });
|
|
701
620
|
} catch (auditError) {
|
|
702
621
|
// An audit-schema outage must be loud, but it must not abort the other patients in this
|
|
703
622
|
// bounded batch. The pre-send claim remains `_en curso`, which the next sweep surfaces as
|
|
@@ -719,7 +638,7 @@ async function runOutreachSweep({
|
|
|
719
638
|
module.exports = {
|
|
720
639
|
runOutreachSweep,
|
|
721
640
|
buildCandidates,
|
|
722
|
-
|
|
641
|
+
buildScopeCandidates,
|
|
723
642
|
rejectionReason,
|
|
724
643
|
priorSweepReason,
|
|
725
644
|
TARGET_DAYS_BY_WEEKDAY,
|
|
@@ -730,7 +649,6 @@ module.exports = {
|
|
|
730
649
|
mondayTriageStillPending,
|
|
731
650
|
VIEW,
|
|
732
651
|
F,
|
|
733
|
-
MONITOR_F,
|
|
734
652
|
PATIENT_F,
|
|
735
653
|
AUDIT,
|
|
736
654
|
sweepClaims,
|
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' | '
|
|
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.
|
|
525
|
-
source?: 'weekly' | '
|
|
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;
|