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 _allergies; private readonly ILocalAuditService _auditService; private readonly Lazy _calculatedObservationsService; private readonly IClientMessageService _clientMessageService; private readonly IConfigObservationService _configObservationService; private readonly IConfigUnitsService _configUnitsService; private readonly List _diagnosisCode; private readonly IDiagnosisService _diagnosisService; private readonly List _drainageCode; private readonly IGroupedObservationService _groupedObservationService; private readonly IHttpContextAccessor _httpContextAccessor; private readonly List _intravenousLines; private readonly List _isolationCode; private readonly ILightBeaconService _lightBeaconService; private readonly ILogger _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 _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, IOptions cacheSettings, ILightBeaconService lightBeaconService, IRelayService relayService, IRecordingService recordingService, ILogger logger, IGroupedObservationService groupedObservationService, IAlarmService alarmService, IClientMessageService clientMessageService, ISubscribersService subscribersService, ISubscriberGroupedService subscriberGroupedService, Lazy 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> FindLastObservations(ObjectId patientId, int num = 2, List? filterObservations = null) { var result = await _observationRepository.AggregatedPatientLastObservations(patientId, num, filterObservations); return result; } private async Task> AggregatedLastObsCached( ObjectId patientId, List? 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> FindLastObservationsByField( ObjectId patientId, List? 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(); foreach (var o in raw) { var mo = await MapObservationsByName(o); if (mo != null) mappedList.Add(mo); } return mappedList; } public async Task MapObservationsByName(PatientObservation obs) { return await _configObservationService.Map(obs, true); } public async Task 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; } } /// /// Inserts a new observation if needed /// /// Observation /// /// True if it needs to be inserted 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 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 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 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 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(); // Alarm to insert // var alrmToInsert = new List(); 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> FindByPatientIdAsync(ObjectId patientId) { return await _observationRepository.FindByPatientIdAsync(patientId); } public async Task> 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> FindLastIntravenousLinesObservationByLocation(ObjectId patientId) { return await _observationRepository.AggregatedPatientActiveIntravenousLinesObservations(patientId); } public async Task> 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 FindLastBeforeDate(ObjectId patientId, DateTime date, string? obsName) { return await _observationRepository.FindLastObservationBeforeDate(patientId, obsName, date); } public async Task?> FindAnyWithSameDate(ObjectId patientId, DateTime date, string? obsName) { return await _observationRepository.FindAnyWithSameDate(patientId, obsName, date); } //TODO To implement public async Task> FindAllBeforeDate(ObjectId patientId, DateTime date, List? filterObservations = null) { return await _observationRepository.FindAnyBeforeDate(patientId, date); } public async Task> FindLatestUniqueValuesByName(ObjectId patientId, string name, int? expires) { return await _observationRepository.FindLatestUniqueValuesByName(patientId, name, expires); } //TODO To implement public Task> FindAllAfterDate(ObjectId patientId, DateTime date, List? filterObservations = null) { throw new NotImplementedException(); } public async Task> FindLastNotExpiredObservatonsByPatient(ObjectId patientId, string name, int? endAfter = null, int? num = null) { return await _observationRepository.FindLastNotExpiredObservatonsByPatient(patientId, name, endAfter, num); } /// /// 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. /// 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 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 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(); 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> FindAllBetweenDates( ObjectId patientId, DateTime? startDate = null, DateTime? endDate = null, List? filterObservations = null, bool fromArchived = false, PaginationFilter? filter = null ) { try { var collection = fromArchived ? _observationArchiveRepository.Collection : _observationRepository.Collection; var filterBuilder = Builders.Filter; var conditions = new List> { filterBuilder.Eq(o => o.PatientId, patientId), filterBuilder.Ne(o => o.Name, null), //filterBuilder.In(o => o.Name, filterObservations ?? new List()), 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 { 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> 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(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(); 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 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(), notes = new List() }; 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(), notes = new List() }; 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 configs, IEnumerable 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 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 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); } } }