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

884 lines
42 KiB
C#

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 adas_core.Domain.Utils;
using adas_core.Domain.Utils.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
namespace adas_core.Application.Customizations.HUVH.UCIN;
/// <summary>
/// Represents a concrete implementation of the <see cref="ICalculatedObservations"/> interface,
/// providing a collection of calculated observation data.
/// </summary>
/// <!-- aidoc:v1 sig=1b16cfc -->
public class CalculatedObservations : ICalculatedObservations
{
private const string Spo2PreObservationName = "SpO2";
private const string Spo2PostObservationName = "SpO2_Post";
private const string Spo2PrePostObservationName = "SpO2_Pre_Post";
private const string CodingSystem = "ADAS";
private readonly List<string> _complexityObservations =
[
"Respiratory_Device_Multivalue", "Intravenous_Routes_Multivalue", "Medication_Multivalue",
"System_Multivalue", "Newborn_Weight", "Surgery_Multivalue"
];
private readonly ILogger<CalculatedObservations> _logger;
private readonly IMappingUtils _mappingUtils;
private readonly Lazy<IMedicineService> _medicineService;
private readonly Lazy<IObservationService> _observationService;
private readonly Lazy<ITreatmentService> _treatmentService;
/// <summary>
/// Initializes a new instance of the <see cref="CalculatedObservations"/> class by resolving its treatment, medicine, observation, logging, and mapping dependencies from the supplied <see cref="IServiceProvider"/>.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> used to obtain the dependencies required by this instance.</param>
/// <exception cref="InvalidOperationException">Thrown when a required service cannot be resolved from <paramref name="serviceProvider"/>.</exception>
/// <!-- aidoc:v1 sig=409f903 body=5dd8024 -->
public CalculatedObservations(IServiceProvider serviceProvider)
{
_treatmentService = serviceProvider.GetRequiredService<Lazy<ITreatmentService>>();
_medicineService = serviceProvider.GetRequiredService<Lazy<IMedicineService>>();
_observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>();
_logger = serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();
var apiSettings = serviceProvider.GetRequiredService<IOptions<ApiSettings>>(); //apiSettings;
_mappingUtils = new MappingUtils(apiSettings);
}
/// <summary>
/// Calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient for whom the active bolus is calculated.</param>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
/// <!-- aidoc:v1 sig=7d38721 body=bfa6f2f -->
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a patient observation by applying the appropriate calculation logic based on its name, handling multi-value interventions, surgeries, complexity scoring, pre/post saturation differences, pain scales, and last defecation value.
/// </summary>
/// <param name="obs">The patient observation to be mapped, which may be transformed depending on its name.</param>
/// <param name="onlyByName">Indicates whether the mapping should be performed using only the observation name.</param>
/// <returns>The mapped patient observation, potentially enriched with computed values, wrapped in a task.</returns>
/// <!-- aidoc-review:v1 severity=medium kind=wrong_param_role
/// "The 'onlyByName' parameter is documented as controlling whether 'the mapping should be performed using only the observation name,' but the parameter is never referenced in the method body, so passing a value for it has no effect on the mapping behavior." -->
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
if (obs.Name is "Intervention_In" or "Intervention_Out") await CalculateInterventionMultiValueObservation(obs);
if (obs.Name is "Surgery") await CalculateMultiValueObservation(obs);
if (obs.Name != null && _complexityObservations.Contains(obs.Name))
{
_logger.LogDebug("Mapping observation Complexity {obs}", obs);
//TODO ver si hay un máximo de puntuación por grupo de observaciones
_ = CalculateComplexity(obs);
}
if (obs.Name is Spo2PreObservationName or Spo2PostObservationName)
{
_logger.LogDebug("Pre-post saturation observation {obs}", obs);
_ = CalculateSaturation_DiffObservation(obs);
}
if (obs.Name is "ALPS" or "NPASS_Sedation" or "NPASS_Analgesia") _ = CalculatePainScale(obs);
if (obs.Name is "Last_Defecation") obs = (T)CalculateLastDefecationValue(obs);
return obs;
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> by determining its <see cref="OrderControlType"/> based on prior treatments and expiration status.
/// If the placer order number is missing, the treatment is returned unchanged. The order control is set to <see cref="OrderControlType.Xo"/> when previous treatments exist, <see cref="OrderControlType.Dc"/> when the treatment has expired (end time set), or <see cref="OrderControlType.Nw"/> otherwise.
/// </summary>
/// <param name="treatment">The patient treatment to map, including its placer order, end time, and medicines.</param>
/// <returns>The mapped <see cref="PatientTreatment"/> with its order control type and medicines updated.</returns>
/// <!-- aidoc:v1 sig=442a6e8 body=e1bed22 -->
public async Task<PatientTreatment> Map(PatientTreatment treatment)
{
var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden
if (string.IsNullOrEmpty(order)) return treatment;
//comprobamos el estado de los tratamientos anteriores
var oldTreatments = await CheckExpiredPatientTreatments(treatment);
treatment.OrderControl = oldTreatments.Any() ? OrderControlType.Xo : OrderControlType.Nw;
if (treatment.EndTime != null)
//el tratamiento ha expirado
treatment.OrderControl = OrderControlType.Dc;
await CheckTreatmentMedicines(treatment);
return treatment;
}
/// <summary>
/// Maps the provided <see cref="PumpObservation"/> instance by returning it unchanged, wrapped in a completed task.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A completed <see cref="Task{TResult}"/> containing the provided <see cref="PumpObservation"/>.</returns>
/// <!-- aidoc:v1 sig=50be4c1 body=e481734 -->
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Persists a multivalue medication observation for the specified patient based on the currently active medicines, expiring any previous medication observation recorded for the same patient.
/// </summary>
/// <param name="activeMedicines">The list of active medicines from which distinct medication names are extracted to build the observation value.</param>
/// <param name="patientId">The identifier of the patient for whom the medication observation is calculated and stored.</param>
/// <!-- aidoc:v1 sig=299c0fe body=8145436 -->
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
var medicineObs = new PatientObservation
{
PatientId = patientId,
Name = "Medication_Multivalue",
CodingSystem = CodingSystem,
Value = activeMedicines.Select(m => m.Name ?? string.Empty).Distinct().ToArray(),
Time = DateTime.Now
};
var lastMedicationsObs =
await _observationService.Value.FindLastObservations(patientId, 1, ["Medication_Multivalue"]);
var lastMedicationObs = lastMedicationsObs.FirstOrDefault();
if (lastMedicationObs != null)
{
//expiramos la anterior
lastMedicationObs.Expired = true;
_ = _observationService.Value.UpdateObservation(lastMedicationObs);
}
await _observationService.Value
.InsertObservation(
medicineObs); //mapObs:volvemos a mapear la observación para que pase por el cálculo de la complejidad
}
/// <summary>
/// Asynchronously retrieves the collection of active treatments associated with the specified patient identifier.
/// </summary>
/// <param name="id">The unique <see cref="ObjectId"/> of the patient whose active treatments should be fetched.</param>
/// <returns>A task that represents the asynchronous operation, containing an <see cref="IEnumerable{T}"/> of nullable <see cref="PatientTreatment"/> instances 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>
/// Asynchronously maps a <see cref="PatientDiagnosis"/> to a target <see cref="PatientDiagnosis"/> representation.
/// </summary>
/// <param name="diagnosis">The source <see cref="PatientDiagnosis"/> instance to be mapped.</param>
/// <returns>A <see cref="Task{PatientDiagnosis}"/> that represents the asynchronous mapping operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="T:System.NotImplementedException">Thrown when the method is invoked, as the mapping logic has not yet been implemented.</exception>
/// <!-- aidoc:v1 sig=8ddf601 body=bfa6f2f -->
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Adjusts the time of a new patient observation to ensure it is at least one second after the most recent observation with the same name, preventing time conflicts. If the observation name is null or empty, the method logs an error and returns the observation unchanged.
/// </summary>
/// <param name="newObservation">The new patient observation whose time may be adjusted to follow the most recent observation for the same patient and measurement.</param>
/// <returns>The patient observation with a corrected time if a time conflict was detected, or the original observation if no adjustment was needed or the name was invalid.</returns>
/// <!-- aidoc:v1 sig=2b42b30 body=04118be -->
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
{
_logger.LogError(
"Error FixTimeInconsistencyWithLast. Observation name is null or empty. Observation: {newObservation}",
newObservation);
return newObservation;
}
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);
return newObservation;
}
/// <summary>
/// Performs a pre-mapping operation on a list of patient observations, returning the list for further processing.
/// </summary>
/// <param name="listToInsert">The list of patient observations to pre-map.</param>
/// <returns>A task that represents the asynchronous operation, containing the provided list of patient observations.</returns>
/// <!-- aidoc:v1 sig=5a37e9b body=0791af0 -->
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm onto a patient observation, returning the observation unchanged without applying the alarm data.
/// </summary>
/// <param name="obs">The patient observation that the alarm is to be associated with.</param>
/// <param name="alarmToInsert">The alarm intended to be inserted into the observation.</param>
/// <returns>A task containing the patient observation, returned as-is.</returns>
/// <!-- aidoc:v1 sig=405db8e body=ea790e4 -->
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm notification for the specified patient observation, identified by name and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code categorizing the type of alarm to be sent.</param>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented.</exception>
/// <!-- aidoc:v1 sig=90426a4 body=bfa6f2f -->
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Calculates and stores a derived "Pain_Scale" observation in the ADAS coding system based on the provided source observation.
/// Handles the ALPS scale by using its value directly, and the NPASS scale by combining the complementary "NPASS_Sedation" and "NPASS_Analgesia" observations into a single combined value (defaulting to "0" when the complementary observation is not found).
/// Returns without creating a record when the observation is not a <see cref="PatientObservation"/> or when its name does not match any of the supported scales.
/// </summary>
/// <param name="obs">The source patient observation used to derive the pain scale; only instances of <see cref="PatientObservation"/> with a supported name (ALPS, NPASS_Sedation, NPASS_Analgesia) are processed.</param>
/// <!-- aidoc:v1 sig=f89cebc body=f5dfb0c -->
private async Task CalculatePainScale(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs) return;
object valueObs;
switch (pobs.Name)
{
case "ALPS":
valueObs = pobs.Value;
break;
case "NPASS_Sedation":
{
//Buscamos la complementaria "NPASS Analgesia"
var analgesiaObs =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Analgesia"]);
var analgesiaValue = analgesiaObs.FirstOrDefault()?.Value.ToString() ?? "0";
valueObs = $"{pobs.Value}/{analgesiaValue}";
}
break;
case "NPASS_Analgesia":
{
//Buscamos la complementaria "NPASS Sedation"
var sedationObs =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Sedation"]);
var sedationValue = sedationObs.FirstOrDefault()?.Value.ToString() ?? "0";
valueObs = $"{sedationValue}/{pobs.Value}";
}
break;
default:
return;
}
var painScaleObs = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = "ADAS",
Name = "Pain_Scale",
Value = valueObs
};
_ = _observationService.Value.InsertObservation(painScaleObs, mapObs: false);
}
/// <summary>
/// Calculates the last defecation value for a patient observation by assigning the observation's time, converted to local time and formatted as a date/time string, to its value. If the observation is not a <see cref="PatientObservation"/>, it is returned unchanged.
/// </summary>
/// <param name="obs">The base patient observation to process.</param>
/// <returns>The original <see cref="BasePatientObservation"/> if it is not a <see cref="PatientObservation"/>; otherwise, the <see cref="PatientObservation"/> with its value set to the locally formatted observation time.</returns>
/// <!-- aidoc:v1 sig=479318b body=e536a62 -->
private static BasePatientObservation CalculateLastDefecationValue(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs) return obs;
//Assign obs time to value
//Convert the UTC DateTime to local time.
var localTime = pobs.Time.ToLocalTime();
//Format the local time into a short date/time string.
pobs.Value = localTime.ToString("dd/MM/yyyy HH:mm");
return pobs;
}
/// <summary>
/// Calculates and persists a multi-value observation by appending the current value to the existing list of values for the corresponding "Multivalue" observation. Expirates the previous multi-value observation before inserting the new one.
/// </summary>
/// <param name="obs">The base patient observation used to derive the multi-value observation; it must be a <see cref="PatientObservation"/> with a non-empty name and a string value.</param>
/// <!-- aidoc:v1 sig=d821a40 body=264c069 -->
private async Task CalculateMultiValueObservation(BasePatientObservation obs)
{
//El valor de la observación es un array de strings
if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name)) return;
var newObsName = $"{obs.Name}_Multivalue";
var actualObsInBd =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, [newObsName]);
var newValue = new List<string>();
if (pobs.Value is not string stringValueName) return;
if (actualObsInBd.Count > 0)
if (actualObsInBd.First().Value is string[] oldValue)
{
oldValue = oldValue.Where(x => x != stringValueName).ToArray();
newValue.AddRange(oldValue);
}
newValue.Add(stringValueName);
if (newValue.Count == 0) return;
//Expiramos la anterior
var actualObs = actualObsInBd.FirstOrDefault();
if (actualObs != null)
{
actualObs.Expired = true;
await _observationService.Value.UpdateObservation(actualObs);
}
var newObservation = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = newObsName,
Value = newValue.ToArray()
};
_logger.LogDebug(
"Calculate {name}_Multivalue {outpatient} Insert Calculated {time} value {value}",
newObsName, newObservation.PatientId, newObservation.Time,
newObservation.Value.ToJson());
await _observationService.Value.InsertObservation(newObservation);
}
/// <summary>
/// Calculates and persists a multi-value observation that aggregates the active intervention groups (e.g., devices, routes) for a patient based on incoming "<c>_In</c>" and "<c>_Out</c>" observations. Handles the insertion of new groups (with special duplicate prevention for respiratory devices) and the removal of groups when a corresponding "Out" event is received, expiring the previous multi-value observation and inserting a new one when the resulting set is not empty.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation; expected to be a <see cref="PatientObservation"/> with a non-empty name and a string value representing the intervention group.</param>
/// <!-- aidoc:v1 sig=25ac55d body=191bd88 -->
private async Task CalculateInterventionMultiValueObservation(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name) ||
pobs.Value is not string valueObs) return;
if (!GetInterventionValue(valueObs, out var r) || r == null) return;
var obsType = r.Value.type; //"Intravenous_Routes";
var obsGroup = r.Value.Name; // "Vía arterial";
var newObsName = $"{obsType}_Multivalue";
// Obtener la última observación multivalue
var result = await _observationService.Value.FindLastObservations(obs.PatientId, 1, [newObsName]);
var actualMultiValueObsInDb = result.FirstOrDefault(o => !o.Expired);
var oldValue = actualMultiValueObsInDb?.Value as string[] ?? [];
var newValue = new List<string>(oldValue);
if (obs.Name.Contains("_In"))
{
//Por seguridad solo tomamos el último valor que nos llega
//Puede ser que no se acuerden de cancelar el anterior antes de añadir un nuevo dispositivo y no puede haber dos dispositivos del mismo tipo activos al mismo tiempo
if (obsType == "Respiratory_Device")
newValue.Clear();
// Inserción dispositivo
if (!oldValue.Contains(obsGroup))
newValue.Add(obsGroup);
}
else if (obs.Name.Contains("_Out"))
{
// Retirada del dispositivo
//Buscamos la inserción
var name = $"{obs.Name.Split('_')[0]}_In";
var inObs = await _observationService.Value.FindLastNotExpiredObservatonsByPatient(obs.PatientId, name);
var sameValueObs = inObs.Where(o => o.Value == pobs.Value).ToList();
if (sameValueObs.Any())
// Si hay más de una observación del mismo tipo, no eliminar del multivalue
if (sameValueObs.Count > 1)
return;
// Actualizar valor eliminando el grupo
newValue.Remove(obsGroup);
}
else
{
return;
}
//Expiramos la anterior
if (actualMultiValueObsInDb is { Expired: false })
{
actualMultiValueObsInDb.Expired = true;
await _observationService.Value.UpdateObservation(actualMultiValueObsInDb);
}
// Si no hay valores nuevos, no se crea una nueva observación
if (!newValue.Any())
return;
// Crear y registrar nueva observación
var newObservation = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = newObsName,
Value = newValue.ToArray()
};
_logger.LogDebug(
"Calculate {name}_Multivalue {patientid} Insert Calculated {time} value {value}",
newObsName, newObservation.PatientId, newObservation.Time,
newObservation.Value.ToJson()
);
await _observationService.Value.InsertObservation(newObservation);
}
/// <summary>
/// Extracts a numeric code from the observation value, normalizes it, and resolves the corresponding intervention
/// by looking it up in the mapping repository. Returns false when the code cannot be parsed as a number or when no
/// matching intervention is found.
/// </summary>
/// <param name="valueObs">Observation text from which the leading token is taken as the candidate intervention code.</param>
/// <param name="r">When the method returns true, contains the resolved intervention's type, name, and group; otherwise null.</param>
/// <returns><c>true</c> if a matching intervention was found; <c>false</c> if the code is not numeric or the lookup yields no result.</returns>
/// <!-- aidoc:v1 sig=d3884cf body=1ea9ac7 -->
private bool GetInterventionValue(string valueObs, out (string type, string Name, string Group)? r)
{
var code = valueObs.Split(" ")[0]; //
if (code.EndsWith(".")) // Verifica si termina con un punto
code = code.Substring(0, code.Length - 1).Replace('.', ','); // Remueve el último carácter
if (!double.TryParse(code, out var codeParsed))
{
r = null;
return false;
}
r = _mappingUtils.SearchByCode(codeParsed, "Intervention"); //TODO validar este parámetro con Lola
return r != null;
}
/**
* Calculamos la complejidad basándonos en el valor de Respiratorio/Vías/Medicación/Sistemas/Peso nacimiento/cirugía
* cuando entra una nueva observación que afecte a complejidad, se recalcula de 0 para no tener que estar pendiente de qué puntos corresponden a qué observación
* Valor máximo 46 Guardamos la observación que nos llega y recalculamos complejidad.
*/
/// <summary>
/// Calcula la complejidad del paciente en función de las observaciones registradas.
/// </summary>
/// <param name="obs">Observación del paciente que se va a evaluar para recalcular su complejidad.</param>
/// <!-- aidoc:v1 sig=4545d3a body=a6e9e71 -->
private async Task CalculateComplexity(BasePatientObservation obs)
{
var pobs = obs as PatientObservation;
if (pobs == null) return;
var complexity = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = "Complexity",
Value = 0
};
var complexityValue = 0;
// Diccionario para manejar las operaciones basadas en el nombre de la observación
var operations = new Dictionary<string, Func<ObjectId, PatientObservation?, Task<int>>>
{
{
"Respiratory_Device_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "Respiratory_Device_Multivalue",
observation)
},
{
"Intravenous_Routes_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "Intravenous_Routes_Multivalue",
observation)
},
{
"Medication_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "Medication_Multivalue", observation)
},
{
"System_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "System_Multivalue", observation)
},
{ "Newborn_Weight", CalculateWeightNewBornValue },
{
"Surgery_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "Surgery_Multivalue", observation)
}
};
// Realizar cálculos iterando sobre las operaciones
foreach (var operation in operations)
complexityValue += await operation.Value(pobs.PatientId, operation.Key.Equals(pobs.Name) ? pobs : null);
complexity.Value = Math.Min(complexityValue, 46); // Limitar el valor máximo a 46
_logger.LogDebug(
"complexity for patient {patientid} => inserting complexity value: {complexityValue} for patient: {complexityPatientid} at time {fixedTimeComplexityTime}",
complexity.PatientId, complexity.Value, complexity.PatientId, complexity.Time);
await _observationService.Value.InsertObservation(complexity, mapObs: false);
}
/// <summary>
/// Calculates the total complexity value for a multivalue observation belonging to a patient. Uses the supplied observation when provided; otherwise retrieves the most recent one from the database and sums the complexity values resolved via the mapping utility, returning 0 when no data is available.
/// </summary>
/// <param name="patientId">The identifier of the patient whose observation is being evaluated.</param>
/// <param name="observationName">The name of the multivalue observation to look up and calculate complexity for.</param>
/// <param name="observation">An optional pre-loaded observation that overrides the database lookup when supplied.</param>
/// <returns>The aggregated complexity value for the observation, or 0 if no observation or values are found.</returns>
/// <!-- aidoc:v1 sig=cdba6ba body=3c7e7fd -->
private async Task<int> CalculateMultivalueObservationComplexityValue(ObjectId patientId, string observationName,
PatientObservation? observation = null)
{
var result =
await _observationService.Value.FindLastObservations(patientId, 1, [observationName]);
var actualObsInBd = result.FirstOrDefault();
var valueObj = observation == null ? actualObsInBd?.Value : observation.Value;
var valueList = valueObj as string[];
var valueToReturn = valueList?.Sum(v => _mappingUtils.GetComplexityValue(v)) ?? 0;
_logger.LogDebug(
"complexity for patient {patientid} => observation: {observationName} plus complexity: {Value}", patientId,
observationName, valueToReturn);
return valueToReturn;
}
/// <summary>
/// Calculates the complexity value associated with a newborn's weight observation for the specified patient.
/// When <paramref name="pobs"/> is null, the most recent stored "Newborn_Weight" observation is used; otherwise the provided observation is used, converting from kilograms when applicable. Returns 0 when no value can be retrieved or parsed, or when the observation name is missing, and otherwise returns the complexity value resolved via the mapping utilities.
/// </summary>
/// <param name="patientId">The identifier of the patient whose newborn weight complexity is being calculated.</param>
/// <param name="pobs">An optional patient observation to use instead of the latest stored one; if provided with "kg" units, its value is converted.</param>
/// <returns>The calculated newborn weight complexity value, or 0 when no valid value is available.</returns>
/// <!-- aidoc:v1 sig=85b6f46 body=c89d98c -->
private async Task<int> CalculateWeightNewBornValue(ObjectId patientId, PatientObservation? pobs = null)
{
var result =
await _observationService.Value.FindLastObservations(patientId, 1, ["Newborn_Weight"]);
var actualObsInBd = result.FirstOrDefault();
string? strValue;
string? obsName;
if (pobs == null)
{
if (actualObsInBd?.Value == null) return 0;
strValue = actualObsInBd.Value.ToString();
obsName = actualObsInBd.Name;
}
else
{
if (pobs.Units == "kg") pobs = ParseWeight(pobs);
strValue = pobs.Value.ToString();
obsName = pobs.Name;
}
if (!double.TryParse(strValue, out var valueParsed) || obsName == null)
return 0;
var weightNewBornValue = _mappingUtils.GetComplexityValue(obsName, valueParsed);
_logger.LogDebug(
"complexity for patient {patientid} => Weight_Newborn plus complexity: {Weight_NewbornValue}",
patientId, weightNewBornValue);
return weightNewBornValue;
}
/// <summary>
/// Parses a weight observation by converting its value to grams (multiplying by 1000) and assigning the "gr" unit.
/// If the value cannot be parsed as a double, the error is logged and the original observation is returned unchanged.
/// </summary>
/// <param name="obs">The patient observation containing the weight value to parse and convert.</param>
/// <returns>The patient observation with the value converted to grams, or the original observation if the value could not be parsed.</returns>
/// <!-- aidoc:v1 sig=2d84953 body=d947a19 -->
private PatientObservation ParseWeight(PatientObservation obs)
{
if (!double.TryParse(obs.Value.ToString(), out var dValue))
{
_logger.LogError("Error casting weight Observation {obs}:", obs);
return obs;
}
obs.Units = "gr";
obs.Value = dValue * 1000;
return obs;
}
/// <summary>
/// Calculates the SpO2 difference between pre and post observations for a patient and persists the resulting SpO2_Diff and combined pre/post observations.
/// Handles both pre and post observation entry points, adjusts the timestamp to the earlier of the two observations, and defaults to 0 when a value cannot be parsed as a double.
/// </summary>
/// <param name="obs">The incoming patient observation (either a pre or post SpO2 measurement) used to drive the calculation and identify the counterpart observation.</param>
/// <returns>A task that represents the asynchronous insert of the derived observations.</returns>
/// <!-- aidoc:v1 sig=7f9be58 body=e3f6858 -->
private async Task CalculateSaturation_DiffObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
PatientObservation? pre;
PatientObservation? post;
toSearchList.AddRange(new List<string> { Spo2PreObservationName, Spo2PostObservationName });
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
var time = obs.Time;
if (obs.Name == Spo2PreObservationName)
{
pre = (PatientObservation)obs;
post = values.FirstOrDefault(o => o.Name == Spo2PostObservationName);
if (post?.Time != null && time.CompareTo(post.Time) > 0) time = post.Time;
}
else
{
post = (PatientObservation)obs;
pre = values.FirstOrDefault(o => o.Name == Spo2PreObservationName);
if (pre?.Time != null && time.CompareTo(pre.Time) > 0) time = pre.Time;
}
if (pre?.Value != null && post?.Value != null)
{
var preSuccess = double.TryParse(pre.Value.ToString(), out var preValue);
if (!preSuccess) preValue = 0;
var postSuccess = double.TryParse(post.Value.ToString(), out var postValue);
if (!postSuccess) postValue = 0;
var saturationDiff = Math.Round(preValue - postValue, 2);
var saturationDiffObs = new PatientObservation
{
Name = "SpO2_Diff",
CodingSystem = CodingSystem,
PatientId = obs.PatientId,
Time = time,
Value = saturationDiff
};
var saturationPrePost = new PatientObservation
{
Name = Spo2PrePostObservationName,
CodingSystem = CodingSystem,
PatientId = obs.PatientId,
Time = time,
Value = pre.Value + "/" + post.Value
};
await _observationService.Value.InsertObservation(saturationDiffObs);
await _observationService.Value.InsertObservation(saturationPrePost);
}
}
/// <summary>
/// Checks for expired patient treatments and updates the matching treatment's <see cref="OrderControlType"/> to <c>DC</c> when its end time is in the future, returning the remaining active treatments for the same patient.
/// </summary>
/// <param name="treatment">The patient treatment being evaluated, used to locate the existing record by its placer order entity identifier and to persist the updated state.</param>
/// <returns>A task that yields the list of active patient treatments for the patient that are not the same placer order as the supplied treatment.</returns>
/// <!-- aidoc-review:v1 severity=medium kind=wrong_summary
/// "Summary claims to 'check for expired patient treatments' but the code calls GetActiveTreatmentsByPatient (active, not expired) and sets DC when t.EndTime > DateTime.UtcNow (future, not past), making the word 'expired' contradictory to the documented condition 'end time is in the future'." -->
private async Task<List<PatientTreatment>> CheckExpiredPatientTreatments(PatientTreatment treatment)
{
//si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas
var result = new List<PatientTreatment>();
var oldTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId);
foreach (var t in oldTreatments)
{
if (t == null) continue;
if (t.PlacerOrder?.EntityIdentifier == treatment.PlacerOrder?.EntityIdentifier)
{
//Es el mismo tratamiento actualizamos
if (t.EndTime != null && t.EndTime > DateTime.UtcNow) treatment.OrderControl = OrderControlType.Dc;
treatment.Id = t.Id; //Asignamos el ID del tratamiento existente para actualizarlo
_ = _treatmentService.Value.UpdateTreatment(treatment);
}
else
{
result.Add(t);
}
}
return result;
}
/// <summary>
/// Validates the medicines requested in a patient treatment, handling nutrition items by creating
/// dedicated observations, detecting vasoactive drugs to record a flag, and combining remaining
/// medicines with the patient's active treatment medicines to calculate a unified medicine observation.
/// Medicines that are not found by code are skipped, and the method returns early when no new
/// medicines remain after filtering.
/// </summary>
/// <param name="treatment">The patient treatment whose requested give codes are inspected for medicines, nutrition items, and vasoactive drugs.</param>
/// <!-- aidoc:v1 sig=e762e81 body=6f65d8e -->
private async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
var newMedicines = new List<Medicine>();
var hasVasoactives = false;
foreach (var code in treatment.RequestedGiveCodes)
{
var m = await _medicineService.Value.GetByCode(code.Identifier);
if (m == null) continue;
//Si el código es de tipo nutrición entonces creamos una observación de ese tipo que tendrá el valor mapeado con el nombre correspondiente
//CreateNutritionObservation y NO agregamos a medicamentos
var r = _mappingUtils.SearchByCode(m.Codes.First(), "Treatment");
if (r is { type: "Nutrition" })
{
await CreateNutritionObservation(r.Value.group, treatment.PatientId);
continue;
}
if (r is { type: "Medication", group: "Drogas vasoactivas" })
hasVasoactives = true;
newMedicines.Add(m);
}
if (hasVasoactives)
{
var vObs = new PatientObservation
{
Name = "HasVasoactive",
Value = true,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem
};
await _observationService.Value.InsertObservation(vObs, mapObs: false);
}
if (newMedicines.Count == 0)
return;
var _ = await GetActiveTreatmentsByPatient(treatment.PatientId);
var activeTreatments = _.ToList();
var __ = await _medicineService.Value.GetMedicinesOfTreatments(activeTreatments); //Todo revisar
var activeMedicines = __.ToList();
// Combina ambas listas y selecciona solo elementos únicos con el mismo nombre
var uniqueMedicines = newMedicines
.Concat(activeMedicines)
.GroupBy(m => m.Name)
.Select(g => g.First())
.ToList();
await CalculateMedicineObservation(uniqueMedicines, treatment.PatientId);
}
/// <summary>
/// Creates a new feeding/nutrition observation for the specified patient. When the value indicates parenteral feeding, the observation name is normalized to "Parenteral" and the value to "SI". Any previous non-expired observation with the same name is marked as expired before the new one is inserted.
/// </summary>
/// <param name="value">The feeding type value to record; if it contains "parenteral" (case-insensitive), the observation is stored as a parenteral entry.</param>
/// <param name="patientId">The identifier of the patient to whom the observation belongs.</param>
/// <!-- aidoc:v1 sig=7b17fa3 body=af49d06 -->
private async Task CreateNutritionObservation(string value, ObjectId patientId)
{
var nutritionObs = new PatientObservation
{
PatientId = patientId,
Name = "Feeding_Type",
CodingSystem = CodingSystem,
Value = value,
Time = DateTime.Now
};
if (value.ToLower().Contains("parenteral"))
{
nutritionObs.Name = "Parenteral";
nutritionObs.Value = "SI";
}
var lastNutritionObsList =
await _observationService.Value.FindLastObservations(patientId, 1, [nutritionObs.Name]);
var lastNutritionObs = lastNutritionObsList.FirstOrDefault();
if (lastNutritionObs is { Expired: false })
{
//expiramos la anterior
lastNutritionObs.Expired = true;
_ = _observationService.Value.UpdateObservation(lastNutritionObs);
}
await _observationService.Value.InsertObservation(nutritionObs);
}
}