@peopl-health/nexus 5.68.0 → 5.68.2

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.
@@ -6,12 +6,12 @@ const URGENCY_LABELS = {
6
6
  stat: 'inmediata', asap: 'lo antes posible', urgent: 'urgente', routine: 'rutina',
7
7
  };
8
8
 
9
- const PROPOSAL_HEADING = 'Propuesta de tratamiento (sugerencia para su aprobación, no es una indicación):';
10
- const REQUESTED_ACTION = 'Acción solicitada: ¿aprueba la propuesta, la modifica o prefiere una valoración presencial?';
11
- const ASSESSMENT_ACTION = 'Acción solicitada: valoración médica prioritaria. ¿La valora en persona, la envía a urgencias o indica otra conducta?';
9
+ const REQUESTED_ACTION = '¿Aprueba la propuesta, la modifica o prefiere una valoración presencial?';
10
+ const ASSESSMENT_ACTION = 'Requiere valoración médica: ¿la valora en persona, la envía a urgencias o indica otra conducta?';
12
11
  const DOCTOR_TITLE = /^(dr|dra)\.?\s/i;
13
12
 
14
13
  const withTitle = (name) => (DOCTOR_TITLE.test(name) ? name : `Dr./Dra. ${name}`);
14
+ const sentence = (text) => (/[.!?]$/.test(text) ? text : `${text}.`);
15
15
 
16
16
  function latestAlarm(review) {
17
17
  const alarms = Array.isArray(review?.alarms) ? review.alarms : [];
@@ -28,19 +28,6 @@ function flagsByStatus(sections) {
28
28
  };
29
29
  }
30
30
 
31
- function symptomLines(sections, stamp, fallbackSymptom = null) {
32
- const grade = [1, 2, 3].includes(sections?.grade) ? `G${sections.grade}` : 'grado no establecido';
33
- const said = [inlineOf(sections?.symptom) || fallbackSymptom || 'síntoma no especificado', grade, ...stamp].filter(Boolean);
34
- const { present, absent, unassessed } = flagsByStatus(sections);
35
- if (!present.length && !absent.length && !unassessed.length) return [`Síntoma: ${said.join(' · ')}`, 'Banderas rojas: no documentadas.'];
36
- return [
37
- `Síntoma: ${said.join(' · ')}`,
38
- `Banderas rojas presentes: ${present.length ? present.join(', ') : 'ninguna'}.`,
39
- `Banderas rojas ausentes: ${absent.length ? absent.join(', ') : 'ninguna revisada'}.`,
40
- ...(unassessed.length ? [`Banderas rojas sin evaluar: ${unassessed.join(', ')}.`] : []),
41
- ];
42
- }
43
-
44
31
  function vitalsLine(vitals) {
45
32
  const parts = [];
46
33
  if (vitals?.temperatureC != null) parts.push(`T ${vitals.temperatureC} °C`);
@@ -48,62 +35,66 @@ function vitalsLine(vitals) {
48
35
  if (vitals?.systolic != null || vitals?.diastolic != null) parts.push(`PA ${vitals.systolic ?? '?'}/${vitals.diastolic ?? '?'} mmHg`);
49
36
  if (vitals?.spo2 != null) parts.push(`SpO2 ${vitals.spo2}%`);
50
37
  if (vitals?.respiratoryRate != null) parts.push(`FR ${vitals.respiratoryRate} rpm`);
51
- return parts.length ? `Signos vitales: ${parts.join(' · ')}.` : 'Signos vitales: no reportados.';
38
+ return parts.length ? `Signos vitales: ${parts.join(' · ')}.` : null;
52
39
  }
53
40
 
54
- function proposalItemLine(symptom, item) {
41
+ function drugOf(symptom, item) {
55
42
  const entry = formularyFor(symptom);
56
43
  const drug = entry ? findDrug(entry, item) : null;
57
44
  if (!drug) return null;
58
- const name = [drug.drug, drug.brand ? `(${drug.brand})` : null, drug.strength].filter(Boolean).join(' ');
59
- const parts = [`- ${name}`];
60
- if (drug.instructions) parts.push(drug.instructions);
61
- parts.push(`posición en la tabla: ${drug.tableLabel}`);
62
- const why = inlineOf(item?.justification?.text);
63
- if (why) parts.push(`motivo: ${why}`);
64
- return parts.join(' — ');
45
+ const name = [drug.drug, drug.strength].filter(Boolean).join(' ');
46
+ const why = inlineOf(item?.justification?.text)?.replace(/[.;]+$/, '');
47
+ return [drug.instructions ? `${name} (${drug.instructions})` : name, why && `motivo: ${why}`].filter(Boolean).join(', ');
65
48
  }
66
49
 
67
- function proposalBody({ proposal, withheldReason }) {
68
- if (!proposal?.items?.length) {
69
- const reason = inlineOf(withheldReason) || 'sin propuesta farmacológica';
70
- return [`No se propone tratamiento sintomático: ${reason}. Se prioriza la valoración médica.`];
71
- }
72
- const lines = proposal.items.map((item) => proposalItemLine(proposal.symptom, item)).filter(Boolean);
73
- const declared = [...(proposal.conditions ?? []), ...(proposal.ruledOut ?? []).map((key) => `descartado: ${key}`)].map(inlineOf).filter(Boolean);
74
- if (declared.length) lines.push(`Condiciones que se declararon para esta propuesta (verificar): ${declared.join(', ')}.`);
75
- return lines;
50
+ const proposedDrugs = ({ proposal }) => (proposal?.items ?? []).map((item) => drugOf(proposal.symptom, item)).filter(Boolean);
51
+
52
+ function proposalLine({ proposal, withheldReason }) {
53
+ const drugs = proposedDrugs({ proposal });
54
+ if (!drugs.length) return `→ Sin propuesta de tratamiento: ${sentence(inlineOf(withheldReason) || 'requiere valoración médica')}`;
55
+ const declared = [...(proposal.conditions ?? []), ...(proposal.ruledOut ?? []).map((key) => `descartado ${key}`)]
56
+ .map((key) => inlineOf(key)?.replace(/_/g, ' ')).filter(Boolean);
57
+ const table = formularyFor(proposal.symptom)?.label;
58
+ return [`→ Propuesta para su aprobación (tabla ${table}): ${sentence(drugs.join('; '))}`, declared.length && `Verificar: ${sentence(declared.join(', '))}`].filter(Boolean).join(' ');
76
59
  }
77
60
 
78
- function proposalHeading(entry, symptomName) {
79
- const tableLabel = entry.proposal?.items?.length ? formularyFor(entry.proposal.symptom)?.label : null;
80
- return tableLabel ? `${symptomName} (tabla: ${tableLabel})` : symptomName;
61
+ function symptomParagraph(entry, fallbackSymptom) {
62
+ const sections = entry.sections ?? {};
63
+ const grade = [1, 2, 3].includes(sections.grade) ? `G${sections.grade}` : 'grado no establecido';
64
+ const { present, absent, unassessed } = flagsByStatus(sections);
65
+ const isUndocumented = !present.length && !absent.length && !unassessed.length;
66
+ const summary = inlineOf(sections.characterizationSummary);
67
+ return [
68
+ `${inlineOf(sections.symptom) || fallbackSymptom || 'Síntoma no especificado'} (${grade}):`,
69
+ summary && sentence(summary),
70
+ present.length && `${present.length > 1 ? 'Banderas rojas presentes' : 'Bandera roja presente'}: ${sentence(present.join(', '))}`,
71
+ unassessed.length && `Falta confirmar: ${sentence(unassessed.join(', '))}`,
72
+ isUndocumented && 'Banderas rojas no documentadas.',
73
+ !present.length && !unassessed.length && absent.length && 'Sin banderas rojas.',
74
+ proposalLine(entry),
75
+ ].filter(Boolean).join(' ');
81
76
  }
82
77
 
83
78
  function composeDoctorBody({ alarm, shared, symptoms }) {
84
- const several = symptoms.length > 1;
85
- const name = (entry) => inlineOf(entry.sections?.symptom) || 'Síntoma';
86
- const stamp = [
87
- URGENCY_LABELS[alarm?.urgencyLevel] ?? inlineOf(alarm?.urgencyLevel),
88
- alarm?.raisedAt ? mexicoStamp(alarm.raisedAt) : null,
89
- ];
90
79
  const context = shared?.oncologyContext;
91
- const value = (text) => inlineOf(text) || 'no disponible';
80
+ const treatment = inlineOf(context?.treatment);
81
+ const lastDose = inlineOf(context?.lastDose);
82
+ const priorManagement = inlineOf(context?.priorManagement);
83
+ const response = inlineOf(context?.response);
84
+ const urgency = [URGENCY_LABELS[alarm?.urgencyLevel] ?? inlineOf(alarm?.urgencyLevel), alarm?.raisedAt ? mexicoStamp(alarm.raisedAt) : null].filter(Boolean);
85
+ const priorities = inlineOf(shared?.doctorPriorities);
92
86
  return [
93
- ...symptoms.flatMap((entry, index) => symptomLines(entry.sections, index === 0 ? stamp : [], several ? null : inlineOf(alarm?.symptom))),
94
- '',
95
- ...symptoms.map((entry) => `${several ? `Caracterización (${name(entry)})` : 'Caracterización'}: ${inlineOf(entry.sections?.characterizationSummary) || 'no disponible'}`),
87
+ urgency.length ? `Urgencia: ${urgency.join(' · ')}` : null,
88
+ `Tratamiento: ${treatment || 'no disponible'}; última dosis ${lastDose || 'no disponible'}.`,
89
+ priorManagement || response ? `Manejo previo: ${sentence([priorManagement, response && `respuesta ${response}`].filter(Boolean).join('; '))}` : null,
96
90
  vitalsLine(shared?.vitals),
97
- ...(inlineOf(shared?.objectiveData) ? [`Datos objetivos: ${inlineOf(shared.objectiveData)}`] : []),
98
- '',
99
- `Contexto oncológico: tratamiento ${value(context?.treatment)}; última dosis ${value(context?.lastDose)}.`,
100
- `Manejo previo y respuesta: ${value(context?.priorManagement)}; respuesta ${value(context?.response)}.`,
91
+ inlineOf(shared?.objectiveData) ? `Datos objetivos: ${sentence(inlineOf(shared.objectiveData))}` : null,
101
92
  '',
102
- PROPOSAL_HEADING,
103
- ...symptoms.flatMap((entry) => (several ? [`${proposalHeading(entry, name(entry))}:`, ...proposalBody(entry)] : proposalBody(entry))),
93
+ ...symptoms.map((entry) => symptomParagraph(entry, symptoms.length > 1 ? null : inlineOf(alarm?.symptom))),
104
94
  '',
105
- symptoms.some((entry) => entry.proposal?.items?.length) ? REQUESTED_ACTION : ASSESSMENT_ACTION,
106
- ].join('\n');
95
+ priorities ? `Prioridades: ${sentence(priorities)}` : null,
96
+ symptoms.some((entry) => proposedDrugs(entry).length) ? REQUESTED_ACTION : ASSESSMENT_ACTION,
97
+ ].filter((line) => line !== null).join('\n');
107
98
  }
108
99
 
109
100
  function identityLines({ identity, review }) {
@@ -23,7 +23,6 @@ const {
23
23
  const { getPatientAuditLog } = require('../models/patientAuditLogModel');
24
24
  const { isWithin24HourWindow } = require('../helpers/conversationWindowHelper');
25
25
  const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
26
- const { sendMessage } = require('../core/NexusMessaging');
27
26
  const { generateEscalationDraft } = require('../clinical/services/escalationDraftService');
28
27
  const { conversationLink, recordEmergency, writeUnresolvedRequest } = require('../clinical/services/clinicalAirtableService');
29
28
  const { renderDoctorMessage, templateSummary, withIdentity } = require('../clinical/helpers/doctorReviewMessageHelper');
@@ -39,6 +38,8 @@ const { getEscalationApprover, listEscalationApprovers } = require('./physicianS
39
38
  const { switchThreadStoppedStatus } = require('./threadService');
40
39
  const { effectiveUrgencyOf, responseDeadlineOf, responseDeadlinePolicy } = require('./reviewDeadlineService');
41
40
 
41
+ const getMessaging = () => require('../core/NexusMessaging');
42
+
42
43
  const AUDIT_SOURCE = 'escalation_review';
43
44
  const RECENT_REVIEW_LIMIT = 20;
44
45
  const TIMELINE_LIMIT = 500;
@@ -367,7 +368,7 @@ async function sendDoctorAlert({ message, approver, operator }) {
367
368
  const outgoing = inWindow
368
369
  ? { code: doctorCode, body: message.body, triggeredBy: operator }
369
370
  : { code: doctorCode, contentSid: configured('ESCALATION_REVIEW_DOCTOR_ALERT_IDENTIFIED_CONTENT_SID'), variables: message.template, triggeredBy: operator };
370
- const result = await sendMessage(outgoing);
371
+ const result = await getMessaging().sendMessage(outgoing);
371
372
  if (!result?.success && !result?.completed) {
372
373
  throw httpError(502, 'The WhatsApp message to the physician could not be sent');
373
374
  }
@@ -1608,7 +1609,7 @@ async function sendApprovedIndications({ id, outsideWindowRoute = null, sentBy,
1608
1609
 
1609
1610
  let result;
1610
1611
  try {
1611
- result = await sendMessage(message);
1612
+ result = await getMessaging().sendMessage(message);
1612
1613
  const alreadyDelivered = result?.status === 'duplicate_suppressed' && result?.completed === true;
1613
1614
  if (!result?.success && !alreadyDelivered) {
1614
1615
  throw httpError(502, 'The indications could not be sent to the patient');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.68.0",
3
+ "version": "5.68.2",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",