Files

1568 lines
58 KiB
C#

using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Application.Subscriptions;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Serilog;
using Patient = adas_core.Domain.Models.MongoModels.Patient;
namespace adas_core.Application.Services;
public class ObservationService : IObservationService
{
private readonly ICacheService _cacheService;
private readonly CacheSettings? _cacheSettings;
private readonly IAlarmService _alarmService;
private readonly List<string> _allergies;
private readonly ILocalAuditService _auditService;
private readonly Lazy<ICalculatedObservationsService> _calculatedObservationsService;
private readonly IClientMessageService _clientMessageService;
private readonly IConfigObservationService _configObservationService;
private readonly IConfigUnitsService _configUnitsService;
private readonly List<string> _diagnosisCode;
private readonly IDiagnosisService _diagnosisService;
private readonly List<string> _drainageCode;
private readonly IGroupedObservationService _groupedObservationService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly List<string> _intravenousLines;
private readonly List<string> _isolationCode;
private readonly ILightBeaconService _lightBeaconService;
private readonly ILogger<ObservationService> _logger;
private readonly IObservationArchiveRepository _observationArchiveRepository;
private readonly IObservationRepository _observationRepository;
private readonly IPatientService _patientService;
//Suprimo el warning porque detecta que no se usa en el código, pero sí se usa en los test
//#pragma warning disable IDE0051 // Quitar miembros privados no utilizados
//#pragma warning disable CS0169 // El campo '_observationService.createPatientWithORU' nunca se usa
/*private readonly bool createPatientWithORU;
private readonly bool createPatientWithoutLocation;
private readonly bool createPatientWithLocation;
//private readonly bool updatePatientLocationWithORU;
private readonly bool updatePatientDataWithORU;
private readonly bool ArchivePatientWithOru;*/
//#pragma warning restore CS0169 // El campo '_observationService.createPatientWithORU' nunca se usa
//#pragma warning restore IDE0051 // Quitar miembros privados no utilizados
private readonly bool _persistObservationCodes;
private readonly IPointOfCareService _pointOfCareService;
private readonly List<string> _positionCode;
private readonly IRecordingService _recordingService;
private readonly IRelayService _relayService;
private readonly ISubscriberGroupedService _subscriberGroupedService;
private readonly ISubscribersService _subscribersService;
public ObservationService(
IPatientService patientService,
IConfigObservationService configObservationService,
IObservationRepository observationRepository,
IObservationArchiveRepository observationArchiveRepository,
IConfigUnitsService configUnitsService,
IDiagnosisService diagnosisService,
IOptions<ApiSettings> apiSettings,
IOptions<CacheSettings> cacheSettings,
ILightBeaconService lightBeaconService,
IRelayService relayService,
IRecordingService recordingService,
ILogger<ObservationService> logger,
IGroupedObservationService groupedObservationService,
IAlarmService alarmService,
IClientMessageService clientMessageService,
ISubscribersService subscribersService,
ISubscriberGroupedService subscriberGroupedService,
Lazy<ICalculatedObservationsService> calculatedObservationsService,
IHttpContextAccessor httpContextAccessor,
ILocalAuditService auditService,
IPointOfCareService pointOfCareService,
ICacheService cacheService
)
{
_patientService = patientService;
_configObservationService = configObservationService;
_observationRepository = observationRepository;
_observationArchiveRepository = observationArchiveRepository;
_configUnitsService = configUnitsService;
_diagnosisService = diagnosisService;
_lightBeaconService = lightBeaconService;
_relayService = relayService;
_recordingService = recordingService;
_httpContextAccessor = httpContextAccessor;
_auditService = auditService;
_groupedObservationService = groupedObservationService;
_cacheService = cacheService;
_logger = logger;
if (apiSettings == null) throw new Exception("ApiSettings must be defined");
_persistObservationCodes = apiSettings.Value.PersistObservationCodes;
_intravenousLines = apiSettings.Value.IntravenousLinesCode ?? [];
_allergies = apiSettings.Value.AllergiesCode;
_drainageCode = apiSettings.Value.DrainageCode;
_isolationCode = apiSettings.Value.IsolationCode;
_positionCode = apiSettings.Value.PositionCode;
_diagnosisCode = apiSettings.Value.DiagnosisCode;
_alarmService = alarmService;
_clientMessageService = clientMessageService;
_subscribersService = subscribersService;
_subscriberGroupedService = subscriberGroupedService;
_calculatedObservationsService = calculatedObservationsService;
_pointOfCareService = pointOfCareService;
_cacheSettings = cacheSettings.Value;
}
public async Task<List<PatientObservation>> FindLastObservations(ObjectId patientId, int num = 2,
List<string>? filterObservations = null)
{
var result =
await _observationRepository.AggregatedPatientLastObservations(patientId, num, filterObservations);
return result;
}
private async Task<List<PatientObservation>> AggregatedLastObsCached(
ObjectId patientId,
List<Field>? filterObservations,
CancellationToken ct = default)
{
// Normalizar campos
var fieldNames = (filterObservations ?? new())
.Select(f => f.Name ?? string.Empty)
.Where(n => !string.IsNullOrWhiteSpace(n))
.ToList();
// Clave + TTL según PatientObservation / backend configurado
var (key, ttl) = CacheKeys.LatestObservationsKeyWithTtl(
_cacheSettings,
patientId,
fieldNames
);
// GET → MISS → LOCK → AGGREGATE → SET
var result = await _cacheService.GetOrSetObjectAsync(
key,
async () =>
{
var raw = await _observationRepository
.AggregatedPatientLastObservationsByField(patientId, filterObservations);
return raw;
},
ttl, ct);
return result;
}
public async Task<List<PatientObservation>> FindLastObservationsByField(
ObjectId patientId,
List<Field>? filterObservations = null,
bool mapped = true,
CancellationToken ct = default)
{
//lista RAW desde la caché
var raw = await AggregatedLastObsCached(patientId, filterObservations, ct);
if (!mapped)
return raw;
// Mapeo
var mappedList = new List<PatientObservation>();
foreach (var o in raw)
{
var mo = await MapObservationsByName(o);
if (mo != null)
mappedList.Add(mo);
}
return mappedList;
}
public async Task<PatientObservation?> MapObservationsByName(PatientObservation obs)
{
return await _configObservationService.Map(obs, true);
}
public async Task<PatientObservation?> MapObservation(PatientObservation obs, bool onlyByName = false)
{
try
{
_logger.LogTrace("Mapping config Observation obs: {obs} onlyByName: {onlyByName}", obs, onlyByName);
var obs2 = await _configObservationService.Map(obs, onlyByName);
if (obs2 == null)
{
_logger.LogTrace("Mapping obs2 {obs}: Ignored", obs);
return null;
}
var obs3 = await _configUnitsService.Map(obs2);
var obs4 = await _calculatedObservationsService.Value.Map(obs3, onlyByName);
_logger.LogTrace("Mapping calculatedObservations.Map obs4: {obs4}", obs4);
if (obs4 == null)
{
_logger.LogTrace("Mapping obs4 {obs3}: Ignored", obs3);
return null;
}
if (obs4.CheckObservations)
_ = _alarmService.CheckObservationAlarm(obs4);
return obs4;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error Mapping Observation, Ignoring Observation: {obs} Exception:{ex}", obs,
ex.Message);
return null;
}
}
/// <summary>
/// Inserts a new observation if needed
/// </summary>
/// <param name="obs">Observation</param>
/// <param name="persistObs"></param>
/// <param name="mapObs">True if it needs to be inserted</param>
public async Task InsertObservation(PatientObservation obs, bool persistObs = true, bool mapObs = true)
{
try
{
var obs2 = obs;
//only will be false if the obs comes from the inner refactor job
if (mapObs) obs2 = await MapObservation(obs2);
if (obs2 == null)
{
_logger.LogDebug("Mapped observation returns null. Ignored {obs}", obs);
}
else
{
_logger.LogDebug("Mapped {obs2}", obs2);
if (!_persistObservationCodes)
{
obs2.Code = null;
obs2.CodingSystem = null;
obs2.ParentData = null;
}
_ = SendObsBroadcast(obs2);
if (obs2.Persist.HasValue)
persistObs = obs2.Persist.Value;
_logger.LogDebug("Insert {obs}, persist is {persistObs}", obs2, persistObs);
if (persistObs)
{
await _observationRepository.InsertOneAsync(obs2);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, obs2);
_logger.LogDebug("Inserted {obs2}", obs2);
_ = Task.Run(() =>
CheckForGroupedObs(obs2)
);
}
_ = DoRetentionActions(obs2);
}
}
catch (Exception ex)
{
_logger.LogError("Error Inserting observation {obs}. Excepcion; {ex} ", obs, ex);
}
}
public async Task InsertNurseObservation(PatientObservation obs)
{
try
{
var obs2 = obs;
obs2 = await MapObservation(obs2, true);
if (obs2 == null || obs2.Name == null)
{
_logger.LogError("Error mapping observation {obs}", obs.ToString());
return;
}
await _observationRepository.InsertOneAsync(obs2);
var (key, ttl) = CacheKeys.LatestObservationsKeyWithTtl(
_cacheSettings,
obs2.PatientId,
[obs2.Name]
);
// GET → MISS → LOCK → AGGREGATE → SET
var result = _cacheService.GetOrSetObjectAsync(
key,
async () =>
{
return obs2;
},
ttl, default);
_ = SendObsBroadcast(obs2);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, obs2);
}
catch (Exception ex)
{
_logger.LogError("Error Inserting nurse observation {obs}. Excepcion; {ex} ", obs, ex);
}
}
public async Task<bool> InsertIfChanged(string name, PatientObservation observation, bool persistObs = true,
bool mapObs = true)
{
var changedList = await FindLastObservations(observation.PatientId, 1, [name]);
var changed = changedList.All(o => observation.Value != o.Value);
if (!changed) return false;
await InsertObservation(observation, persistObs, mapObs);
return true;
}
public Task SendObsBroadcast(List<PatientObservation> obsList, PatientLocation location)
{
// Display Subscription
var displaySubscribers = _subscribersService.GetSubscribers().Where(s =>
!s.Locations.IsNullOrEmpty() && s.Locations.Any(c =>
c.UnitName == location.UnitName &&
c.Bed == location.Bed &&
c.Room == location.Room
)).ToList();
foreach (var subscriber in displaySubscribers)
foreach (var obs in obsList)
_ = _clientMessageService.SendAsync(subscriber.Id, OperationType.Observation, obs);
return Task.CompletedTask;
}
public Task SendObsBroadcast(List<PatientObservation> obsList, ObjectId pocId)
{
// Display Subscription
var displaySubscribers = _subscribersService.GetSubscribers().Where(s =>
!s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c =>
c == pocId
)).ToList();
foreach (var subscriber in displaySubscribers)
foreach (var obs in obsList)
_ = _clientMessageService.SendAsync(subscriber.Id, OperationType.Observation, obs);
return Task.CompletedTask;
}
public async Task SendObsBroadcast(BasePatientObservation obs)
{
if (obs.Name == null) return;
var patient = obs.Patient ?? await _patientService.FindById(obs.PatientId);
if (patient == null)
{
_logger.LogDebug("Not patient on bd to sendOnBroadcastObs: {obspatientid}", obs.PatientId);
return;
}
_logger.LogTrace(
"sending obs name: {obsname} to patient id: {patientid}, PointOfCare: {patientpointOfCare} {patientbed}",
obs.Name, patient.Id, patient.UnitString, patient.Bed);
var displaySubscribers = _subscribersService.GetSubscribers().Where(s =>
!s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c =>
c == patient.PointOfCareId
)).ToList();
foreach (var subscriber in displaySubscribers)
_ = _clientMessageService.SendAsync(subscriber.Id, OperationType.Observation, obs);
}
public async void ProcessObservations(List<PatientObservation> observations, Patient patient,
DateTime messageTime, ObservationData? observationData = null)
{
try
{
_logger.LogDebug(
"Patient: {patientid} PoC {patientpointOfCare} {patientbed} msg {messageTime} INSERTING {observationsCount} OBSERVATIONS",
patient.Id, patient.PointOfCare, patient.Bed, messageTime, observations.Count);
ParentDataClass? parentData = null;
if (observationData != null)
parentData = new ParentDataClass
{
Code = observationData.Code,
CodingSystem = observationData.CodingSystem,
Name = observationData.Text
};
var obsToInsert = new List<PatientObservation>();
// Alarm to insert
// var alrmToInsert = new List<PatientObservationAlarm>();
foreach (var obs in observations)
{
obs.ParentData = parentData;
obs.MessageTime = messageTime;
obs.PatientId = patient.Id;
obs.Patient = patient;
obs.Id = ObjectId.GenerateNewId();
if (obs.Time == DateTime.MinValue)
obs.Time = DateTime.UtcNow;
if (obs.MessageTime == DateTime.MinValue)
obs.MessageTime = DateTime.UtcNow;
_logger.LogDebug(
"Patient: {patientId} PoC {patientpointOfCare} {patientbed} msg {messageTime} INSERTING observation {obs}",
patient.Id, patient.PointOfCare, patient.Bed, messageTime, obs);
obsToInsert.Add(obs);
}
obsToInsert = await _calculatedObservationsService.Value.MapList(obsToInsert);
_ = Task.WhenAll(obsToInsert.Select(obs =>
Task.Run(() => InsertObservation(obs)))); //.GetAwaiter().GetResult();
}
catch (Exception ex)
{
_logger.LogError(
"Patient: {patientid} PoC {patientpointOfCare} {patientbed} msg {messageTime} ERROR PROCESSING observations. Exception: {ex}",
patient.Id, patient.PointOfCare, patient.Bed, messageTime, ex);
}
}
public Task SaveRequestNurseObsAsync(ApiRequest request)
{
return Task.Run(() => SaveRequestNurseObs(request));
}
public async Task SaveRequest(ApiRequest apiRequest)
{
if (
string.IsNullOrEmpty(apiRequest.PatientNumber) &&
apiRequest.Location != null && apiRequest.Location.IsEmpty()
)
{
_logger.LogDebug("Patient and PointOfCare are nulls");
throw new ApiRequestException("Patient and PointOfCare are nulls");
}
_logger.LogDebug("patientNumber: {PatientNumber} location: {Location}", apiRequest.PatientNumber,
apiRequest.Location);
_logger.LogDebug("RequestType: {Type}", apiRequest.Type);
var patient = await _patientService.FindPatientByApiRequest(apiRequest);
if (patient == null)
{
// NO PATIENTS OR LOCATIONS WERE FOUND
_logger.LogWarning(
"Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring",
apiRequest.PatientNumber, apiRequest.Location);
return;
}
try
{
switch (apiRequest.Type)
{
/*
* ORU_R01 - Unsolicited transmission of an observation message
* ORU_R40 - Unsolicited transmission of an alert observation message
*/
case "ORU_R40": // UNSOLICITED ALERT OBSERVATION
await _alarmService.SaveRequestAsync(apiRequest);
return;
case "ORU_R01": // UNSOLICITED OBSERVATON
if (!apiRequest.Alarms.IsNullOrEmpty())
{
await _alarmService.SaveRequestAsync(apiRequest);
return;
}
// OBSERVATIONS
if (apiRequest.Observation != null &&
(apiRequest.Observations == null || !apiRequest.Observations.Any()))
apiRequest.Observations = [apiRequest.Observation];
var obrcode = apiRequest.ObservationData?.Code ?? "";
if (apiRequest.Observations != null)
{
if (_intravenousLines.Contains(obrcode))
await ProcessIntravenousLinesObservation(apiRequest, patient);
else if (_allergies.Contains(obrcode))
await ProcessAllergiesObservation(apiRequest, patient);
else if (_drainageCode.Contains(obrcode))
await ProcessDrainageObservation(apiRequest, patient);
//TODO PARA SALIR EN EL RYC REFACTOR
else if (_isolationCode.Contains(obrcode))
await ProcessIsolationObservation(apiRequest, patient);
//TODO REFACTOR, CAMBIO RAPIDO PARA SALIR RYC, PENSAR SI NO SOLO HAY QUE FIARSE POR EL CÓDIGO SI NO POR TODA LA ESTRUCTURA
//DE CONFIG OBSERVATIONS, A LO MEJOR LA BD TIENE QUE INDICAR SI ES UN PROCESAMIENTO ESPECIAL? REVISAR.
else if (_positionCode.Contains(obrcode) &&
!"Temperatura(ºC)".Equals(apiRequest.ObservationData?.Text))
await ProcessPositionObservation(apiRequest, patient);
else if (_diagnosisCode.Contains(obrcode))
await _diagnosisService.SaveRequest(apiRequest, patient);
else
ProcessObservations(apiRequest.Observations, patient, apiRequest.MessageTime,
apiRequest.ObservationData);
}
break;
default:
_logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Observations",
apiRequest.Type);
throw new ApiRequestException("ApiRequest type " + apiRequest.Type +
" is not valid for Observations");
}
}
catch (Exception ex)
{
_logger.LogError(
"ERROR SAVING REQUEST: exception: {exMessage} trace: {exStackTrace}",
ex.Message, ex.StackTrace); //add traceability
throw new Exception(ex.Message);
}
}
public Task SaveRequestAsync(ApiRequest apiRequest)
{
//return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
return Task.Run(() => SaveRequest(apiRequest));
}
public async Task<IAsyncCursor<PatientObservation>> FindByPatientIdAsync(ObjectId patientId)
{
return await _observationRepository.FindByPatientIdAsync(patientId);
}
public async Task<IAsyncCursor<PatientObservation>> FindByPatientIdAndCodingSystemAsync(ObjectId patientId,
string codingSystem, string name)
{
return await _observationRepository.FindByPatientIdAndCodingSystemAsync(patientId, codingSystem, name);
}
public async Task DeleteByPatientId(ObjectId id)
{
try
{
var deletedObservationList = await _observationRepository.FindByPatientId(id);
if (deletedObservationList == null || deletedObservationList.Count == 0)
return;
_logger.LogDebug("Delete Observations by Patient Id {id}", id);
await _observationRepository.DeleteByPatientId(id);
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", id.ToString()));
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, deletedObservationList,
null);
}
catch (Exception ex)
{
_logger.LogError("Error Deleting by PatientId. Exception: {ex}", ex);
}
}
public async Task Archive(PatientObservation observation)
{
await _observationArchiveRepository.InsertOneAsync(observation);
await _observationRepository.DeleteAsync(observation.Id);
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", observation.PatientId.ToString()));
}
public async Task Archive(Patient patient)
{
await ArchiveByPatientId(patient.Id);
}
public async Task ArchiveByPatientId(ObjectId id)
{
_logger.LogDebug("Archive Observations by Patient Id {id}", id);
var cursor = await FindByPatientIdAsync(id);
while (await cursor.MoveNextAsync())
foreach (var current in cursor.Current)
{
current.Id = ObjectId.GenerateNewId();
await _observationArchiveRepository.InsertOneAsync(current);
}
await DeleteByPatientId(id);
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", id.ToString()));
}
public async Task<List<PatientObservation?>> FindLastIntravenousLinesObservationByLocation(ObjectId patientId)
{
return await _observationRepository.AggregatedPatientActiveIntravenousLinesObservations(patientId);
}
public async Task<Dictionary<ObjectId, DateTime>> FindAllLastPatientObservationTime()
{
return await _observationRepository.FindAllLastPatientObservationTime();
}
public async Task UpdateObservation(PatientObservation observation)
{
var obs = await _observationRepository.FindById(observation.Id);
if (obs != null)
{
await _observationRepository.Update(observation);
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs",observation.PatientId.ToString()));
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, obs, observation);
}
}
public async Task<PatientObservation?> FindLastBeforeDate(ObjectId patientId, DateTime date, string? obsName)
{
return await _observationRepository.FindLastObservationBeforeDate(patientId, obsName, date);
}
public async Task<List<PatientObservation>?> FindAnyWithSameDate(ObjectId patientId, DateTime date,
string? obsName)
{
return await _observationRepository.FindAnyWithSameDate(patientId, obsName, date);
}
//TODO To implement
public async Task<List<PatientObservation>> FindAllBeforeDate(ObjectId patientId, DateTime date,
List<string>? filterObservations = null)
{
return await _observationRepository.FindAnyBeforeDate(patientId, date);
}
public async Task<List<PatientObservation>> FindLatestUniqueValuesByName(ObjectId patientId, string name,
int? expires)
{
return await _observationRepository.FindLatestUniqueValuesByName(patientId, name, expires);
}
//TODO To implement
public Task<List<PatientObservation>> FindAllAfterDate(ObjectId patientId, DateTime date,
List<string>? filterObservations = null)
{
throw new NotImplementedException();
}
public async Task<IEnumerable<PatientObservation>> FindLastNotExpiredObservatonsByPatient(ObjectId patientId,
string name, int? endAfter = null, int? num = null)
{
return await _observationRepository.FindLastNotExpiredObservatonsByPatient(patientId, name, endAfter, num);
}
/// <summary>
/// Retrieve all observations with expire time from configObservation service
/// Check all of this observations in patient_observations and expire them if (obs.time + config.expires) smaller than
/// current time
/// mark as expired in bd.
/// </summary>
public async Task CheckAndExpireObservations()
{
var count = 0;
_logger.LogDebug("start checking expired observations");
await using var enumerator = FindNotExpiredObservationsShouldBeExpired().GetAsyncEnumerator();
while (await enumerator.MoveNextAsync())
{
count++;
var current = enumerator.Current;
current.Expired = true;
await _observationRepository.Update(current);
}
_logger.LogDebug("found observations {count} ", count);
}
public async Task UpdateExpiredObservations(List<PatientObservation> patientObservations)
{
await _observationRepository.UpdateExpiredObservations(patientObservations);
var id = patientObservations.First().Id;
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", id.ToString()));
foreach (var observation in patientObservations)
{
var auxObs = await _auditService.DeepCopyAsync(observation);
observation.Expired = true;
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxObs, observation);
}
}
public async IAsyncEnumerable<PatientObservation> FindNotExpiredObservationsShouldBeExpired()
{
var expiringObservations = await _configObservationService.GetAllConfigs();
var expiringList = expiringObservations.ToList();
expiringList.RemoveAll(z => z.Expires == null);
var count = 0;
var uniqueNames = expiringList
.Select(item => item.Name)
.Distinct()
.ToList();
using var enumerator = (await _observationRepository.FindNotExpired(uniqueNames)).GetEnumerator();
while (enumerator.MoveNext())
{
count++;
var current = enumerator.Current;
var patientid = current.PatientId;
var patient = await _patientService.FindById(patientid);
if (patient == null)
{
await _observationRepository.FindByPatientId(patientid);
await _observationRepository.DeleteByPatientId(patientid);
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", patientid.ToString()));
continue;
}
if(current.Name == null) continue;
await _configObservationService.GetConfigObservationItemsByName(current.Name);
var configObs = expiringObservations?
.FirstOrDefault();
if (configObs == null) continue;
if (!double.TryParse(configObs.Expires.ToString(), out var expires))
continue;
var expectedExpireTime = current.Time.AddMinutes(expires);
if (DateTime.Now > expectedExpireTime) yield return current;
}
_logger.LogDebug("expired observations retrieved {count} observations", count);
}
public async Task ExpireObservations()
{
//TODO expire each section
var expiringObservations = await _configObservationService.GetAllConfigs();
var expiringList = expiringObservations.ToList();
expiringList.RemoveAll(z => z.Expires is null or <= 0);
await _observationRepository.ExpireExpiredObservations(expiringList.Distinct()
.ToList());
}
public async Task ExpireObservationsAndRecalculateAsync()
{
try
{
GlobalData.AddData("isCheckingExpiration", true);
var configObservations =
await _configObservationService.GetAllConfigs();
var names = configObservations.Select(o => o.Name).ToList();
var patients = await _patientService.FindAll();
foreach (var patient in patients)
{
var listField = names?.Select(n => new Field
{
Name = n,
Last = 1,
OnlyExpired = true
}).ToList();
var lastPatientObservationsByName =
await _observationRepository.AggregatedPatientLastObservationsByField(patient.Id, listField);
foreach (var obs in
lastPatientObservationsByName)
await InsertObservation(obs, false, false); //Not really insert, only makes calcs
}
var obsToExpireList = new List<PatientObservation>();
var i = 0;
var count = 0;
await using var enumerator = FindNotExpiredObservationsShouldBeExpired().GetAsyncEnumerator();
while (await enumerator.MoveNextAsync())
{
i++;
//every 1000 cut list to avoid memory leaks when there are tons of data.
if (i > 1000)
{
await UpdateExpiredObservations(obsToExpireList);
i = 0;
obsToExpireList.Clear();
}
var current = enumerator.Current;
current.Expired = true;
obsToExpireList.Add(current);
count++;
}
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.PatientObservations));
_logger.LogDebug("number of expired observations should be expired:{count}", count);
}
catch (Exception ex)
{
_logger.LogError("ERROR expire observations And recalculate {exMessage} trace: {exStackTrace}",
ex.Message, ex.StackTrace);
}
GlobalData.AddData("isCheckingExpiration", false);
}
public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
{
await _observationRepository.UpdateManyObjectId(nameId, id, oldId);
}
public async Task InsertSimpleObservation(PatientObservation observation)
{
await _observationRepository.InsertOneAsync(observation);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, observation);
}
public async Task<List<PatientObservation>> FindAllBetweenDates(
ObjectId patientId,
DateTime? startDate = null,
DateTime? endDate = null,
List<string>? filterObservations = null,
bool fromArchived = false,
PaginationFilter? filter = null
)
{
try
{
var collection = fromArchived
? _observationArchiveRepository.Collection
: _observationRepository.Collection;
var filterBuilder = Builders<PatientObservation>.Filter;
var conditions = new List<FilterDefinition<PatientObservation>>
{
filterBuilder.Eq(o => o.PatientId, patientId),
filterBuilder.Ne(o => o.Name, null),
//filterBuilder.In(o => o.Name, filterObservations ?? new List<string?>()),
filterBuilder.Gt(o => o.Time, startDate ?? DateTime.MinValue),
filterBuilder.Lt(o => o.Time, endDate ?? DateTime.MaxValue)
};
if (filterObservations != null && filterObservations.Any())
conditions.Add(filterBuilder.In(o => o.Name, filterObservations));
var combinedFilter = filterBuilder.And(conditions);
var findOptions = new FindOptions<PatientObservation>
{
Skip = (filter?.PageNumber - 1) * filter?.PageSize,
Limit = filter?.PageSize
};
var cursor = await collection.FindAsync(combinedFilter, findOptions);
var observationsList = await cursor.ToListAsync();
return observationsList;
}
catch (Exception ex)
{
Log.Error("Error while getting PatientObservation between dates. Exception: {ex}", ex);
throw;
}
}
public async Task ExpireAlertsAndPowerOffAsync()
{
try
{
GlobalData.AddData("isCheckingAlertsExpiration", true);
var pointOfCares = await _pointOfCareService.GetAllConfigs();
pointOfCares = pointOfCares
.Where(box =>
box.Configuration is { BeaconList: not null } &&
box.Configuration!.BeaconList.Any() &&
!box.Configuration!.BeaconList.First().Options.Emulate)
.ToList();
var configObs = await _configObservationService.GetAllConfigs();
var configList = configObs.ToList();
configList.RemoveAll(z => z.Alarm is not { Enabled: true });
if (configList.Count == 0)
return;
var patients = await _patientService.FindAll();
if (!patients.Any())
return;
//apagamos balizas de los boxes que no tienen pacientes por si se ha quedado alguna encendida
var emptyBoxes = pointOfCares
.Where(box => box.Status != StatusEnum.PointOfCare.InUse)
.ToList();
emptyBoxes.ForEach(async void (b) =>
{
try
{
await _lightBeaconService.PowerOffLed(b.Id);
}
catch (Exception ex)
{
_logger.LogError("Exception powerOffLed {exMessage} trace: {exStackTrace}",
ex.Message, ex.StackTrace);
}
});
var configObservationItems = configObs.ToList();
var endAfterDefaultValue = configObservationItems
.Where(c => c.Alarm != null)
.Select(c =>
c.Alarm?.EndAfter ?? 0) // comprobación de null aquí por seguridad adicional.
.DefaultIfEmpty(0)
.Max();
foreach (var patient in patients)
{
//Nos quedamos con los pacientes que tienen cama
if (patient.Location.UnitName == nameof(VirtualPointOfCare.Moved) ||
patient.Location.UnitName == nameof(VirtualPointOfCare.Deleted) ||
patient.Location.UnitName == nameof(VirtualPointOfCare.Pushed) ||
patient.Location.UnitName == nameof(VirtualPointOfCare.Unknown))
continue;
_ = CheckBeacon(patient, configObs, endAfterDefaultValue);
_ = CheckRelay(patient, configObs, endAfterDefaultValue);
_ = CheckRecordings(patient, pointOfCares, configObs, endAfterDefaultValue);
}
}
catch (Exception ex)
{
_logger.LogError("ERROR expire observations And recalculate {exMessage} trace: {exStackTrace}",
ex.Message, ex.StackTrace);
}
GlobalData.AddData("isCheckingAlertsExpiration", false);
}
public async Task<PaginationResponse<PatientObservation>> GetPaginatedObservations(PaginationFilter filter)
{
var result = _observationRepository.GetPaginatedObservations(filter);
var count = await result.CountDocumentsAsync();
var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize)
.Limit(filter.PageSize)
.ToCursorAsync();
var dataList = await data.ToListAsync();
return new PaginationResponse<PatientObservation>(dataList, filter.PageNumber, filter.PageSize, count);
}
private async Task DoRetentionActions(PatientObservation obs)
{
try
{
var result = await _configObservationService.RetentionActions(obs);
if (result == null || !result.RetentionPolicyValue.HasValue || string.IsNullOrEmpty(obs.Name)) return;
var deletedObs = new List<PatientObservation>();
switch (result.RetentionPolicy)
{
case RetentionPolicy.DeleteOlderDays:
deletedObs =
await _observationRepository.DeleteOlderDaysAsync(obs.Name, result.RetentionPolicyValue.Value);
break;
case RetentionPolicy.DeleteOlderSeconds:
deletedObs = await _observationRepository.DeleteOlderSecondsAsync(obs.Name,
result.RetentionPolicyValue.Value);
break;
case RetentionPolicy.DeleteOlderNumber:
deletedObs = await _observationRepository.DeleteOlderNumberAsync(obs.Name,
result.RetentionPolicyValue.Value);
break;
case RetentionPolicy.NoDelete:
default:
break;
}
foreach (var observation in deletedObs)
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, observation, null);
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", obs.PatientId.ToString()));
}
catch (Exception ex)
{
_logger.LogError("Error doing retention actions. Exception: {ex}", ex);
}
}
private async void CheckForGroupedObs(PatientObservation obs)
{
try
{
//Removed ignore obs to future, we accept them and recalculate and front decide what to do
foreach (var wsg in _subscriberGroupedService.GetGrouped().Where(g =>
g.PatientId == obs.PatientId && obs.Name != null && g.Names.Contains(obs.Name) &&
g.IsNewObservationRelevantForGroup(obs)))
{
wsg.Timer.Stop();
var groupedField = new GroupedField
{
Names = wsg.Names,
Name = obs.Name ?? string.Empty,
Max = wsg.Max,
Group = wsg.Group.FirstOrDefault().Value,
Regularity = wsg.Regularity,
Result = wsg.Result,
Since = wsg.Since,
StartTimeShift = wsg.StartTimeShift
};
var newGroupedObservation = await _groupedObservationService
.GenerateGroupedObservation(obs.PatientId, groupedField, wsg.LastGroupedObservationObs, obs,
wsg.TimeZoneId);
_subscriberGroupedService.UpdateLastGroupedObsInGroup(wsg.HashCode, newGroupedObservation);
foreach (var gr in wsg.Group)
{
var grName = wsg.Group.GetValue(gr.Key);
if (grName == null) continue;
newGroupedObservation.Group = grName;
_ = Task.Run(() => _clientMessageService.SendAsync(gr.Key, OperationType.GroupedObservation,
newGroupedObservation));
}
//wsg.TimerReestart();
await _cacheService.DeleteByPatternAsync(CacheKeys.GroupedObs(obs.PatientId, obs.Name ?? string.Empty));
}
}
catch (Exception e)
{
_logger.LogError("error checking grouped observations: exception: {eMessage} trace:{eStackTrace}",
e.Message, e.StackTrace);
}
}
private async Task ProcessIntravenousLinesObservation(ApiRequest apiRequest, Patient patient)
{
var isInsertable = false;
_logger.LogDebug("INSERT IntraVenousLineObservation");
if (apiRequest.ObsertationData == null || !apiRequest.ObsertationData.Time.HasValue)
{
_logger.LogError("Error INSERTING IntraVenousLineObservation. ");
return;
}
var obs = new PatientObservation
{
Time = apiRequest.ObsertationData.Time.Value,
Code = apiRequest.ObsertationData.Code,
CodingSystem = apiRequest.ObsertationData.CodingSystem,
PatientId = patient.Id,
ParentData = new ParentDataClass
{
Code = apiRequest.ObservationData?.Code, CodingSystem = apiRequest.ObservationData?.CodingSystem
},
MessageTime = apiRequest.MessageTime
};
//Todos los formatos de texto de vías son Catéter X: + localizacion
var typeLocation = apiRequest.ObservationData?.Text?.Split(':');
obs.Value = new PatientIntravenousLinesValue
{
Type = typeLocation != null ? typeLocation[0] : string.Empty
};
if (typeLocation is { Length: > 1 })
if (obs.Value is PatientIntravenousLinesValue obsValue)
obsValue.Location = typeLocation[1];
if (apiRequest.Observations != null)
{
if (obs.Value is not PatientIntravenousLinesValue obsValue)
return;
foreach (var observation in apiRequest.Observations)
{
var obsValueStr = observation.Value.ToString();
if (obsValueStr == null) continue;
switch (observation.Code)
{
case "273248003":
//In ICCA an intravenouse Line OBS should come with Insertado and Retirado but can come with more, ignore them.
if (obsValueStr.Equals("Insertado") || obsValueStr.Equals("Retirado"))
{
obsValue.Action = observation.Value.ToString() ?? "NULL";
isInsertable = true;
}
else
{
return;
}
break;
case "397898000":
obsValue.RemoveTime = (DateTime?)observation.Value;
break;
case "439272007":
obsValue.InsertTime = (DateTime?)observation.Value;
break;
case "228864003":
obsValue.Duration = observation.Value.ToString() ?? "NULL";
break;
}
}
}
if (!isInsertable) return;
await InsertObservation(obs);
}
private async Task ProcessAllergiesObservation(ApiRequest apiRequest, Patient patient)
{
_logger.LogDebug("INSERT AllergiesObservation");
if (apiRequest.ObservationData == null)
{
_logger.LogError("Error Processing Allergies Observation. ObservationData is null");
return;
}
if (!apiRequest.ObservationData.Time.HasValue)
{
_logger.LogError("Error Processing Allergies Observation. ObservationData time null");
return;
}
var obs = new PatientObservation
{
Time = apiRequest.ObservationData.Time.Value,
Name = apiRequest.ObsertationData?.Text ?? string.Empty,
Code = apiRequest.ObservationData.Code,
CodingSystem = apiRequest.ObservationData.CodingSystem,
PatientId = patient.Id,
ParentData = new ParentDataClass
{
Code = apiRequest.ObservationData.Code,
CodingSystem = apiRequest.ObservationData.CodingSystem,
Name = apiRequest.ObservationData.Text
},
MessageTime = apiRequest.MessageTime
};
List<PatientAllergiesValue> patientAllergiesValues = [];
PatientAllergiesValue allergiesValues = new();
if (apiRequest.Observations == null)
{
_logger.LogError("Error Processing Allergies Observation. Observations null");
return;
}
for (var i = 0; i <= apiRequest.Observations.Count - 1; i++)
{
switch (apiRequest.Observations[i].Code)
{
case "263490005":
if (apiRequest.Observations[i].Value.ToString() == "Sin alergias conocidas") return;
continue;
case "300916003": //Latex
if (apiRequest.Observations[i].Value.ToString() == "Si")
{
allergiesValues.Type = "Latex";
allergiesValues.Value = "Si";
//patientAllergiesValues.Add(allergiesValues);
//allergiesValues = new PatientAllergiesValue { };
}
break;
case "419199007":
allergiesValues.Type = apiRequest.Observations[i].Value.ToString();
break;
case "277054007":
case "416098002":
allergiesValues.Value = apiRequest.Observations[i].Value.ToString();
break;
case "281296001":
allergiesValues.Notes = apiRequest.Observations[i].Value.ToString();
break;
}
if (i == apiRequest.Observations.Count - 1)
{
if (allergiesValues is { Type: not null })
patientAllergiesValues.Add(allergiesValues);
//allergiesValues = new PatientAllergiesValue { value = new List<string>(), notes = new List<string>() };
allergiesValues = new PatientAllergiesValue();
}
else
{
i++;
if (apiRequest.Observations[i].Code == "300916003" ||
apiRequest.Observations[i].Code == "419199007")
{
if (allergiesValues is { Type: not null })
patientAllergiesValues.Add(allergiesValues);
//allergiesValues = new PatientAllergiesValue { value = new List<string>(), notes = new List<string>() };
allergiesValues = new PatientAllergiesValue();
}
i--;
}
}
obs.Value = patientAllergiesValues;
await InsertObservation(obs);
}
private async Task ProcessIsolationObservation(ApiRequest apiRequest, Patient patient)
{
if (apiRequest.ObservationData is { Text: "Aislamiento", Time: not null })
{
_logger.LogDebug("INSERT Isolation OBS");
var value = apiRequest.ObservationData.Value?.ToString()?.Replace(";", ",");
if (value == null)
{
_logger.LogError("Error Processing Isolation Observation. Value is null");
return;
}
var obs = new PatientObservation
{
Time = apiRequest.ObservationData.Time.Value,
Name = "Isolation",
CodingSystem = "ADAS",
PatientId = patient.Id,
Value = value,
MessageTime = apiRequest.MessageTime
};
await InsertObservation(obs);
}
}
private async Task ProcessPositionObservation(ApiRequest apiRequest, Patient patient)
{
if (apiRequest.ObservationData != null &&
apiRequest.ObservationData?.Text?.ToUpper() == "CAMBIOS POSTURALES")
{
_logger.LogDebug("INSERT Position OBS");
var strValue = apiRequest.ObservationData.Value?.ToString();
if (string.IsNullOrEmpty(strValue))
{
var ob = apiRequest.Observation ?? apiRequest.Observations?.FirstOrDefault();
if (ob != null)
strValue = ob.Value.ToString();
}
if (string.IsNullOrEmpty(strValue))
{
_logger.LogWarning("Position Observation value is null or empty.");
return;
}
if (!apiRequest.ObservationData.Time.HasValue)
{
_logger.LogWarning("Position Observation time is null. ");
return;
}
var obs = new PatientObservation
{
Time = apiRequest.ObservationData.Time.Value,
Name = "Patient_Position",
CodingSystem = "ADAS",
PatientId = patient.Id,
Value = strValue.Replace(";", ","),
MessageTime = apiRequest.MessageTime
};
await InsertObservation(obs);
}
}
private async Task ProcessDrainageObservation(ApiRequest apiRequest, Patient patient)
{
_logger.LogDebug("INSERT DrainageObservation");
if (apiRequest.ObservationData == null || !apiRequest.ObservationData.Time.HasValue ||
apiRequest.Observations == null)
{
_logger.LogWarning("Error Processing Drainage Observation.");
return;
}
var obs = new PatientObservation
{
Time = apiRequest.ObservationData.Time.Value,
Code = apiRequest.ObservationData.Code,
CodingSystem = apiRequest.ObservationData.CodingSystem,
PatientId = patient.Id,
ParentData = new ParentDataClass
{
Code = apiRequest.ObservationData.Code,
CodingSystem = apiRequest.ObservationData.CodingSystem,
Name = apiRequest.ObservationData.Text
},
MessageTime = apiRequest.MessageTime
};
if (apiRequest.Observation != null) obs.Time = apiRequest.Observation.Time;
obs.Value = new PatientDrainagesValue();
if (obs.Value is not PatientDrainagesValue obsVal)
{
_logger.LogDebug("Error Processing Drainage Observation. Value is null. ");
return;
}
foreach (var observation in apiRequest.Observations)
switch (observation.Code)
{
case "138875005":
switch (observation.Name ?? "")
{
case "Tipo de drenaje":
obsVal.Type = observation.Value.ToString();
break;
case "Altura columna(cmH2O)":
if (int.TryParse(observation.Value.ToString(), out var valueParsed))
obsVal.Height = valueParsed;
break;
}
break;
case "10546003":
obsVal.Location = observation.Value.ToString();
break;
case "56868008":
if (int.TryParse(observation.Value.ToString(), out var obsValue)) obsVal.Volume = obsValue;
break;
}
await InsertObservation(obs);
}
/**
* Las obs que se insertan de forma manual desde nurse deben seguir la logica contraria a las obs
* recibidas desde el censo
*/
public async Task SaveRequestNurseObs(ApiRequest apiRequest)
{
var patient = await _patientService.FindPatientByApiRequest(apiRequest);
if (patient == null)
{
// NO PATIENTS OR LOCATIONS WERE FOUND
_logger.LogWarning(
"Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring",
apiRequest.PatientNumber, apiRequest.Location);
return;
}
if (apiRequest.Observations != null)
ProcessObservations(apiRequest.Observations, patient, apiRequest.MessageTime,
apiRequest.ObservationData);
}
private async Task CheckRecordings(Patient patient, List<PointOfCare> configs,
IEnumerable<ConfigObservation> obsConfigList, int defaultValue)
{
//paramos grabación si no hay alarmas activas y hay una grabación
var recordingEndAfter = obsConfigList
.Where(c => c.Alarm is { Recording: not null })
.Select(c => c.Alarm is { Recording: not null } ? c.Alarm.Recording.EndAfter : defaultValue)
.DefaultIfEmpty(defaultValue)
.Max();
var obsListWithRecordingAlarms =
await _observationRepository.FindLastNotExpiredObservatonsByPatient(patient.Id, "ADAS_ALARM",
recordingEndAfter);
if (obsListWithRecordingAlarms.Any())
return;
var boxCfg = configs.FirstOrDefault(box => box.Id == patient.PointOfCareId);
var roomId = boxCfg?.Configuration?.Id;
if (roomId == null || !int.TryParse(roomId.ToString(), out var roomIdParsed))
return;
var currentRecordings = await _recordingService.GetRecordings(roomIdParsed);
if (currentRecordings == null || !currentRecordings.Any())
return;
var manualRecordings =
currentRecordings.Where(r => r.AlarmType == AlarmEnum.Type.Manual);
foreach (var rec in manualRecordings)
{
_logger.LogDebug("PatientId: {obsid}. Send Stop Manual Recording Power Off", patient.Id);
var manualRecording = new ManualRecording
{
Recording = new MRecording
{
StartRecordingTime = rec.StartRecordingTime,
StopRecordingTime = rec.StopRecordingTime ?? DateTime.Now
}
};
if (boxCfg != null)
await _recordingService.SendRecordingData(patient, boxCfg, manualRecording, false);
}
}
private async Task CheckRelay(Patient patient,
IEnumerable<ConfigObservation> obsConfigList, int defaultValue)
{
var relayEndAfter = obsConfigList
.Where(c => c.Alarm is { OpenDoor: not null })
.Select(c => c.Alarm is { OpenDoor: not null } ? c.Alarm.OpenDoor.EndAfter : defaultValue)
.DefaultIfEmpty(defaultValue)
.Max();
var obsListWithRelayAlarms =
await _observationRepository.FindLastNotExpiredObservatonsByPatient(patient.Id, "ADAS_ALARM",
relayEndAfter);
if (!patient.PointOfCareId.HasValue)
{
_logger.LogError("PatientId: {PatId} has no points of Care on check relay", patient.Id);
return;
}
//Si no hay activas apagamos Relay
var relayConfig = await _pointOfCareService.FindById(patient.PointOfCareId.Value);
if (relayConfig is { Configuration.RelayIdList: not null } && !obsListWithRelayAlarms.Any())
{
_logger.LogDebug("PatientId: {PatId}. Send relay Power Off", patient.Id);
var relays = _relayService.GetRelayInList(relayConfig.Configuration.RelayIdList);
relays.ForEach(async void (r) =>
{
try
{
await _relayService.PowerOff(r);
}
catch (Exception e)
{
_logger.LogError("PatientId: {PatId}. Exception Power Off Relay {exMessage} trace: {exStackTrace}",
patient.Id, e.Message, e.StackTrace);
}
});
}
}
private async Task CheckBeacon(Patient patient,
IEnumerable<ConfigObservation> obsConfigList, int defaultValue)
{
var beaconEndAfter = obsConfigList
.Where(c => c.Alarm is { Enabled: true, Beacon.Enabled: true })
.Select(c => c.Alarm is { Beacon: not null } ? c.Alarm.Beacon.EndAfter : defaultValue)
.DefaultIfEmpty(defaultValue)
.Max();
var obsListWithBeaconAlarms =
await _observationRepository.FindLastNotExpiredObservatonsByPatient(patient.Id, "ADAS_ALARM",
beaconEndAfter);
//TODO: Heredar de la observación el endAfter si el de la baliza está 0
//Si no tiene alarmas activas y no está ya apagada lo hacemos
if (!obsListWithBeaconAlarms.Any() && patient.PointOfCareId.HasValue)
{
_logger.LogDebug("PatientId: {patientId}. Send Beacon code Power Off", patient.Id);
await _lightBeaconService.PowerOffLed(patient.PointOfCareId.Value);
}
}
}