Files
adas-core/adas-core.Application/Customizations/H12O/UCIN/CalculatedObservations.cs
T

1873 lines
92 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Pumps;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
namespace adas_core.Application.Customizations.H12O.UCIN;
/// <summary>
/// Implements the UCIN-specific clinical calculations that derive secondary
/// <see cref="PatientObservation"/> values (Complexity, Oxygenation_Index, Respiratory, IntravenousLines,
/// Medication, ERMedication, Monitor, Surgery, Temp_Gradient, TAm alert, etc.) from incoming
/// raw observations and active <see cref="PatientTreatment"/> entries, using the catalogue of
/// codes, groups, and types configured in <see cref="ApiSettings"/>.
/// </summary>
/// <!-- aidoc:v1 sig=1b16cfc -->
public class CalculatedObservations : ICalculatedObservations
{
private readonly List<string> _complexityObservations =
["Respiratory", "IntravenousLines", "Medication", "Surgery", "Weight_Newborn", "Weight_Current", "Monitor"];
private readonly List<string> _ecmo = [];
private readonly List<string> _electroencephalogram = [];
private readonly List<string> _highFrequencyVentilation = [];
//private readonly List<string> _shift = [];
private readonly Tuple<IntraVenousLineTypes, int, List<string>>[] _intraVenousLinesTypeValue =
[
Tuple.Create(IntraVenousLineTypes.Artery, 5,
new List<string> { "Catéter arterial", "Catéter UMBILICAL arteria" }),
Tuple.Create(IntraVenousLineTypes.CentralVein, 4,
new List<string> { "Catéter venoso CENTRAL", "Catéter UMBILICAL vena" }),
Tuple.Create(IntraVenousLineTypes.Picc, 3, new List<string> { "Catéter PICC" }),
Tuple.Create(IntraVenousLineTypes.MiddleLine, 2,
new List<string> { "Catéter línea media", "Catéter EPICUTÁNEO PERIFÉRICO" }),
Tuple.Create(IntraVenousLineTypes.Peripheral, 1, new List<string> { "Catéter Venoso PERIFÉRICO" })
];
private readonly List<string> _invasiveVentilation = [];
private readonly ILogger<CalculatedObservations> _logger;
private readonly List<string> _medicationBolus = [];
//private readonly Lazy<IMedicineService> medicineService;
private readonly IMedicineService _medicineService;
private readonly List<string> _nonInvasiveVentilation = [];
//private readonly List<string> Oni = new List<string>();
private readonly List<string> _notesIndicatingMedication = [];
private readonly Lazy<IObservationService> _observationService;
private readonly List<string> _oniCodes = [];
private readonly List<string> _regionalBrainSaturation = [];
private readonly List<string> _respiratory = [];
private readonly Tuple<RespiratoryTypes, int, List<string>>[] _respiratoryTypeValue =
[
// Tuple.Create(RespiratoryTypes.INO, 10, new List<string>{ ""}),
Tuple.Create(RespiratoryTypes.Vafo, 5, new List<string> { "V.A.F.O." }),
Tuple.Create(RespiratoryTypes.Vmc, 3, new List<string> { "V.M.C." }),
Tuple.Create(RespiratoryTypes.Vmni, 2, new List<string> { "V.N.I. Ciclada", "CPAP" }),
Tuple.Create(RespiratoryTypes.NasalCannulas, 1, new List<string> { "Alto Flujo", "Bajo Flujo" }),
Tuple.Create(RespiratoryTypes.None, 0, new List<string> { "Sin assistance respiratoria" })
];
private readonly List<string> _rxaStatus = ["Concluido"];
private readonly List<string> _surgery = [];
private readonly List<string> _surgeryText = [];
private readonly List<string> _transcutanous = [];
private readonly Lazy<ITreatmentService> _treatmentService;
/// <summary>
/// Initializes a new instance of the <see cref="CalculatedObservations"/> class,
/// resolving its dependencies (observation, medicine, and treatment services, plus the logger)
/// from the supplied <see cref="IServiceProvider"/> and loading the configured code catalogues
/// (EEG, bolus medications, transcutaneous, regional brain saturation, surgery, respiratory,
/// ONi, ventilation modes, ECMO, and notes indicating medication) from <see cref="ApiSettings"/>.
/// </summary>
/// <param name="serviceProvider">The application's service provider used to resolve the required dependencies and configuration.</param>
/// <!-- aidoc-review:v1 severity=low kind=stale_summary
/// "The summary lists the configured catalogues loaded from ApiSettings but omits SurgeryText, which is also loaded in the constructor." -->
public CalculatedObservations(IServiceProvider serviceProvider)
{
_observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>(); //observationService;
_medicineService = serviceProvider.GetRequiredService<IMedicineService>(); //medicineService;
_treatmentService = serviceProvider.GetRequiredService<Lazy<ITreatmentService>>(); //treatmentService;
var apiSettings = serviceProvider.GetRequiredService<IOptions<ApiSettings>>(); //apiSettings;
_logger = serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();
var electroencephalogram = apiSettings.Value.Electroencephalogram ?? null;
electroencephalogram?.ForEach(x => _electroencephalogram.Add(x.Trim()));
var medicationBolus = apiSettings.Value.MedicationBolus ?? null;
medicationBolus?.ForEach(x => _medicationBolus.Add(x.Trim()));
var transcutaneous = apiSettings.Value.Transcutaneous ?? null;
transcutaneous?.ForEach(x => _transcutanous.Add(x.Trim()));
var regionalBrainSaturation = apiSettings.Value.RegionalBrainSaturation ?? null;
regionalBrainSaturation?.ForEach(x => _regionalBrainSaturation.Add(x.Trim()));
var surgery = apiSettings.Value.Surgery ?? null;
surgery?.ForEach(x => _surgery.Add(x.Trim()));
var surgeryText = apiSettings.Value.SurgeryText ?? null;
surgeryText?.ForEach(x => _surgeryText.Add(x.Trim()));
var respiratory = apiSettings.Value.Respiratory ?? null;
respiratory?.ForEach(x => _respiratory.Add(x.Trim()));
//inoCode = Respiratory.LastOrDefault();
var oni = apiSettings.Value.Oni ?? null;
oni?.ForEach(x => _oniCodes.Add(x.Trim()));
var highFrequencyVentilation = apiSettings.Value.HighFrequencyVentilation ?? null;
highFrequencyVentilation?.ForEach(x => _highFrequencyVentilation.Add(x.Trim()));
var invasiveVentilation = apiSettings.Value.InvasiveVentilation ?? null;
invasiveVentilation?.ForEach(x => _invasiveVentilation.Add(x.Trim()));
var nonInvasiveVentilation = apiSettings.Value.NonInvasiveVentilation ?? null;
nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim()));
//var shift = apiSettings.Value.Shift ?? null;
//shift?.ForEach(x => _shift.Add(x.Trim()));
var notesIndicatingMedication =
apiSettings.Value.NotesIndicatingMedication ?? null;
notesIndicatingMedication?.ForEach(x => _notesIndicatingMedication.Add(x.Trim()));
var ecmo = apiSettings.Value.Ecmo ?? null;
ecmo?.ForEach(x => _ecmo.Add(x.Trim()));
}
/// <summary>
/// Applies the appropriate clinical calculations to a raw patient observation based on its
/// <see cref="BasePatientObservation.Name"/>, code, coding system, and expiration state, producing
/// derived observations (respiratory assistance, oxygenation index, ventilation mode, TAm alert,
/// parsed weight, temperature gradient, intravenous lines, monitor, surgery expiration, complexity,
/// and hourly/temperature time-increment handling).
/// </summary>
/// <typeparam name="T">The concrete observation type, deriving from <see cref="BasePatientObservation"/>.</typeparam>
/// <param name="obs">The observation to map or transform in-place.</param>
/// <param name="onlyByName">
/// Reserved for future use. When <see langword="true"/>, restricts the mapping strategy to
/// name-based lookups only.
/// </param>
/// <returns>
/// A <see cref="Task{T}"/> that resolves to the (possibly transformed) observation, or
/// <see langword="null"/> when the input observation has no name.
/// </returns>
/// <!-- aidoc-review:v1 severity=high kind=wrong_returns
/// "The documentation states the method resolves to null 'when the input observation has no name', but the early-exit branch returns the original `obs`, not null." -->
public async Task<T?> Map<T>(T obs, bool onlyByName) where T : BasePatientObservation
{
// Early exit: if the observation has no name, there's nothing to map
if (string.IsNullOrEmpty(obs.Name)) return obs;
// Respiratory observations: only process SNOMED-coded respiratory metrics
if (!string.IsNullOrEmpty(obs.Code) && _respiratory.Contains(obs.Code) && obs.CodingSystem == "SNM")
{
_logger.LogDebug("Mapping observation Respiratorio {obs}", obs);
await CalculateAsistResp(obs);
}
// Oxygenation index calculation: triggered by air pressure, FiO2, or PaO2 readings
if (obs.Name is "AirPressure_Mean" or "FiO2" or "PaO2") await CalculateOxygenationIndex(obs);
// Ventilation mode evaluation: processes respiratory mode changes
if (obs.Name == "Resp_Mode")
{
_logger.LogDebug("Mapping Resp mode observation {obs}", obs);
await CalculateVentilationMode(obs);
}
// Mean arterial pressure (TAm) alerting: includes gestational age considerations
if (obs.Name is "TAm" or "Age_Gestational_Fixed" or "Age_Gestational") await CalculateTAmAlert(obs);
// Weight parsing: only for active (non-expired) patient observations
if (obs.Name is "Weight_Newborn" or "Weight_Current")
if (obs is PatientObservation { Expired: false })
obs = (T)await ParseWeight(obs);
// Temperature gradient calculation between incubator and patient
if (obs is { Name: "Temp_Incubator" } or { Name: "Temp_Patient" }) await CalculateTempGradient(obs);
// Intravenous lines observation processing with potential transformation
if (obs is { Name: "IntravenousLinesObs" })
{
var intraObs = await CalculateIntravenousLineObservation(obs);
// Apply the transformed observation if calculation produced a result
if (intraObs != null)
obs = (T)intraObs;
}
// Regional brain saturation monitoring: pCO2tc or transcutaneous O2 with specific codes
if (obs.Name is "pCO2tc" or "Transcutaneous_O2")
if (obs.Code != null && _regionalBrainSaturation.Contains(obs.Code))
await CalculateMonitor(obs);
// Surgery-related expiration check
if (obs.Name == "Surgery") await CheckSurgeryExpired(obs);
// Complexity calculations for predefined complex observation types
if (obs.Name != null && _complexityObservations.Contains(obs.Name))
{
_logger.LogDebug("Mapping observation Complexity {obs}", obs);
await CalculateComplexity(obs);
}
// Hourly accumulation observations: diuresis, drainages, hydric balance, and fluid entries
// These check for existing observations at the same time and increment values
if (obs.Name != null && (obs.Name.Equals("Diuresis_Hour") ||
obs.Name.Equals("Drainages_Hour") ||
obs.Name.Equals("Hydric_Balance") ||
obs.Name.Equals("IV_Entries_Hour") ||
obs.Name.Equals("Enteral_Entries_Hour")))
obs = (T)await CheckObsExistsAndIncrementTime(obs);
// Temperature observations: handle duplicate time entries by incrementing time
if (obs.Name != null && (obs.Name.Equals("Temp_Patient") ||
obs.Name.Equals("Temp_Incubator")
))
obs = (T)await CheckObsWithSameTimeExistsAndIncrementTime(obs);
return obs;
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> to its processed form, computing the end time of single-dose
/// treatments, evaluating the medicines associated with the treatment's codes/notes, and dispatching
/// the appropriate downstream calculations (bolus, monitor, surgery, ECMO complexity, and ONi observations)
/// based on the configured code catalogues.
/// </summary>
/// <param name="treatment">The <see cref="PatientTreatment"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation. The task result contains the processed <see cref="PatientTreatment"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="treatment"/> parameter is <c>null</c>.</exception>
/// <!-- aidoc-review:v1 severity=high kind=extra_exception
/// "ArgumentNullException is documented but the method body never throws it; no null check exists for the treatment parameter, so a null argument would result in a NullReferenceException, not ArgumentNullException." -->
public async Task<PatientTreatment> Map(PatientTreatment treatment)
{
if (treatment.SingleDose)
treatment = CalculateSingleDoseEndDate(treatment);
var tempTreatment = treatment;
await CheckTreatmentMedicines(tempTreatment);
if (tempTreatment.RequestedGiveCodes.Any())
{
if (tempTreatment.RequestedGiveCodes.Any(t =>
_medicationBolus.Contains(t.Identifier))) //remove && t.codingSystem == "FTPCS" not match always
{
_logger.LogDebug("Medication Bolus detected. Creating calculated observation");
await CalculateBolus(tempTreatment);
}
if (tempTreatment.RequestedGiveCodes.Any(t => _electroencephalogram.Contains(t.Identifier)))
await CalculateMonitor(tempTreatment);
if (tempTreatment.RequestedGiveCodes.Any(t =>
_surgery.Contains(t.Identifier) && _surgeryText.Contains(t.Text)))
await CalculateSurgery(tempTreatment);
if (treatment.RequestedGiveCodes.Any(t => _ecmo.Contains(t.Identifier)))
//If ecmo treatment is NW recalculate complexity to show ECMO if is DC recalculate complexity
await CalculateComplexityOnEcmo(treatment);
if (tempTreatment.RequestedGiveCodes.Any(t => _oniCodes.Contains(t.Identifier)))
{
if (tempTreatment.OrderControl == OrderControlType.Nw)
{
var oniObservation = new PatientObservation
{
PatientId = treatment.PatientId,
Code = "84481000140102",
Name = "ONi",
CodingSystem = "MDC",
Value = tempTreatment.RequestedGiveAmountMinimum ?? '-',
Time = tempTreatment.OrderTime ?? DateTime.Now,
Units = "ppm"
};
await _observationService.Value.InsertObservation(oniObservation, mapObs: false);
}
if (treatment.OrderControl == OrderControlType.Dc)
{
var lastOnis = await _observationService.Value.FindLastObservations(treatment.PatientId, 1,
["ONi"]);
var lastOni = lastOnis.FirstOrDefault();
if (lastOni == null) return treatment;
lastOni.Expired = true;
await _observationService.Value.UpdateObservation(lastOni);
await _observationService.Value.InsertObservation(lastOni, false);
}
}
}
return treatment;
}
/// <summary>
/// Identity mapping for a <see cref="PumpObservation"/>: returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract; pump observations
/// do not currently require UCIN-specific calculation.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A task containing the same <see cref="PumpObservation"/> instance that was passed in.</returns>
/// <!-- aidoc:v1 sig=50be4c1 body=e481734 -->
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Convenience overload that recalculates the "Medication" and "ERMedication" observations for a
/// patient using the supplied list of active medicines, building a transient <see cref="PatientTreatment"/>
/// for the underlying call.
/// </summary>
/// <param name="activeMedicines">The list of active <see cref="Medicine"/> instances currently associated with the patient.</param>
/// <param name="patientId">The unique identifier of the patient.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
/// <!-- aidoc:v1 sig=299c0fe body=f6867d8 -->
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
await CalculateMedicineObservation(activeMedicines, [], new PatientTreatment { PatientId = patientId });
}
/// <summary>
/// Recalculates the "OpiateBoluses" observation for a patient based on the bolus treatments
/// returned by <c>GetActiveBolus</c>. Skips persistence when the value is unchanged from the
/// last stored observation.
/// </summary>
/// <param name="patientId">The unique identifier of the patient.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
/// <!-- aidoc:v1 sig=2aadb2d body=f1796c6 -->
public async Task CalculateActiveBolus(ObjectId patientId)
{
// SIN RXA
/*
* var activeBolus = treatmentService.Value.GetActiveTreatmentsByPatient(patientId)
.FindAll(p => p.requestedGiveCodes.Any(r => MedicationBolus.Contains(r.identifier)) && p.orderTime > DateTime.Now.AddHours(-12) && p.boloPom);
var bolusObs = new PatientObservation()
{
patientid = patientId,
time = DateTime.Now,
//code = "MedicationBolus", //treatment.requestedGiveCode.identifier,
//codingSystem = "ADAS", //treatment.requestedGiveCode.codingSystem,
name = "OpiateBoluses",
codingSystem = "ADAS",
value = activeBolus.Count
};
var lastBolus = observationService.Value.FindLastObservations(patientId, 1, new List<string> { "OpiateBoluses" }).FirstOrDefault();
if (lastBolus != null && int.Parse(lastBolus.value.ToString()) == int.Parse(bolusObs.value.ToString()))
{
return;
}
observationService.Value.InsertObservation(bolusObs);
*/
//Por RXA
var activeBolus = await GetActiveBolus(patientId, null);
var bolusObs = new PatientObservation
{
PatientId = patientId,
Time = DateTime.Now,
//code = "MedicationBolus", //treatment.requestedGiveCode.identifier,
//codingSystem = "ADAS", //treatment.requestedGiveCode.codingSystem,
Name = "OpiateBoluses",
CodingSystem = "ADAS",
Value = activeBolus.Count
};
var lastsBolus = await _observationService.Value.FindLastObservations(patientId, 1, ["OpiateBoluses"]);
var lastBolus = lastsBolus.FirstOrDefault();
if (lastBolus != null && int.TryParse(lastBolus.Value.ToString(), out var lastbolus) &&
int.TryParse(bolusObs.Value.ToString(), out var bolusobs) && lastbolus == bolusobs) return;
await _observationService.Value.InsertObservation(bolusObs, mapObs: false);
}
/// <summary>
/// Retrieves all treatments currently considered active for the specified patient, delegating
/// the actual retrieval to the configured <see cref="ITreatmentService"/>.
/// </summary>
/// <param name="id">The unique identifier of the patient.</param>
/// <returns>A collection of active <see cref="PatientTreatment"/> objects for the patient.</returns>
/// <!-- aidoc:v1 sig=1c9e799 body=2a62eb7 -->
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
return activeTreatments;
}
/// <summary>
/// Identity mapping for a <see cref="PatientDiagnosis"/>: returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract; diagnoses do not
/// currently require UCIN-specific calculation.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to map.</param>
/// <returns>A task containing the same <see cref="PatientDiagnosis"/> instance that was passed in.</returns>
/// <!-- aidoc:v1 sig=8ddf601 body=ad326e6 -->
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
/// <summary>
/// Resolves time-inconsistencies for a new patient observation when it arrives with the same
/// (down-to-the-second) timestamp as the latest stored observation of the same name. The new
/// observation's <c>Time</c> is shifted forward by one second, and a fresh <see cref="ObjectId"/>
/// is generated to avoid duplicate-key collisions.
/// </summary>
/// <param name="newObservation">The new patient observation to evaluate for time inconsistencies.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains the fixed
/// <see cref="PatientObservation"/> if the input had a valid name; otherwise, <see langword="null"/>.
/// </returns>
/// <!-- aidoc-review:v1 severity=low kind=wrong_summary
/// "The summary states the trigger is when the new observation arrives with the 'same (down-to-the-second) timestamp', but the code's DateTime.Compare >= 0 condition also triggers the shift when the new observation's truncated time is strictly earlier than the last observation's." -->
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
{
_logger.LogError("Observation name is null or empty: {newObservation}", newObservation);
return null;
}
var lastObservations =
await _observationService.Value.FindLastObservations(newObservation.PatientId, 1, [newObservation.Name]);
var lastObservation = lastObservations.FirstOrDefault();
if (lastObservation != null && DateTime.Compare(
new DateTime(lastObservation.Time.Year, lastObservation.Time.Month,
lastObservation.Time.Day, lastObservation.Time.Hour,
lastObservation.Time.Minute, lastObservation.Time.Second),
new DateTime(newObservation.Time.Year, newObservation.Time.Month,
newObservation.Time.Day, newObservation.Time.Hour,
newObservation.Time.Minute, newObservation.Time.Second)
) >= 0)
newObservation.Time = lastObservation.Time.AddSeconds(1);
newObservation.Id = ObjectId.GenerateNewId();
return newObservation;
}
/// <summary>
/// Identity mapping for a list of <see cref="PatientObservation"/> instances: returns the supplied
/// list unchanged, wrapped in a completed task. Provided to satisfy the customization contract;
/// the pre-mapping phase does not currently require UCIN-specific transformation.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped.</param>
/// <returns>A task containing the original list of patient observations.</returns>
/// <!-- aidoc:v1 sig=5a37e9b body=0791af0 -->
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Identity mapping for a source alarm observation paired with a <see cref="PatientObservationAlarm"/>:
/// returns the supplied observation unchanged, wrapped in a completed task. Provided to satisfy the
/// customization contract; alarms are not currently transformed in the UCIN customization.
/// </summary>
/// <param name="obs">The patient observation that triggered the alarm.</param>
/// <param name="alarmToInsert">The alarm metadata to be inserted alongside the observation.</param>
/// <returns>A task containing the same <see cref="PatientObservation"/> instance that was passed in.</returns>
/// <!-- aidoc:v1 sig=405db8e body=ea790e4 -->
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Placeholder alarm dispatch hook used by the customization contract. The UCIN customization
/// does not currently implement custom alarm dispatching; calling this method always throws.
/// </summary>
/// <param name="obs">The patient observation that triggered the alarm.</param>
/// <param name="name">The alarm display name.</param>
/// <param name="code">The optional alarm code from <see cref="AlarmEnum.Name"/>.</param>
/// <returns>Never returns a result.</returns>
/// <exception cref="NotImplementedException">Always thrown because alarm dispatch is not implemented in the UCIN customization.</exception>
/// <!-- aidoc:v1 sig=90426a4 body=bfa6f2f -->
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// When a "Surgery" observation is marked as expired, inserts a follow-up "Surgery" observation
/// with value <c>0</c> at the same time plus one second, signaling that the surgery complexity
/// contribution has been removed. Errors are logged and rethrown.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with <c>Name == "Surgery"</c>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
/// <exception cref="InvalidCastException">Thrown when <paramref name="obs"/> cannot be cast to <see cref="PatientObservation"/>.</exception>
/// <exception cref="Exception">Rethrown after the underlying exception is written to the console for diagnostics.</exception>
/// <!-- aidoc-review:v1 severity=low kind=wrong_summary
/// "Summary states 'Errors are logged' but the code uses Console.WriteLine for exception output, not the _logger. The companion <exception> tag correctly describes console output, so the summary is mildly inconsistent with the actual behavior." -->
public async Task CheckSurgeryExpired(BasePatientObservation obs)
{
try
{
var pobs = (PatientObservation)obs;
if (!pobs.Expired) return;
var surgeryObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Surgery",
Min = 0,
Max = 5,
CodingSystem = "ADAS",
Time = obs.Time.AddSeconds(1),
Value = 0,
Expires = pobs.Expires
};
_logger.LogDebug("CheckSurgeryExpired. Inserting: {obs}", surgeryObs.ToString());
await _observationService.Value.InsertObservation(surgeryObs, mapObs: false);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// For hourly accumulation observations, checks whether another observation with the same name
/// already exists within the same hour and, if so, shifts this observation's <c>Time</c> forward
/// by one second so the system can identify the most recent entry.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the (possibly time-shifted) base patient observation.</returns>
/// <!-- aidoc:v1 sig=02a8359 body=9a6b741 -->
public async Task<BasePatientObservation> CheckObsExistsAndIncrementTime(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var lastObsFromHour =
await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59), obs.Name);
if (lastObsFromHour != null &&
DateTime.Compare(new DateTime(lastObsFromHour.Time.Year, lastObsFromHour.Time.Month,
lastObsFromHour.Time.Day, lastObsFromHour.Time.Hour, 0, 0)
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
pobs.Time = lastObsFromHour.Time.AddSeconds(1);
return pobs;
}
/// <summary>
/// For temperature observations, checks whether another observation with the same name already
/// exists at the exact same timestamp and, if so, shifts this observation's <c>Time</c> forward
/// by one second so the system can identify the most recent entry.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the (possibly time-shifted) base patient observation.</returns>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "The summary claims the method is for 'temperature observations', but the code contains no temperature-specific logic — it works for any observation type passed in, using whatever Name is on the observation." -->
public async Task<BasePatientObservation> CheckObsWithSameTimeExistsAndIncrementTime(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var existObsWithSameTime =
await _observationService.Value.FindAnyWithSameDate(pobs.PatientId, pobs.Time, pobs.Name);
if (existObsWithSameTime != null && existObsWithSameTime.Any()) pobs.Time = pobs.Time.AddSeconds(1);
return pobs;
}
/// <summary>
/// Computes the Oxygenation Index (PMAP × FiO2 × 100 / PaO2) using the supplied observation plus
/// the latest stored <c>AirPressure_Mean</c>, <c>FiO2</c>, and <c>PaO2</c> values. Persists the
/// resulting <c>Oxygenation_Index</c> observation with a 10-second expiration matching FiO2's
/// expiration window. Skipped gracefully when PaO2 is zero, when any of the inputs cannot be
/// parsed, or when an exception is caught and logged.
/// </summary>
/// <param name="obs">The observation that triggered the calculation (one of <c>AirPressure_Mean</c>, <c>FiO2</c>, or <c>PaO2</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
/// <!-- aidoc:v1 sig=3429ff2 body=103e384 -->
public async Task CalculateOxygenationIndex(BasePatientObservation obs)
{
var toSearchList = new List<string>();
PatientObservation? airPressMeanObs = null;
PatientObservation? fiO2Obs = null;
PatientObservation? paO2Obs = null;
try
{
toSearchList.AddRange(new List<string> { "AirPressure_Mean", "FiO2", "PaO2" });
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
switch (obs.Name)
{
case "AirPressure_Mean":
airPressMeanObs = (PatientObservation)obs;
fiO2Obs = values.FirstOrDefault(o => o.Name == "FiO2");
paO2Obs = values.FirstOrDefault(o => o.Name == "PaO2");
break;
case "FiO2":
fiO2Obs = (PatientObservation)obs;
airPressMeanObs = values.FirstOrDefault(o => o.Name == "AirPressure_Mean");
paO2Obs = values.FirstOrDefault(o => o.Name == "PaO2");
break;
case "PaO2":
paO2Obs = (PatientObservation)obs;
fiO2Obs = values.FirstOrDefault(p => p.Name == "FiO2");
airPressMeanObs = values.FirstOrDefault(o => o.Name == "AirPressure_Mean");
break;
}
//Controlar que fio2 no haya expirada al recogerla
if (fiO2Obs != null && paO2Obs != null && airPressMeanObs != null)
if (int.TryParse(airPressMeanObs.Value.ToString(), out var airPressureMean) &&
int.TryParse(fiO2Obs.Value.ToString(), out var fio2) &&
int.TryParse(paO2Obs.Value.ToString(), out var pao2))
{
if (pao2 == 0)
{
_logger.LogWarning(
"Calculate Oxygenation Index observation. PaO2: {pao2}, observation: {obsName}", pao2,
obs.Name);
return;
}
var oxygenationIndexValue = airPressureMean * fio2 * 100 / pao2;
var oxygenationIndexObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Oxygenation_Index",
CodingSystem = "ADAS",
Value = oxygenationIndexValue,
Time = DateTime.Now,
Expires = 10 //Expires in 10 seg because its FiO2 expire time.
};
await _observationService.Value.InsertObservation(oxygenationIndexObs, mapObs: false);
}
}
catch (Exception ex)
{
_logger.LogError("Error calculate Oxygenation Index observation. Exception: {exMessage}", ex.Message);
}
}
/// <summary>
/// Sets the <c>EndTime</c> of a single-dose treatment to 8 hours after its <c>OrderTime</c>
/// (or 8 hours from now when <c>OrderTime</c> is <see langword="null"/>).
/// </summary>
/// <param name="treatment">The single-dose treatment whose end time is to be calculated.</param>
/// <returns>The same <see cref="PatientTreatment"/> instance with its <c>EndTime</c> updated.</returns>
/// <!-- aidoc:v1 sig=271263e body=4ddb3f4 -->
private static PatientTreatment CalculateSingleDoseEndDate(PatientTreatment treatment)
{
/* Deprecated calc finish treatment date on shift end. Now every treatment single dose is last 8 Hours
DateTime now = DateTime.UtcNow;
//Turno actual, tenemos que poner como fecha fin la fecha de inicio del turno siguiente
var shiftNow = Shift.Where(
i => DateTime.ParseExact(i, "HH:mm", CultureInfo.InvariantCulture).Ticks < DateTime.Now.Ticks
).LastOrDefault();
var index = Shift.IndexOf(shiftNow);
string endDateShift;
if (Shift.Count <= index + 1) endDateShift = Shift[0];
else endDateShift = Shift[index + 1];
var HourMinutes = endDateShift.Split(':');
if (now.Hour.CompareTo(int.Parse(HourMinutes[0])) > 0)
now = now.AddDays(1);
DateTime finisTreatmentDate = new DateTime(now.Year, now.Month, now.Day, int.Parse(HourMinutes[0]), int.Parse(HourMinutes[1]), 0);
*/
treatment.EndTime = treatment.OrderTime?.AddHours(8) ?? DateTime.Now.AddHours(8);
return treatment;
}
/// <summary>
/// Resolves the medicines associated with a treatment (by matching its codes and notes against the
/// medicine catalogue, falling back to <c>NotesIndicatingMedication</c> patterns), aggregates the
/// patient's active treatments and medicines, detects parenteral nutrition (NPT) treatments, and
/// triggers <c>CalculateMedicineObservation</c> to update the Medication and ERMedication observations.
/// </summary>
/// <param name="treatment">The treatment whose medicines should be checked.</param>
/// <returns>A task that represents the asynchronous check operation.</returns>
/// <!-- aidoc:v1 sig=26b9617 body=0b5d20b -->
public async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
//sI TIENE UNA NOTA CON NPT SON DE TIPO NUTRICIÓN PARENTERAL Y SUMAN 1
var listNotesCode = treatment.Notes.Select(note => note.Comment).ToList();
if (treatment.RequestedGiveCodes.Any())
listNotesCode.AddRange(treatment.RequestedGiveCodes.Select(treatmentCodes => treatmentCodes.Identifier));
//var medicines = medicineService.Value.GetByCodeOrNote(listNotesCode);
var medicines = await _medicineService.GetByCodeOrNote(listNotesCode);
var parentalNutritionMedicine = await CalculateParentalNutritionMedicine(treatment);
medicines.Add(parentalNutritionMedicine);
//If not detect any medicine in medicine table, check if contáis NTE with, if it's true, it's a medicine.
/*
NTE|||Medicación|^OrderDefinition\r
NTE|||Perfusiones|^OrderDefinition\r
NTE|||SUEROS Y HEMODER|^OrderDefinition\r
*/
if (medicines is { Count: 0 })
{
var isMedicine = treatment.Notes.Any(n => _notesIndicatingMedication.Contains(n.Comment));
if (isMedicine && treatment.RequestedGiveCodes is { Count: > 0 })
treatment.RequestedGiveCodes.ForEach(c =>
{
if (!string.IsNullOrEmpty(c.Identifier))
medicines.Add(new Medicine
{
Codes = [c.Identifier],
Name = c.Text
});
});
}
if (medicines.Any())
{
if (!string.IsNullOrEmpty(treatment.RequestedGiveTreatment))
try
{
medicines =
[
new Medicine
{
Name = treatment.RequestedGiveTreatment,
Type = medicines.FindAll(t => t.Type.Any())
.Select(m => m.Type.Aggregate((x, y) => x + "," + y)).Distinct().ToList(),
Codes = medicines.FindAll(t => t.Codes.Any())
.Select(m => m.Codes.Aggregate((x, y) => x + "," + y)).Distinct().ToList(),
Group = medicines.FindAll(t => t.Group.Any())
.Select(m => m.Group.Aggregate((x, y) => x + "," + y)).Distinct().ToList(),
Notes = medicines.FindAll(t => t.Notes.Any())
.Select(m => m.Notes.Aggregate((x, y) => x + "," + y)).Distinct().ToList()
}
];
}
catch (Exception ex)
{
_logger.LogError("Error Checking Treatment Medicines. {medicines} . Exception {ex}",
string.Join(",", medicines), ex);
}
var _ = await GetActiveTreatmentsByPatient(treatment.PatientId);
var activeTreatments = _.ToList();
//var activeMedicines = medicineService.Value.GetMedicinesOfTreatments(activeTreatments);
var __ = await _medicineService.GetMedicinesOfTreatments(activeTreatments);
var activeMedicines = __.ToList();
var activeNptTreatments =
activeTreatments.FindAll(t => t?.Notes.FirstOrDefault(n => n.Comment == "NPT") != null);
if (activeNptTreatments.Count > 0)
{
//activeMedicines.AddRange((IEnumerable<Medicine>)activeNPTTreatments.Select(async s => await CalculateParentalNutritionMedicine(s)).Where(parentalMedicine => parentalMedicine != null));
//activeMedicines.AddRange(await Task.WhenAll(activeNPTTreatments.Select(async s => await CalculateParentalNutritionMedicine(s))).Where(parentalMedicine => parentalMedicine != null));
var tasks = activeNptTreatments.Select(async s => await CalculateParentalNutritionMedicine(s));
var taskResults = await Task.WhenAll(tasks);
var filteredMedicines = taskResults.ToList();
if (filteredMedicines.Any()) filteredMedicines.ForEach(m => { activeMedicines.Add(m); });
}
await CalculateMedicineObservation(activeMedicines, medicines, treatment);
}
}
/// <summary>
/// Builds a synthetic <see cref="Medicine"/> representing parenteral nutrition (NPT) when the
/// treatment's notes indicate a NPT order. Lipid-based NPTs are tagged with
/// <c>ParenteralNutritionLipids</c>; all other NPTs are tagged with <c>ParenteralNutrition</c>.
/// </summary>
/// <param name="treatment">The treatment to inspect for NPT indicators, or <see langword="null"/>.</param>
/// <returns>A task that resolves to a <see cref="Medicine"/> instance describing the parenteral nutrition, or an unnamed, untyped instance when no NPT notes are present.</returns>
/// <!-- aidoc:v1 sig=d078407 body=3e76d41 -->
private static Task<Medicine> CalculateParentalNutritionMedicine(PatientTreatment? treatment)
{
List<string> medicineType = [];
Note? commentType = null;
if (treatment?.Notes.FirstOrDefault(n => n.Comment == "NPT") != null)
{
commentType = treatment.Notes.FirstOrDefault(n => n.CommentType == "formularybaseformulation");
if (treatment.Notes.FirstOrDefault(n =>
n.Comment is "LÍPIDOS NEONATALES AL 20%" or "LÍPIDOS NEONATALES AL 20% CON...") != null)
medicineType = [nameof(MedicineEnum.Types.ParenteralNutritionLipids)];
else
medicineType = [nameof(MedicineEnum.Types.ParenteralNutrition)];
}
var medicine = new Medicine
{
Type = medicineType,
Name = commentType != null ? commentType.Comment : "UnNamed"
};
return Task.FromResult(medicine);
}
/// <summary>
/// Maintains the patient's active medicine list according to the treatment's <c>OrderControl</c>
/// (<c>NW</c> adds, <c>XO</c> adds if missing, <c>DC</c> removes), counts the distinct medicine
/// types, persists the <c>Medication</c> observation if its value changed, and then recalculates
/// the <c>ERMedication</c> observation.
/// </summary>
/// <param name="activeMedicines">The list of active medicines for the patient. Modified in-place.</param>
/// <param name="medicines">The medicines to add or remove depending on <c>OrderControl</c>.</param>
/// <param name="treatment">The treatment that triggered the recalculation.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
/// <!-- aidoc:v1 sig=0ed4852 body=363a6db -->
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, List<Medicine> medicines,
PatientTreatment treatment)
{
var medicinesWithType = activeMedicines.FindAll(m => m.Type.Any());
foreach (var medicine in medicines)
if (treatment.OrderControl is OrderControlType.Nw or OrderControlType.Xo)
{
if (treatment.StartTime != null && DateTime.UtcNow.CompareTo(treatment.StartTime) < 0) return;
if (treatment.OrderControl == OrderControlType.Nw)
activeMedicines.Add(medicine);
if (treatment.OrderControl == OrderControlType.Xo
&& activeMedicines.All(m => m.Name != medicine.Name)) activeMedicines.Add(medicine);
if (medicinesWithType != null &&
medicinesWithType.All(m => m.Type != medicine.Type))
medicinesWithType.Add(medicine);
}
else if (treatment.OrderControl == OrderControlType.Dc)
{
if (medicinesWithType != null)
{
medicinesWithType.Remove(medicine);
activeMedicines.RemoveAll(m => m.Name == medicine.Name);
}
}
var medicineTypes = medicinesWithType!.SelectMany(m => m.Type.Select(t => new { type = t }))
.GroupBy(x => x.type).Count();
var medicineObs = new PatientObservation
{
PatientId = treatment.PatientId,
Name = "Medication",
CodingSystem = "ADAS",
Value = medicineTypes,
Time = DateTime.Now
};
var lastMedicationsObs = await _observationService.Value.FindLastObservations(treatment.PatientId, 1,
["Medication"]);
var lastMedicationObs = lastMedicationsObs.FirstOrDefault();
if (lastMedicationObs == null || !lastMedicationObs.Value.Equals(medicineObs.Value))
await _observationService.Value.InsertObservation(medicineObs, mapObs: false);
await CalculateErMedication(activeMedicines, treatment);
}
/// <summary>
/// Calculates the patient's risk level from their active medicines and persists the
/// <c>ERMedication</c> observation with min/max bounds of 0 and 5, skipping persistence
/// when the value matches the last stored observation.
/// </summary>
/// <param name="activeMedicines">The list of active medicines used to derive the risk level.</param>
/// <param name="treatment">The treatment that triggered the recalculation.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
/// <!-- aidoc:v1 sig=b13c549 body=24f69f5 -->
private async Task CalculateErMedication(List<Medicine> activeMedicines, PatientTreatment treatment)
{
//var activeTreatments = treatmentService.Value.GetActiveTreatmentsByPatient(treatment.patientid);
var result = await CalculateErMedicineLevels(activeMedicines);
var erMedicationObs = new PatientObservation
{
Name = "ERMedication",
CodingSystem = "ADAS",
PatientId = treatment.PatientId,
Time = DateTime.Now,
Min = 0,
Max = 5,
Value = result
};
var lastErMedicationsObsList = await _observationService.Value.FindLastObservations(treatment.PatientId, 1,
["ERMedication"]);
var lastErMedicationObs = lastErMedicationsObsList.FirstOrDefault();
if (lastErMedicationObs == null || !lastErMedicationObs.Value.Equals(erMedicationObs.Value))
await _observationService.Value.InsertObservation(erMedicationObs, mapObs: false);
}
/// <summary>
/// Maps the patient's active medicine list to a discrete risk level (0 to 5) based on medicine
/// types and groups: lipidic parenteral nutrition or multiple high-risk medicines map to 5,
/// non-lipid NPT or one or two high-risk medicines map to 4, metabolic medicines map to 2,
/// remaining medicines map to 1, and a list containing only iron/vitamin D maps to 0.
/// </summary>
/// <param name="activeMedicines">The list of active medicines to evaluate.</param>
/// <returns>A task that resolves to the integer risk level (05).</returns>
/// <!-- aidoc:v1 sig=3c6a3b1 body=606ffd9 -->
private static Task<int> CalculateErMedicineLevels(List<Medicine> activeMedicines)
{
var ironVitaminDCodes = new List<string> { "374424002", "175041000140104" };
//Aquí a cambiar que la nutrición parenteral siempre sume 4 no 5 quitar de medicinas de 5 solo suma 5 en caso de ser
//Los lípidos son dos órdenes de NP con el segmento RXO6.2 (NPT: 1 ml LÍPIDOS NEONATALES AL 20% CON MEDICAMENTOS 1 o
//NPT: 1 ml LÍPIDOS NEONATALES AL 20% 1)
var medicines5Points = activeMedicines.Where(m =>
m.Type.Contains(nameof(MedicineEnum.Types.Pge1)) ||
m.Type.Contains(nameof(MedicineEnum.Types.Insulin)) ||
m.Group.Contains(nameof(MedicineEnum.Group.DoubleSignature)) ||
m.Group.Contains(nameof(MedicineEnum.Group.Vasoactive)) ||
m.Type.Contains(nameof(MedicineEnum.Types.ParenteralNutritionLipids))
)
.ToList();
if (medicines5Points.Count > 2 ||
activeMedicines.FirstOrDefault(m =>
m.Type.Contains(nameof(MedicineEnum.Types.ParenteralNutritionLipids))) !=
null) return Task.FromResult(5);
if (medicines5Points.Count is > 0 and <= 2 ||
activeMedicines.FirstOrDefault(m => m.Type.Contains(nameof(MedicineEnum.Types.ParenteralNutrition))) !=
null
) return Task.FromResult(4);
var medicines2Points = activeMedicines.Where(m =>
m.Group.Contains(nameof(MedicineEnum.Group.Metabolic))
).ToList();
if (medicines2Points.Count > 0) return Task.FromResult(2);
//All medicines without type/group and not vitamin D or Iron
var medicines1Point = activeMedicines.FindAll(m => !m.Codes.Any(c => ironVitaminDCodes.Contains(c)));
if (medicines1Point.Count > 0) return Task.FromResult(1);
var ironVitaminDMedicines = activeMedicines.Where(m =>
m.Codes.Any(c => ironVitaminDCodes.Contains(c))
).ToList();
if (ironVitaminDMedicines.Count == activeMedicines.Count || activeMedicines.Count == 0)
return Task.FromResult(0);
return Task.FromResult(1);
}
/// <summary>
/// Maps a <c>Resp_Mode</c> observation's value to a discrete <c>Resp_Type</c> (HighFrequencyVentilation,
/// Invasive, NonInvasive, or None) by matching it against the configured catalogues for each mode.
/// </summary>
/// <param name="obs">The respiratory-mode observation expected to be a <see cref="PatientObservation"/>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
/// <!-- aidoc:v1 sig=a32f888 body=5994877 -->
public async Task CalculateVentilationMode(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var respTypeObs = new PatientObservation
{
Name = "Resp_Type",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time
};
var strValue = pobs.Value.ToString();
respTypeObs.Value = _highFrequencyVentilation.Contains(strValue ?? string.Empty)
? nameof(RespirationType.HighFrequencyVentilation)
: _invasiveVentilation.Contains(strValue ?? string.Empty)
? respTypeObs.Value = nameof(RespirationType.Invasive)
: _nonInvasiveVentilation.Contains(strValue ?? string.Empty)
? respTypeObs.Value = nameof(RespirationType.NonInvasive)
: nameof(RespirationType.None);
await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
}
/// <summary>
/// Calculates the patient's respiratory assistance score from the supplied observation's
/// <c>Value</c> (matched against the configured respiratory-type catalogue) or from the latest
/// stored <c>Resp_Mode</c> when the supplied observation is <c>ONi</c>. Active ONi treatment
/// (or a non-expired last ONi observation) forces the score to 10. Persists the resulting
/// <c>Respiratory</c> observation and returns the original input unchanged.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (expected to be a <see cref="PatientObservation"/>).</param>
/// <returns>A task that resolves to the same base patient observation that was passed in.</returns>
/// <!-- aidoc:v1 sig=dbb815a body=5ae4249 -->
public async Task<BasePatientObservation> CalculateAsistResp(BasePatientObservation obs)
{
//Comprobar si hay un resp_type más nuevo y si no lo hay ignorar
var pobs = (PatientObservation)obs;
var lastsAsistResp = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Resp_Mode"]);
var lastAsistResp = lastsAsistResp.FirstOrDefault();
if (lastAsistResp != null && lastAsistResp.Time.CompareTo(obs.Time) > 0 &&
lastAsistResp.Name != "ONi") return obs;
var calculatedRespiratory = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.Now,
CodingSystem = "ADAS",
Name = "Respiratory",
Value = 0
};
if (obs.Name == "ONi")
{
//find last assist resp obs
if (lastAsistResp != null)
{
var respiratoryValue = _respiratoryTypeValue.FirstOrDefault(r => r.Item3.Contains(lastAsistResp.Value));
if (respiratoryValue != null) calculatedRespiratory.Value = respiratoryValue.Item2;
}
}
else
{
var respiratoryValue = _respiratoryTypeValue.FirstOrDefault(r => r.Item3.Contains(pobs.Value));
if (respiratoryValue != null) calculatedRespiratory.Value = respiratoryValue.Item2;
}
var isIno = !string.IsNullOrEmpty(obs.Code) && _oniCodes.Contains(obs.Code);
if (isIno && !pobs.Expired) calculatedRespiratory.Value = 10;
//Check if last ino exist and is not expired
var lastsIno = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["ONi"]);
var lastIno = lastsIno.FirstOrDefault();
if (lastIno is { Expired: false }) calculatedRespiratory.Value = 10;
await _observationService.Value.InsertObservation(calculatedRespiratory, mapObs: false);
return obs;
}
/// <summary>
/// Updates the synthetic <c>ECMO</c> observation according to the treatment's <c>OrderControl</c>:
/// <c>NW</c> sets it to <c>NW</c>, <c>DC</c> sets it to <c>XO</c> if any other ECMO treatment
/// remains active or to <c>DC</c> otherwise, and any other <c>OrderControl</c> is ignored.
/// In every case the change is followed by a complexity recalculation via <c>CalculateComplexity</c>.
/// </summary>
/// <param name="treatment">The treatment whose ECMO state is being applied.</param>
/// <returns>A task that represents the asynchronous recalculation operation.</returns>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "Documentation states 'In every case the change is followed by a complexity recalculation via CalculateComplexity', but the default case returns early without calling CalculateComplexity." -->
private async Task CalculateComplexityOnEcmo(PatientTreatment treatment)
{
var ecmoObs = new PatientObservation
{
PatientId = treatment.PatientId,
Name = "ECMO"
};
switch (treatment.OrderControl)
{
case OrderControlType.Nw:
ecmoObs.Value = OrderControlType.Nw;
break;
case OrderControlType.Dc:
var haveEcmo = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId);
ecmoObs.Value = haveEcmo.Any(t => t != null && t.RequestedGiveCodes.Any(r =>
_ecmo.Contains(r.Identifier) &&
t.PlacerOrder?.EntityIdentifier != treatment.PlacerOrder?.EntityIdentifier))
? OrderControlType.Xo
: OrderControlType.Dc;
break;
default:
return;
}
await CalculateComplexity(ecmoObs);
}
/// <summary>
/// Recalculates the patient's overall <c>Complexity</c> observation from scratch using the latest
/// values for the configured complexity-contributing observations (weight, medication, surgery,
/// intravenous lines, monitor, and respiratory), the active ECMO treatment state, and the
/// optionally supplied <paramref name="obs"/> which is preferred over the database value when
/// it is newer and not expired. The result is persisted only when its integer value differs
/// from the last stored complexity observation, with time inconsistencies resolved via
/// <c>FixTimeInconsistencyWithLast</c>. Returns the original observation unchanged.
/// </summary>
/// <param name="obs">The base patient observation that triggered the recalculation.</param>
/// <param name="ignoreObs">When <see langword="true"/>, the supplied <paramref name="obs"/> is not added to the calculation inputs (useful for synthetic observations like ECMO).</param>
/// <returns>A task that resolves to the same base patient observation that was passed in.</returns>
/// <!-- aidoc:v1 sig=b7809c7 body=cf3438b -->
public async Task<BasePatientObservation> CalculateComplexity(BasePatientObservation obs, bool ignoreObs = false)
{
try
{
var logUid = Guid.NewGuid();
var complexity = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = "ADAS",
Name = "Complexity",
Value = 0
};
var complexityValue = 0;
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(obs.PatientId);
var haveEcmo = activeTreatments.Any(t =>
t != null && t.RequestedGiveCodes.Any(r => _ecmo.Contains(r.Identifier)));
var pobs = obs as PatientObservation;
var complexityObservations =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, _complexityObservations);
//Nos acaba de entrar, la usamos por delante de la última de base de datos
//21-02-22 No es correcto, solo remover si el tiempo de esa observación que entra es más nuevo que la de bd
var actualObsInBd = complexityObservations.FirstOrDefault(o => o.Name == obs.Name);
if (actualObsInBd != null && obs.Time > actualObsInBd.Time)
complexityObservations.RemoveAll(it =>
it.Name == obs.Name);
if (!ignoreObs && pobs is { Expired: false }) complexityObservations.Add(pobs);
foreach (var observation in complexityObservations)
switch (observation.Name)
{
case "Weight_Current":
// if(observation.expired) break; A este no le importa que esté expirado, lo tiene que hacer siempre.
var weightCurrentValue = await CalculateWeight(observation);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => Weight_Current plus complexity: {WeightCurrentValue}",
observation.PatientId, logUid, weightCurrentValue);
complexityValue += weightCurrentValue;
break;
case "Weight_Newborn": //Only count newborn weight when weight current don't exist.
if (complexityObservations.FirstOrDefault(o => o.Name == "Weight_Current") == null)
{
var weightNewbornValue = await CalculateWeight(observation);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => Weight_Newborn plus complexity: {Weight_NewbornValue}",
observation.PatientId, logUid, weightNewbornValue);
complexityValue += weightNewbornValue;
}
break;
case "Medication":
if (observation.Expired) break;
var value = observation.Value.ToString();
if (!int.TryParse(value, out var valueParsed))
valueParsed = 0;
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => Medication plus complexity: {valueParsed}",
observation.PatientId, logUid, valueParsed);
complexityValue += valueParsed;
break;
case "Surgery":
if (observation.Expired) break;
var surgeryValue = Convert.ToInt32(observation.Value);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => Surgery plus complexity: {surgeryValue}",
observation.PatientId, logUid, surgeryValue);
complexityValue += surgeryValue;
break;
case "IntravenousLines":
if (observation.Expired) break;
var intravenousLinesValue = Convert.ToInt32(observation.Value);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => IntravenousLines plus complexity: {IntravenousLinesValue}",
observation.PatientId, logUid, intravenousLinesValue);
complexityValue += intravenousLinesValue;
break;
case "Monitor":
if (observation.Expired) continue;
var monitorValue = Convert.ToInt32(observation.Value);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => monitor plus complexity: {monitorValue}",
observation.PatientId, logUid, monitorValue);
complexityValue += monitorValue;
break;
case "Respiratory":
if (observation.Expired) break;
var respiratoryValue = Convert.ToInt32(observation.Value);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => respiratory plus complexity: {respiratoryValue}",
observation.PatientId, logUid, respiratoryValue);
complexityValue += respiratoryValue;
break;
}
complexity.Value = complexityValue;
if (haveEcmo && obs.Name != "ECMO")
//complexity.value = $"ECMO ({complexityValue})";
complexity.Value = $"{complexityValue} ECMO";
if (complexityObservations.Any(o => o.Name == "ECMO"))
{
var ecmoObs = complexityObservations.FirstOrDefault(o => o.Name == "ECMO");
if (Enum.TryParse(typeof(OrderControlType), ecmoObs?.Value.ToString(), out var treatmentControl))
switch (treatmentControl)
{
case OrderControlType.Dc:
break;
//case OrderControlType.XO:
//case OrderControlType.NW:
default:
//complexity.value = $"ECMO ({complexityValue})";
complexity.Value = $"{complexityValue} ECMO";
//var pObs = await FixTimeInconsistencyWithLast(complexity);
if (pobs != null)
await _observationService.Value.InsertObservation(pobs);
break;
}
}
var lastsObsComplexity =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, [complexity.Name]);
var lastObsComplexity = lastsObsComplexity.FirstOrDefault();
if (lastObsComplexity is not null)
{
var lastObsParsed = int.TryParse(lastObsComplexity.Value.ToString(), out var lastObsValue);
if (lastObsParsed && int.TryParse(complexity.Value.ToString(), out var intValue) &&
lastObsValue == intValue) return obs;
}
var fixedTimeComplexity = await FixTimeInconsistencyWithLast(complexity);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => inserting complexity value: {complexityValue} for patient: {complexityPatientid} at time {fixedTimeComplexityTime}",
complexity.PatientId, logUid, complexity.Value, complexity.PatientId, fixedTimeComplexity?.Time);
if (fixedTimeComplexity != null)
await _observationService.Value.InsertObservation(fixedTimeComplexity, mapObs: false);
return obs;
}
catch (Exception ex)
{
_logger.LogError("Error calculating complexity. Exception: {ex}", ex);
return obs;
}
}
/// <summary>
/// Recomputes the <c>IntravenousLines</c> complexity score by reconciling the incoming
/// <c>IntravenousLinesObs</c> against the patient's active intravenous lines. Detects and
/// corrects common ICCA mis-clicks (inserts incorrectly carrying a remove time), updates an
/// existing line in place when only the duration changed, and otherwise adds the new line to
/// the active set. The aggregated score is capped at 10 and persisted with bounds [0, 10].
/// <c>InvalidCastException</c>s are logged and swallowed.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (expected to be a <see cref="PatientObservation"/> whose <c>Value</c> is a <see cref="PatientIntravenousLinesValue"/>).</param>
/// <returns>
/// A task that resolves to the original base patient observation when processing succeeds or
/// fails gracefully with logging; resolves to <see langword="null"/> when the function returns
/// early because the incoming observation is a duplicate insert (in which case the caller should
/// not overwrite the stored observation).
/// </returns>
/// <!-- aidoc:v1 sig=09f61b3 body=4d3947c -->
public async Task<BasePatientObservation?> CalculateIntravenousLineObservation(BasePatientObservation obs)
{
try
{
var pobs = (PatientObservation)obs;
var intravenousLineValue = (PatientIntravenousLinesValue)pobs.Value;
//Check for doctor miss click on ICCA. sometimes they put insert with remove time
//when it has a remove time always is remove, never insert
if (intravenousLineValue is { Action: "Insertado", RemoveTime: not null })
intravenousLineValue.Action = "Retirado";
//GetByCodeSysAndCode active Intravenous observations
//var intraVenousLineObservations = observationService.Value.FindLastObservations(obs.patientid, 1, new List<string> { obs.name });
List<PatientObservation?> activeIntraVenousLineObservations = [];
try
{
activeIntraVenousLineObservations =
await _observationService.Value.FindLastIntravenousLinesObservationByLocation(obs.PatientId);
}
catch (Exception ex)
{
_logger.LogWarning(
"Cannot transform to PatientObservation value to PatientIntravenousLinesValue {exMessage}",
ex.Message);
}
if (activeIntraVenousLineObservations.Any())
activeIntraVenousLineObservations = activeIntraVenousLineObservations.FindAll(v =>
v != null && ((PatientIntravenousLinesValue)v.Value).Action == "Insertado");
/*
var activeIntravenousLineObservations = intraVenousLineObservations.GroupBy(obs => (((PatientIntravenousLinesValue)obs.value).type,
((PatientIntravenousLinesValue)obs.value).insertTime,
((PatientIntravenousLinesValue)obs.value).location))
.Where(grp => grp.All(o => ((PatientIntravenousLinesValue)o.value).RemoveTime == null))
.SelectMany(group => group).ToList();
*/
//If intravenous obs have remove time, remove it from activeIntravenousLines before calculating.
if (intravenousLineValue.RemoveTime != null)
{
activeIntraVenousLineObservations.RemoveAll(o =>
o != null && ((PatientIntravenousLinesValue)o.Value).Location == intravenousLineValue.Location);
}
else
{
//If insert come with same data than other insert and same insertTime is update. Don't need to register it again as insert.is updated
var intravenousLineBeforeUpdate = activeIntraVenousLineObservations.FirstOrDefault(o => o != null &&
((PatientIntravenousLinesValue)o.Value).Location == intravenousLineValue.Location &&
Equals(((PatientIntravenousLinesValue)o.Value).InsertTime, intravenousLineValue.InsertTime) &&
((PatientIntravenousLinesValue)o.Value).Type == intravenousLineValue.Type &&
intravenousLineValue.RemoveTime == null);
if (intravenousLineBeforeUpdate != null)
{
//If duration changed Update register
if (((PatientIntravenousLinesValue)intravenousLineBeforeUpdate.Value).Duration ==
intravenousLineValue.Duration)
return null;
((PatientIntravenousLinesValue)intravenousLineBeforeUpdate.Value).Duration =
intravenousLineValue.Duration ?? string.Empty;
await _observationService.Value.UpdateObservation(intravenousLineBeforeUpdate);
return null;
}
if (!activeIntraVenousLineObservations.Any(o =>
o != null &&
((PatientIntravenousLinesValue)o.Value).Location == intravenousLineValue.Location &&
((PatientIntravenousLinesValue)o.Value).Type == intravenousLineValue.Type))
activeIntraVenousLineObservations.Add(pobs);
}
//Generate calculad intravenousLine obs
//var intraVenousLineScore2 = activeIntraVenousLineObservations.Select(activeObservation => IntraVenousLinesTypeValue.Where(i => i.Item3.Contains(((PatientIntravenousLinesValue)activeObservation.value).type))).Select(typeValue => typeValue.FirstOrDefault()?.Item2??0).Sum();
// Step 1: Select active observations and cast to PatientIntravenousLinesValue
var activeObservations = activeIntraVenousLineObservations.Select(activeObservation =>
(PatientIntravenousLinesValue?)activeObservation?.Value);
// Step 2: Match active observations with type-value mappings
// We use .Replace("\u00A0", " ") on type to avoid special codification for spaces between characters
var matchedValues = activeObservations.Select(activeObservation => _intraVenousLinesTypeValue.Where(i =>
activeObservation is { Type: not null } &&
i.Item3.Contains(activeObservation.Type.Replace("\u00A0", " "))))
.ToList();
// Step 3: Extract the values from matched tuples
var values = 0;
matchedValues.ForEach(typeValue => values += typeValue.FirstOrDefault()?.Item2 ?? 0);
// Step 4: Calculate the sum
var intraVenousLineScore = values;
if (intraVenousLineScore > 10) intraVenousLineScore = 10;
var intravenousObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "IntravenousLines",
Min = 0,
CodingSystem = "ADAS",
Max = 10,
Time = DateTime.UtcNow,
Value = intraVenousLineScore
};
var ob = await FixTimeInconsistencyWithLast(intravenousObs);
if (ob != null)
await _observationService.Value.InsertObservation(ob, mapObs: false);
}
catch (InvalidCastException ex)
{
_logger.LogError("CalculateIntravenousLineObservation {obs}: {exMessage}", obs, ex.Message);
}
return obs;
}
/// <summary>
/// Derives the weight-related complexity contribution from a weight observation: values expressed
/// in kilograms are first converted to grams via <c>ParseWeight</c>, and the final integer complexity
/// contribution is mapped from the gram value using the standard UCIN brackets
/// (&lt; 750 g → 7, 750999 → 5, 10001249 → 2, 12501999 → 1, ≥ 2000 → 0). Returns 0 when the
/// observation value cannot be parsed as a number.
/// </summary>
/// <param name="obs">The base patient observation whose value represents the patient's weight.</param>
/// <returns>A task that resolves to the integer complexity contribution (07).</returns>
/// <!-- aidoc:v1 sig=a79c349 body=4c9b390 -->
private async Task<int> CalculateWeight(BasePatientObservation obs)
{
var complexityValueOfWeight = 0;
if (obs.Units == "kg") obs = await ParseWeight(obs);
var pobs = (PatientObservation)obs;
if (!double.TryParse(pobs.Value.ToString(), out var valueParsed))
return complexityValueOfWeight;
complexityValueOfWeight = valueParsed switch
{
< 750 => 7,
>= 750 and <= 999 => 5,
>= 1000 and <= 1249 => 2,
>= 1250 and <= 1999 => 1,
>= 2000 => 0,
_ => complexityValueOfWeight
};
return complexityValueOfWeight;
}
/// <summary>
/// Converts a weight observation expressed in kilograms to grams in-place (updates <c>Units</c> to
/// <c>"gr"</c> and multiplies <c>Value</c> by 1000). Returns the observation unchanged when it is
/// not a <see cref="PatientObservation"/> or when its value cannot be parsed; the error is also
/// logged.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with a numeric value.</param>
/// <returns>A task that resolves to the (possibly converted) base patient observation.</returns>
/// <!-- aidoc:v1 sig=18ad5f6 body=5d01118 -->
public Task<BasePatientObservation> ParseWeight(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs || !double.TryParse(pobs.Value.ToString(), out var dValue))
{
_logger.LogError("Error casting weight Observation {obs}:", obs);
return Task.FromResult(obs);
}
pobs.Units = "gr";
pobs.Value = dValue * 1000;
return Task.FromResult(obs);
}
/// <summary>
/// Recalculates the <c>Monitor</c> observation by adding 1 point for each active
/// transcutaneous / regional-brain-saturation / EEG monitoring signal (codes from the configured
/// catalogues) found in the most recent observations and active treatments. Persists the resulting
/// observation with bounds [0, 3]. Returns early with a warning when no recent monitor observations
/// are found.
/// </summary>
/// <param name="monitorObservation">
/// The trigger for the calculation. Accepts either a <see cref="PatientObservation"/>
/// (preferred when available) or a <see cref="PatientTreatment"/>. The patient identifier is
/// derived from whichever type is supplied.
/// </param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
/// <!-- aidoc:v1 sig=9488dd5 body=f704b41 -->
public async Task CalculateMonitor(object monitorObservation)
{
var monitorValue = 0;
BasePatientObservation? pobs = null;
ObjectId patientId = new();
PatientTreatment? tobs = null;
if (monitorObservation.GetType() == typeof(PatientObservation))
{
pobs = (BasePatientObservation)monitorObservation;
patientId = pobs.PatientId;
}
else if (monitorObservation.GetType() == typeof(PatientTreatment))
{
tobs = (PatientTreatment)monitorObservation;
patientId = tobs.PatientId;
}
//Observations in last 30 min are active
var activeMonitorObservationsList = await _observationService.Value.FindLastObservations(patientId, 1,
["Saturation_RegBrain", "Saturation_RegSomatic", "Transcutaneous_O2"]);
if (!activeMonitorObservationsList.Any())
{
_logger.LogWarning("Active Monitor Observation List is null or empty for patientId: {patientId}",
patientId);
return;
}
var activeMonitorObservations = activeMonitorObservationsList.FindAll(o => !o.Expired);
if (pobs != null && !((PatientObservation)pobs).Expired)
activeMonitorObservations.Add((PatientObservation)pobs);
if (activeMonitorObservations.Any(o => o.Code != null && _transcutanous.Contains(o.Code))) monitorValue += 1;
if (activeMonitorObservations.Any(o => o.Code != null && _regionalBrainSaturation.Contains(o.Code)))
monitorValue += 1;
//Patient treatment
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(patientId);
activeTreatments = activeTreatments.ToList().FindAll(p =>
p != null && p.RequestedGiveCodes.Any(r =>
_electroencephalogram.Contains(r.Identifier) && r.Text.Contains("Monitor EEGa")));
if (tobs != null && tobs.OrderControl != OrderControlType.Dc && activeTreatments.Any()) monitorValue += 1;
else if (tobs is { OrderControl: OrderControlType.Nw }) monitorValue += 1;
//Generate calculad monitor obs
var monitorObs = new PatientObservation
{
PatientId = patientId,
Name = "Monitor",
CodingSystem = "ADAS",
Min = 0,
Max = 3,
Time = DateTime.Now,
Value = monitorValue
};
await _observationService.Value.InsertObservation(monitorObs, mapObs: false);
}
/// <summary>
/// Recalculates the <c>Surgery</c> complexity score by reconciling the supplied treatment's
/// <c>OrderControl</c> (<c>NW</c> adds, <c>DC</c> removes by placer-order identifier) against the
/// patient's other active surgery treatments, then persisting a <c>Surgery</c> observation
/// with a score of 5 when any active surgery treatment remains, or 0 otherwise.
/// </summary>
/// <param name="treatment">The treatment that triggered the recalculation.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
/// <!-- aidoc:v1 sig=a3bb931 body=d219f09 -->
private async Task CalculateSurgery(PatientTreatment treatment)
{
var surgeryScore = 0;
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId);
//Filter only surgery treatments
var activeSurgeryTreatments = activeTreatments.ToList().FindAll(t =>
t != null && t.RequestedGiveCodes.Any(code =>
_surgery.Contains(code.Identifier) && _surgeryText.Contains(code.Text)));
switch (treatment.OrderControl)
{
//Refactor solo con los tratamientos activos de cirugía, un DC cancela a su entityIdentifier correspondiente.
case OrderControlType.Nw:
activeSurgeryTreatments.Add(treatment);
break;
case OrderControlType.Dc:
activeSurgeryTreatments.RemoveAll(t =>
t != null && t.PlacerOrder?.EntityIdentifier == treatment.PlacerOrder?.EntityIdentifier);
break;
}
if (activeSurgeryTreatments.Count > 0) surgeryScore = 5;
var surgeryObs = new PatientObservation
{
PatientId = treatment.PatientId,
Name = "Surgery",
Min = 0,
Max = 5,
CodingSystem = "ADAS",
Time = DateTime.Now,
Value = surgeryScore
};
await _observationService.Value.InsertObservation(surgeryObs, mapObs: false);
}
/// <summary>
/// Computes the patient's active bolus treatments by:
/// (1) optionally including the newly arrived treatment, (2) grouping all bolus-eligible treatments
/// by placer-order <c>NamespaceId</c>, (3) discarding treatments whose administration time was
/// later overridden by an end-time, (4) keeping only the latest message-time entry per distinct
/// administration time, and (5) filtering to the configured medication-bolus catalogue, an
/// administration time within the last 12 hours, and a non-empty RXA status contained in
/// <c>_rxaStatus</c>.
/// </summary>
/// <param name="patientId">The unique identifier of the patient.</param>
/// <param name="newTreatment">The optional new treatment to be included in the active-bolus set.</param>
/// <returns>A task that resolves to the list of treatments currently counted as active boluses.</returns>
/// <!-- aidoc:v1 sig=401adb8 body=c80c23f -->
private async Task<List<PatientTreatment>> GetActiveBolus(ObjectId patientId, PatientTreatment? newTreatment)
{
var treatmentsProcessed = new List<PatientTreatment>();
var activeTreatments = await _treatmentService.Value.GetBolusTreatments(patientId);
if (newTreatment != null) activeTreatments.Add(newTreatment);
//Agrupar por namespaceID y quedarme con el último message time que será el último que han reecho.
//Si ese último es concluido suma 1.
var treatmentsGroupedByNamespaceId = activeTreatments
.GroupBy(t => t.PlacerOrder?.NamespaceId)
//.Select(group => group.OrderByDescending(t => t.messageTime)).ToList();
.Select(grp => grp.ToList())
.ToList();
//.Select(g => g.FirstOrDefault()).ToList();
foreach (var treatmentGroup in treatmentsGroupedByNamespaceId)
{
var modificatedBolus = treatmentGroup.FindAll(t =>
t.RequestedGiveCodesStatus.Any(r => r.EndAdministrationTime != DateTime.MinValue));
//Eliminamos todos aquellos que han sido modificados con posterioridad, esto nos lo indica cuando tiene endAdministrationTime != null
treatmentGroup.RemoveAll(t =>
modificatedBolus.Any(m => m.RequestedGiveCodesStatus.FirstOrDefault()?.EndAdministrationTime
== t.RequestedGiveCodesStatus.FirstOrDefault()?.AdministrationTime));
/* treatmentGroup.RemoveAll(t => t.requestedGiveCodesStatus
.Any(r => r.endAdministrationTime == null && modificatedBolus.FirstOrDefault(m =>
m.requestedGiveCodesStatus.Any(code => code.endAdministrationTime == r.administrationTime))
!= null));
*/
//modificatedBolus.Any(b =>
// b.requestedGiveCodesStatus.Any(code => code.endAdministrationTime == r.administrationTime))));
//Opción B a veces se cancelan sin endTime. Simplemente, agrupar por administrationTime y quedarme con el último por time del mensaje.
treatmentGroup.RemoveAll(t => !t.RequestedGiveCodesStatus.Any());
var groupByAdmTime = treatmentGroup.GroupBy(t =>
t.RequestedGiveCodesStatus.FirstOrDefault()!.AdministrationTime)
.Select(grp => grp.OrderByDescending(t => t.MessageTime).First());
treatmentsProcessed.AddRange(groupByAdmTime);
//todos los que tengan startOfAdministration y no tenga esa fecha ninguno de modificatedBolus en endOfTreatment
}
return treatmentsProcessed
.FindAll(p =>
p.RequestedGiveCodes.Any(r => _medicationBolus.Contains(r.Identifier)) &&
p.RequestedGiveCodesStatus.FirstOrDefault() != null &&
p.RequestedGiveCodesStatus.FirstOrDefault()!.AdministrationTime?.ToUniversalTime() >
DateTime.UtcNow.AddHours(-12) &&
p.RequestedGiveCodesStatus.FirstOrDefault()!.AdministrationTime?.ToUniversalTime() <= DateTime.UtcNow &&
p.RequestedGiveCodesStatus.Any(rxa => rxa.Status != null && _rxaStatus.Contains(rxa.Status)));
}
/// <summary>
/// Persists the <c>OpiateBoluses</c> observation whose value is the count of currently active bolus
/// treatments for the patient (as computed by <c>GetActiveBolus</c> including the new treatment).
/// </summary>
/// <param name="treatment">The treatment that triggered the recalculation and should be included in the active-bolus set.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
/// <!-- aidoc:v1 sig=493996c body=3ef85e3 -->
private async Task CalculateBolus(PatientTreatment treatment)
{
var activeBolus = await GetActiveBolus(treatment.PatientId, treatment);
var bolusObs = new PatientObservation
{
PatientId = treatment.PatientId,
Time = DateTime.Now,
Name = "OpiateBoluses",
CodingSystem = "ADAS",
Value = activeBolus.Count
};
await _observationService.Value.InsertObservation(bolusObs);
}
/// <summary>
/// Applies the red-alert rule for mean arterial pressure (TAm) in neonates: when TAm is below
/// the patient's gestational age (in weeks) for the first week, or below the fixed gestational
/// age thereafter, the <c>TAm</c> observation's <c>Status</c> is set to <c>Alert</c>; otherwise
/// it is set to <c>Ok</c>. If the supplied observation is a gestational-age value (not TAm),
/// a fresh TAm observation is re-inserted with the recalculated status. The threshold is read
/// from the latest <c>Age_Gestational</c> observation when its value is below 2 weeks,
/// otherwise from the latest <c>Age_Gestational_Fixed</c>.
/// </summary>
/// <param name="obs">The base patient observation that triggered the alert evaluation (<c>TAm</c>, <c>Age_Gestational</c>, or <c>Age_Gestational_Fixed</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
/// <!-- aidoc:v1 sig=857f5e0 body=0bf2cd4 -->
public async Task CalculateTAmAlert(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var tamStatus = StatusEnum.Type.Ok;
var groupedObservations = new List<PatientObservation> { pobs };
var gestationalWeeksToCheck = 0;
if (obs.Name == "Age_Gestational")
groupedObservations = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["Age_Gestational_Fixed", "TAm"]);
if (obs.Name == "Age_Gestational_Fixed")
groupedObservations = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["Age_Gestational", "TAm"]);
if (obs.Name == "TAm")
groupedObservations = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["Age_Gestational", "Age_Gestational_Fixed"]);
var ageGestational = groupedObservations.FirstOrDefault(o => o.Name == "Age_Gestational");
if (ageGestational != null && int.TryParse(ageGestational.Value.ToString(), out var ageParsed) && ageParsed < 2)
{
gestationalWeeksToCheck = Convert.ToInt32(ageGestational.Value);
}
else
{
var ageGestationalFixed = groupedObservations.FirstOrDefault(o => o.Name == "Age_Gestational_Fixed");
if (ageGestationalFixed != null) gestationalWeeksToCheck = Convert.ToInt32(ageGestationalFixed.Value);
//var gestationalWeeks = int.Parse(ageGestationalFixed.value.ToString());
//if (Convert.ToInt32(pobs.value) < gestationalWeeks) TAmStatus = ObservationStatus.Alert;
}
var tamObs = groupedObservations.FirstOrDefault(o => o.Name == "TAm");
if (tamObs != null)
if (Convert.ToInt32(tamObs.Value) < gestationalWeeksToCheck)
tamStatus = StatusEnum.Type.Alert;
if (pobs.Name == "TAm")
{
pobs.Status = tamStatus;
}
else
{
if (tamObs != null)
{
tamObs.Id = ObjectId.GenerateNewId();
tamObs.Time = DateTime.Now;
tamObs.Status = tamStatus;
await _observationService.Value.InsertObservation(tamObs, mapObs: false);
}
}
}
/// <summary>
/// Computes the temperature gradient between the incubator and the patient (incubator patient)
/// when both temperatures have been observed within 10 minutes of each other. Persists the
/// <c>Temp_Gradient</c> observation with the resulting value, after shifting its timestamp via
/// <c>CheckObsWithSameTimeExistsAndIncrementTime</c> to avoid colliding with a previous
/// gradient observation. Returns early when the companion temperature is missing or the two
/// readings fall outside the 10-minute window.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (<c>Temp_Patient</c> or <c>Temp_Incubator</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
/// <!-- aidoc:v1 sig=f3ac5ae body=4120ebf -->
public async Task CalculateTempGradient(BasePatientObservation obs)
{
List<PatientObservation> tempRetrievedesFromBd;
PatientObservation? tempRetrievedFromBd = null;
var pobs = (PatientObservation)obs;
if ("Temp_Patient".Equals(pobs.Name))
{
tempRetrievedesFromBd =
await _observationService.Value.FindLastObservations(pobs.PatientId, 1, ["Temp_Incubator"]);
tempRetrievedFromBd = tempRetrievedesFromBd.FirstOrDefault();
}
if ("Temp_Incubator".Equals(pobs.Name))
{
tempRetrievedesFromBd =
await _observationService.Value.FindLastObservations(pobs.PatientId, 1, ["Temp_Patient"]);
tempRetrievedFromBd = tempRetrievedesFromBd.FirstOrDefault();
}
//Now Temp Gradient is only calculated when temp_Patient and Temp_Incubator come in last 10 min.
//if (tempRetrievedFromBd != null && DateTime.UtcNow.CompareTo(tempRetrievedFromBd.time.ToUniversalTime().AddMinutes(10)) <= 0)
if (tempRetrievedFromBd == null) return;
var difFechas = pobs.Time.ToUniversalTime() - tempRetrievedFromBd.Time.ToUniversalTime();
//if (tempRetrievedFromBd != null && pobs.time.ToUniversalTime().CompareTo(tempRetrievedFromBd.time.ToUniversalTime().AddMinutes(10)) <= 0)
if (Math.Abs(difFechas.TotalMinutes) <= 10 &&
double.TryParse(tempRetrievedFromBd.Value.ToString(), out var tempRetrievedFromBdParsed) &&
double.TryParse(pobs.Value.ToString(), out var pobsParsed))
{
double tempGradientValue;
if ("Temp_Patient".Equals(pobs.Name))
tempGradientValue = tempRetrievedFromBdParsed - pobsParsed;
else
tempGradientValue = pobsParsed - tempRetrievedFromBdParsed;
var tempGradientObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Temp_Gradient",
CodingSystem = "ADAS",
Time = obs.Time,
Value = tempGradientValue
};
var obsToInsert = (PatientObservation)await CheckObsWithSameTimeExistsAndIncrementTime(tempGradientObs);
obsToInsert.Id = ObjectId.GenerateNewId();
await _observationService.Value.InsertObservation(obsToInsert, mapObs: false);
}
}
/// <summary>
/// Enumeration of the intravenous-line types recognised by the UCIN complexity model,
/// ordered from most invasive (Artery) to least invasive (Peripheral).
/// </summary>
private enum IntraVenousLineTypes
{
Artery,
CentralVein,
Picc,
MiddleLine,
Peripheral
}
/// <summary>
/// Enumeration of the respiratory-assistance types recognised by the UCIN complexity model,
/// ordered from highest score (Ino) to lowest (None).
/// </summary>
// ReSharper disable once UnusedMember.Local
private enum RespiratoryTypes
{
Ino,
Vafo,
Vmc,
Vmni,
NasalCannulas,
None
}
}