using System.Diagnostics; 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.DTO; using adas_core.Domain.Models.Filter; using adas_core.Domain.Models.GroupedObservations; using adas_core.Domain.Models.Masters; 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; /// /// Provides a concrete implementation of the interface, /// encapsulating patient-related service operations. /// public class PatientService : IPatientService { private readonly Lazy _admissionService; private readonly IAppointmentService _appointmentService; private readonly ILocalAuditService _auditService; private readonly IClientMessageService _clientMessageService; private readonly bool _createPatientWithAdtA08; private readonly bool _createPatientWithLocation; private readonly bool _createPatientWithOru; private readonly bool _cretatePatientWithoutPatientNumber; private readonly IDiagnosisService _diagnosisService; private readonly IDischargeService _dischargeService; private readonly IDisplayConfigService _displayConfigService; private readonly IDisplayService _displayService; private readonly bool _findPatientByLocationWithoutId; private readonly IGroupedObservationService _groupedObservationService; private readonly IHttpContextAccessor _httpContextAccessor; private readonly string _iccaFacility; private readonly ListSettings _listSettings; private readonly ILogger _logger; private readonly IMasterListServiceFactory _masterListServiceFactory; private readonly Lazy _observationService; private readonly OnArchiveAction _onArchiveAction; private readonly IPatientArchiveRepository _patientArchiveRepository; private readonly IPatientCarePlanService _patientCarePlanService; private readonly IPatientRepository _patientRepository; private readonly IPoCMappingService _pocMappingService; private readonly IPointOfCareService _pointOfCareService; private readonly Lazy _pumpService; private readonly bool _pushPatientWithOru; private readonly IRecordingAlertService _recordingAlertService; private readonly List _sendingFacility; private readonly ISubscriberGroupedService _subscriberGroupedService; private readonly ISubscribersService _subscribersService; private readonly Lazy _treatmentService; private readonly IUnitService _unitService; private readonly bool _updatePatientDataWithAdtA02; private readonly bool _updatePatientDataWithOru; private readonly bool _updatePatientLocationWithOru; public PatientService( IPatientRepository patientRepository, IPatientArchiveRepository patientArchiveRepository, Lazy observationService, Lazy treatmentService, IPoCMappingService pocMappingService, IDiagnosisService diagnosisService, IAppointmentService appointmentService, Lazy pumpService, IRecordingAlertService recordingAlertService, IDischargeService dischargeService, IOptions apiSettings, IOptions listSettings, ILogger logger, IClientMessageService clientMessageService, ISubscribersService subscribersService, ISubscriberGroupedService subscriberGroupedService, IUnitService unitService, IDisplayService displayService, IPointOfCareService pointOfCareService, Lazy admissionService, IDisplayConfigService displayConfigService, IGroupedObservationService groupedObservationService, IPatientCarePlanService patientCarePlanService, IHttpContextAccessor httpContextAccessor, ILocalAuditService auditService, IMasterListServiceFactory masterListServiceFactory) { _patientRepository = patientRepository; _patientArchiveRepository = patientArchiveRepository; _observationService = observationService; _treatmentService = treatmentService; _diagnosisService = diagnosisService; _appointmentService = appointmentService; _pumpService = pumpService; _recordingAlertService = recordingAlertService; _logger = logger; _clientMessageService = clientMessageService; _subscribersService = subscribersService; _subscriberGroupedService = subscriberGroupedService; _pocMappingService = pocMappingService; _dischargeService = dischargeService; _unitService = unitService; _displayService = displayService; _pointOfCareService = pointOfCareService; _admissionService = admissionService; _displayConfigService = displayConfigService; _groupedObservationService = groupedObservationService; _patientCarePlanService = patientCarePlanService; _httpContextAccessor = httpContextAccessor; _auditService = auditService; _masterListServiceFactory = masterListServiceFactory; _ = Enum.TryParse(apiSettings.Value.OnArchivePatientAction, out _onArchiveAction); _createPatientWithOru = apiSettings.Value.CreatePatientWithOru; _pushPatientWithOru = apiSettings.Value.PushPatientWithOru; _createPatientWithLocation = apiSettings.Value.CreatePatientWithLocation; _updatePatientLocationWithOru = apiSettings.Value.UpdatePatientLocationWithOru; _updatePatientDataWithOru = apiSettings.Value.UpdatePatientDataWithOru; _findPatientByLocationWithoutId = apiSettings.Value.FindPatientByLocationWithoutId; _iccaFacility = apiSettings.Value.IccaFacility ?? string.Empty; _sendingFacility = apiSettings.Value.SendingFacility ?? []; _cretatePatientWithoutPatientNumber = apiSettings.Value.CreatePatientWithoutPatientNumber; _createPatientWithAdtA08 = apiSettings.Value.CreatePatientWithAdtA08; _updatePatientDataWithAdtA02 = apiSettings.Value.UpdatePatientDataWithAdtA02; _listSettings = listSettings.Value; } /// /// Inserts a new patient into the repository after validating it. If the patient fails validation, the method returns without performing the insertion. After a successful insert, a new patient broadcast notification is dispatched asynchronously. /// /// The patient entity to be inserted into the repository. public async Task Insert(Patient patient) { if (!CheckPatient(patient)) return; await _patientRepository.InsertOneAsync(patient); _ = SendNewPatientBroadcast(patient); } /// /// Retrieves a patient by identifier and returns a locale-translated version of the patient record. /// Returns null when the patient cannot be resolved or has no associated unit, and falls back to the untranslated patient when the unit lookup fails. /// /// The string representation of the patient identifier to parse and look up. /// The locale used to translate the patient data via the master list service. /// A task containing the translated patient, the untranslated patient if the unit is not found, or null if the patient has no associated unit. public async Task FindByPatientIdWithLocale(string patientId, LocaleEnum localeEnum) { ObjectId.TryParse(patientId, out var id); var patient = await FindById(id); if (patient?.UnitId == null) return null; var unit = await _unitService.FindById(patient.UnitId); if (unit == null) return patient; var patientWithLocale = await _masterListServiceFactory.GetPatientTraslated(unit, localeEnum, patient); return patientWithLocale; } /// /// Asynchronously inserts a new patient into the system after validating it. /// If the patient fails validation, the method returns without performing the insert. /// On a successful insert, an audit log is created and a broadcast is dispatched to notify other components. /// /// The patient entity to be inserted into the system. public async Task InsertAsync(Patient patient) { if (!CheckPatient(patient)) return; await _patientRepository.InsertOneAsync(patient); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, patient); _ = SendNewPatientBroadcast(patient); } /// /// Moves a patient from one point of care to another, transferring the patient location data, updating the point of care statuses, and notifying any associated discharge record and subscribers. /// /// The patient to be moved to the new point of care. /// The identifier of the destination point of care. /// The identifier of the source point of care that the patient is leaving. /// A task that resolves to true when the patient is successfully moved; otherwise, false if the new point of care is null, the old point of care is null, or the new point of care is already in use or locked. public async Task Move(Patient patient, ObjectId newPocId, ObjectId oldPocId) { var newPoc = await _pointOfCareService.GetInfo(newPocId); if (newPoc == null) { _logger.LogError("Unable to move patient {PatientId} to new poc {NewPocId} is null", patient.Id, newPocId); return false; } if (newPoc.Status != StatusEnum.PointOfCare.InUse && newPoc.Status != StatusEnum.PointOfCare.Locked) { var oldPoc = await _pointOfCareService.GetInfo(oldPocId); if (oldPoc == null) { _logger.LogError("Unable to move patient {PatientId} from old poc {OldPocId} is null", patient.Id, oldPocId); return false; } patient.Location = new PatientLocation(newPoc.UnitName, newPoc.Bed, newPoc.Room); patient.Bed = newPoc.Bed; patient.UnitString = newPoc.UnitName; patient.Room = newPoc.Room; patient.PointOfCareId = newPoc.Id; patient.UnitId = newPoc.UnitId; newPoc.Patient = patient; await Update(patient); await _pointOfCareService.SetPointOfCareStatus(newPoc.Id, StatusEnum.PointOfCare.InUse); await _pointOfCareService.SetPointOfCareStatus(oldPoc.Id, StatusEnum.PointOfCare.Available); _pointOfCareService.CheckNextAdmission(oldPoc.Id); var currentDischarge = await _dischargeService.GetDischargeByPatientId(patient.Id); if (currentDischarge != null) { _dischargeService.SendDischargeBroadcast(currentDischarge, OperationType.DeleteDischarge); currentDischarge.PatientLocation = patient.Location; currentDischarge.PointOfCareId = newPoc.Id; currentDischarge.UnitId = newPoc.UnitId; currentDischarge.Patient = patient; await _dischargeService.UpdateDischargeAsync(currentDischarge); } // Enviar en base a la config de display que tenga cada subscriptor _ = SendLastObsToSubscriberByPoc(newPoc, patient); // Enviar las grupadas dependiendo del config del subscriptor _ = SendLastGroupedObsToSubscriberByPoc(newPoc.Id, oldPoc.Id, patient); return true; } _logger.LogError("Unable to move patient {PatientId} to new poc {NewPocId} is in use or locked", patient.Id, newPocId); return false; } /// /// Retrieves a patient by their unique identifier, optionally enriching the result with location details /// (point of care, bed, room, and unit) when available. Returns null if the identifier is empty /// or the patient cannot be found. /// /// The unique identifier of the patient to retrieve. /// When true, enriches the patient with location information such as bed, room, and unit name. /// A containing the found patient, or null if no patient matches the identifier or the identifier is empty. /// Thrown when the patient has an associated point of care that cannot be found. public async Task FindById(ObjectId id, bool withLocation = false) { if (id == ObjectId.Empty) return null; var patient = await _patientRepository.FindById(id); if (withLocation && patient != null) { if (patient.PointOfCareId.HasValue) { var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); patient.Bed = poc.Bed; patient.Room = poc.Room; } if (patient.UnitId.HasValue) { var unit = await _unitService.FindById(patient.UnitId.Value); patient.UnitString = unit?.Name; } } // var lastObs = await _observationService.Value.FindLastObservations(patient.Id,1); // if (lastObs.Any()) // { // patient.LastObservationDate = lastObs.First().Time; // } // TODO: check if it can be cached or served faster return patient; } /// /// Retrieves a associated with the specified location by resolving the unit from its name and the point of care from the bed identifier within that unit. /// Returns when the location is null or missing a unit name or bed, when no matching unit exists, or when no matching point of care is found. /// /// The patient location containing the unit name and bed used to locate the patient. /// A if one is found for the given location; otherwise, . public async Task FindByLocation(PatientLocation? location) { if (location?.UnitName == null || location.Bed == null) return null; var unit = await _unitService.FindByName(location.UnitName); if (unit == null) return null; var poc = await _pointOfCareService.FindByBedAndUnitId(location.Bed, unit.Id); if (poc == null) return null; return await FindByUnitAndPocId(unit.Id, poc.Id); } /// /// Retrieves a patient by their point of care identifier by delegating the lookup to the patient repository. /// Returns null when no matching patient is found. /// /// The point of care identifier used to locate the patient. /// A if one is found with the specified point of care identifier; otherwise, null. public async Task FindByPointOfCareId(ObjectId pocId) { return await _patientRepository.FindByPointOfCareId(pocId); } /// /// Asynchronously retrieves the total number of patients associated with the specified unit identifier by delegating to the patient repository. /// /// The of the unit whose patients should be counted. /// A that resolves to the number of patients linked to the given unit. public async Task CountPatientsByUnitId(ObjectId unitId) { return await _patientRepository.CountByUnitId(unitId); } /// /// Archives a patient according to the configured : either inserts the patient into the archive repository (with the current UTC time as the archive date) or simply performs no archival step. The patient record is then deleted, an audit log entry is created, associated patient data is archived, and if the patient was assigned to a point of care, a delete broadcast is sent and that point of care is set to Available. /// /// The patient to archive. If its DisTime is null, it is set to the current UTC time before further processing. public async Task ArchivePatient(Patient patient) { patient.DisTime ??= DateTime.UtcNow; //var sections = sectionService.FindByPatient(patient.id); var pocId = patient.PointOfCareId; _logger.LogDebug("{OnArchiveAction} patient {Patient}", _onArchiveAction, patient); switch (_onArchiveAction) { case OnArchiveAction.Archive: default: patient.ArchiveDate = DateTime.UtcNow; await _patientArchiveRepository.InsertOneAsync(patient); break; case OnArchiveAction.Delete: //No hay archivado break; } await _patientRepository.Delete(patient.Id); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, patient, null); await ArchivePatientData(patient.Id); if (pocId.HasValue) { _ = SendDeletePatientBroadcast(patient.Id, pocId.Value); //cambiamos el estado del punto de cuidado a disponible await _pointOfCareService.SetPointOfCareStatus(pocId.Value, StatusEnum.PointOfCare.Available); } } /// /// Archives or deletes all data associated with a patient based on the configured OnArchiveAction. When the action is set to Archive (the default), patient observations, treatments, diagnoses, appointments, pumps, recording alerts, and care plans are archived; when set to Delete, observations, treatments, diagnoses, appointments, pumps, and recording alerts are deleted (care plan archival is not performed in this case). /// /// The of the patient whose related data should be archived or deleted. public async Task ArchivePatientData(ObjectId patientid) { _logger.LogDebug("Archiving patient data action: {onArchiveAction} : {patientid}", _onArchiveAction, patientid); switch (_onArchiveAction) { case OnArchiveAction.Archive: default: await _observationService.Value.ArchiveByPatientId(patientid); await _treatmentService.Value.ArchiveByPatientId(patientid); await _diagnosisService.ArchiveByPatientId(patientid); await _appointmentService.ArchiveByPatientId(patientid); await _pumpService.Value.ArchiveByPatientId(patientid); await _recordingAlertService.ArchiveByPatientId(patientid); await _patientCarePlanService.ArchiveByPatientId(patientid); break; case OnArchiveAction.Delete: await _observationService.Value.DeleteByPatientId(patientid); await _treatmentService.Value.DeleteByPatientId(patientid); await _diagnosisService.DeleteByPatientId(patientid); await _appointmentService.DeleteByPatientId(patientid); await _pumpService.Value.DeleteByPatientId(patientid); await _recordingAlertService.DeleteByPatientId(patientid); break; } } /// /// Updates a patient record in the repository, handling special cases such as resolving duplicated "Sin cama" bed assignments from ICCADB by generating a unique bed identifier, and assigning a patient number when missing. Also creates an audit log comparing the previous and updated patient and asynchronously broadcasts the update. /// /// The patient entity to update. public async Task Update(Patient patient) { var oldPatient = await _patientRepository.FindById(patient.Id); try { _logger.LogDebug("Update {patient}", patient); //To solve multi "sin cama" from ICCADB making duplicated keys error on mongodb. if (patient.Location is { Bed: "Sin cama" }) patient.Location.Bed = ObjectId.GenerateNewId().ToString(); if (string.IsNullOrEmpty(patient.PatientNumber)) //si permite crear pacientes sin patientNumber le asignamos el patient.Id patient.PatientNumber = !_cretatePatientWithoutPatientNumber ? patient.Id.ToString() : string.Empty; await _patientRepository.Update(patient); if (oldPatient != null) await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); _ = SendPatientUpdateBroadcast(patient); } catch (Exception e) { _logger.LogError("error updating patient: {patient} exception: {e.Message}", patient, e.Message); } } /// /// Updates the location of a patient identified by , moving them to the specified /// . Validates the input, resolves the target unit and point of care, updates the /// patient record, creates an audit log, and broadcasts the location change to subscribers. When the previous /// point of care is flagged as recovered, the discharge time is cleared and a new patient broadcast is sent /// instead of a standard location broadcast. If any required entity (patient, old point of care, new unit, /// or new point of care) is not found, the update is aborted and logged. /// /// The identifier of the patient whose location will be updated. /// The new patient location, or null to abort the update. public async Task UpdateLocation(ObjectId id, PatientLocation? location) { try { if (location == null) { _logger.LogError("PointOfCare is null. Not updated location for patient id: {id} ", id); return; } _logger.LogDebug("Update location id: {id} data: {location}", id, location); var patient = await _patientRepository.FindById(id); var auxPatient = await _auditService.DeepCopyAsync(patient); if (patient == null) { _logger.LogError("person is null. Not updated location for patient id: {id} ", id); return; } if (patient.PointOfCareId == null) { _logger.LogError("person pocId or unitId is null. Not updated location for patient id: {id} ", id); return; } var oldPoc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); if (oldPoc == null) { _logger.LogError("person old poc not found {pocId}. Not updated location for patient id: {id} ", patient.PointOfCareId.Value, id); return; } var isRecovered = Enum.TryParse(oldPoc.Bed, out _); //"DELETED".Equals(patient.Location.UnitName); //var oldLocation =new PatientLocation(oldUnit?.Name, oldPoc.Bed, oldPoc.Room); if (isRecovered) patient.DisTime = null; var newUnit = await _unitService.FindByName(location.UnitName); var newPoc = await _pointOfCareService.FindByBedAndUnitId(location.Bed, newUnit?.Id); if (newUnit == null || newPoc == null) { _logger.LogError("person new poc not found {pocId}. Not updated location for patient id: {id} ", patient.PointOfCareId.Value, id); return; } patient.Location = new PatientLocation(newUnit.Name, newPoc.Bed, newPoc.Room); patient.PointOfCareId = newPoc.Id; patient.UnitId = newUnit.Id; await _patientRepository.UpdateOneAsync(patient.Id, patient); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxPatient, patient); if (isRecovered) await SendNewPatientBroadcast(patient); var pocListToNotify = new List { newPoc.Id }; await _pointOfCareService.SetPointOfCareStatus(newPoc.Id, StatusEnum.PointOfCare.InUse); pocListToNotify.Add(oldPoc.Id); _pointOfCareService.CheckNextAdmission(oldPoc.Id); //Prio location to advertise both locations of movement between sections _logger.LogDebug( "SendPatientLocationBroadcast id: {id} location: {location} newSection: {newSection}, oldSection: {oldSection}", id, location, newPoc.Location.ToString(), oldPoc.Location.ToString()); if (!isRecovered) _ = SendPatientLocationBroadcast(id, location, pocListToNotify); if (oldPoc.Id != newPoc.Id) CheckLocationForWsSubscriber(location, id); } catch (Exception ex) { _logger.LogError("error updating location for patient id: {id} exception: {exMessage}", id, ex.Message); } } /// /// Updates the attending doctor for an existing patient, records the change in an audit log, and triggers a broadcast notification about the new attending doctor. /// /// The unique identifier of the patient whose attending doctor is being updated. /// The new attending doctor to assign to the patient. /// Thrown when the patient cannot be found by id before or after the update operation. public async Task UpdateAttendingDoctor(ObjectId id, Person doctor) { var oldPatient = await _patientRepository.FindById(id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); _logger.LogDebug("Update attending doctor id: {id} data: {doctor}", id, doctor); await _patientRepository.UpdateAttendingDoctor(id, doctor); var newPatient = await _patientRepository.FindById(id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, newPatient); _ = SendPatientAttendingDoctorBroadcast(id, doctor); } /// /// Updates the data of an existing patient identified by , optionally updating the patient number when is true. Throws a conflict exception if the patient cannot be found before or after the update, records the change through the audit service, and asynchronously broadcasts the updated patient data. /// /// The unique identifier of the patient to update. /// The patient number associated with the patient. /// The new data to apply to the patient. /// When true, the patient number is also updated; otherwise only the personal data is changed. /// Thrown when the patient cannot be found by either before or after the update operation. public async Task UpdatePatientData(ObjectId id, string patientNumber, Person data, bool updatePatientNumber = true) { var oldPatient = await _patientRepository.FindById(id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); _logger.LogDebug("Update person data id: {id} number: {patientNumber} data: {data}", id, patientNumber, data); await _patientRepository.UpdatePatientData(id, patientNumber, data, updatePatientNumber); var newPatient = await _patientRepository.FindById(id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, newPatient); _ = SendPatientDataBroadcast(id, data); } /// /// Updates the patient data identified by the given id, including an audit log entry capturing the change, and broadcasts the update to subscribers. If the supplied patient has no associated person, only the broadcast is triggered and no repository update is performed. /// /// The ObjectId of the patient to update. /// The patient number associated with the patient. /// The patient entity containing the updated data. /// Indicates whether the patient number should be updated as part of the operation. /// Thrown when the patient cannot be found before or after the update operation. public async Task UpdatePatientData(ObjectId id, string patientNumber, Patient patient, bool updatePatientNumber = true) { _logger.LogDebug("Update person data id: {id} number: {patientNumber} patient: {patient}", id, patientNumber, patient); if (patient.Person != null) { var oldPatient = await _patientRepository.FindById(id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); await _patientRepository.UpdatePatientData(id, patientNumber, patient.Person, updatePatientNumber); var newPatient = await _patientRepository.FindById(id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, newPatient); } _ = SendPatientBroadcast(patient); } /// /// Retrieves a patient by their unique patient identifier, optionally enriching the result with location details such as bed, room, and unit information. /// /// The unique identifier of the patient to retrieve. /// When true, additional lookups are performed to populate the patient's bed, room, and unit information; otherwise only the patient record is returned. /// The matching the specified identifier, or null if no patient is found. public async Task FindByPatientId(string patientId, bool withLocation = false) { var patient = await _patientRepository.FindByPatientId(patientId); if (withLocation && patient != null) { if (patient.PointOfCareId.HasValue) { var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); patient.Bed = poc?.Bed; patient.Room = poc?.Room; } if (patient.UnitId.HasValue) { var unit = await _unitService.FindById(patient.UnitId.Value); patient.UnitString = unit?.Name; } } return patient; } /// /// Retrieves a patient by their unique patient number, optionally enriching the result with location details such as bed, room, and unit name when the flag is enabled. /// /// The unique identifier of the patient to look up. /// When set to true, populates the returned patient with bed and room information from the point of care and the unit name from the unit service, provided the patient has associated location identifiers. /// A containing the matching if found; otherwise, null. public async Task FindByPatientNumber(string patientNumber, bool withLocation = false) { var patient = await _patientRepository.FindByPatientNumber(patientNumber); if (withLocation && patient != null) { if (patient.PointOfCareId.HasValue) { var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); patient.Bed = poc?.Bed; patient.Room = poc?.Room; } if (patient.UnitId.HasValue) { var unit = await _unitService.FindById(patient.UnitId.Value); patient.UnitString = unit?.Name; } } return patient; } /// /// Retrieves an archived patient by their unique patient number from the archive repository. /// Returns null if no archived patient matches the provided patient number. /// /// The unique patient number used to look up the archived patient. /// The archived if found; otherwise, null. public async Task FindByPatientNumberArchived(string patientNumber) { return await _patientArchiveRepository.FindByPatientNumber(patientNumber); } /// /// Archives patients who have not had observations recorded since the specified date, combining /// observation and pump service data to determine the last activity per patient, and also includes /// inactive PoC patients with no recent observations. Patients found in the patient collection are /// archived individually, while patients not found have their orphan observation data archived /// instead. Errors encountered while archiving individual patients or their data are logged, and /// any unexpected failure causes the original exception to be rethrown after logging. /// /// The cutoff date; patients with no observations updated after this date will be archived. public async Task ArchivePatientWithoutObservationsSinceDate(DateTime date) { try { // Observations not updated since date, archive their patients and data var ptimes = (await _observationService.Value.FindAllLastPatientObservationTime()) .Where(o => date > o.Value).ToDictionary(o => o.Key, o => o.Value); (await _pumpService.Value.FindAllLastPatientObservationTime()).Where(o => date > o.Value) .Where(o => !ptimes.ContainsKey(o.Key) || ptimes[o.Key] > o.Value).ToList() .ForEach(o => ptimes[o.Key] = o.Value); //ptimes.ToList().Sort( (o1, o2) => o1.Value.CompareTo(o2.Value)); var foundPatients = new HashSet(); var notFoundPatients = new HashSet(); foreach (var o in ptimes) { var patient = await FindById(o.Key); if (patient != null) foundPatients.Add(patient); else notFoundPatients.Add(o.Key); } var notUpdatedPatients = await _patientRepository.FindPatientsNotUpdatedSince(date); foreach (var notUpdatedPatient in notUpdatedPatients.Where(p => p.IsInActivePoC())) { var obsPatient = await _observationService.Value.FindLastObservations(notUpdatedPatient.Id); var pumpPatient = await _pumpService.Value.FindLastPumpObservations(notUpdatedPatient.Id); if (!foundPatients.Contains(notUpdatedPatient) && obsPatient.Count == 0 && pumpPatient.Count == 0) foundPatients.Add(notUpdatedPatient); } if (foundPatients.Count > 0) { _logger.LogDebug("Patients found for archive: {foundPatientsCount}", foundPatients.Count); foreach (var patient in foundPatients) try { await ArchivePatient(patient); } catch (Exception ex) { _logger.LogError( "ERROR trying to ArchivePatient {patientId} because {exMessage}", patient.Id, ex.Message); } } if (notFoundPatients.Count > 0) { _logger.LogWarning("Patients not found in patient collection: {notFoundPatientsCount}", notFoundPatients.Count); foreach (var patientId in notFoundPatients) { _logger.LogWarning("- Deleting orphan data for patient: {patientId}", patientId); try { await ArchivePatientData(patientId); } catch (Exception ex) { _logger.LogError( "ERROR trying to ArchivePatientData {patientId} because {exMessage}", patientId, ex.Message); } } } } catch (Exception ex) { _logger.LogError( "ERROR trying to retrieve patients without observations in date range because {exMessage}", ex.Message); throw; } } /// /// Archives discharged patients whose discharge time is older than the specified number of hours. /// Only patients with a non-null DisTime earlier than the calculated cutoff date are processed, /// and any error encountered during archiving is logged rather than propagated. /// /// The number of hours that must elapse after a patient's discharge time before they are eligible for archiving. public async Task ArchiveDischargedPatients(int hoursBeforeArchive) { var disBeforeDate = DateTime.Now.AddHours(-hoursBeforeArchive); try { var dischargedPatients = (await _patientRepository.FindDischargedPatients()) .Where(patient => patient.DisTime != null && patient.DisTime.Value < disBeforeDate ).ToList(); _logger.LogDebug( "ArchiveDischargedPatients older than {HoursBeforeArchive} hours ({DisBeforeDate}). Found {Count} patients for archiving", hoursBeforeArchive, disBeforeDate, dischargedPatients.Count); foreach (var patient in dischargedPatients) await ArchivePatient(patient); } catch (Exception ex) { _logger.LogError("Error archiving discharged patients {Message}", ex.Message); } } /// /// Locates a patient by first attempting a lookup using the patient number, then falling back to the patient identifier, and optionally performing a location-based search when explicitly requested or allowed by configuration. /// /// The unique patient identifier used as a secondary lookup criterion when the patient number is not provided or yields no result. /// The patient number used as the primary lookup criterion. /// The patient location used for a location-based lookup; the search only proceeds when both the unit name and the bed are set, and the location is mapped through the POC mapping service before being applied to the resulting patient. /// When true, allows a location-based lookup even if the corresponding configuration option is disabled. /// The matching if found by any of the attempted criteria, or null when no patient can be located. public async Task FindPatient(string? patientId, string? patientNumber, PatientLocation? location, bool findPatientByLocation = false) { Patient? patient = null; if (!string.IsNullOrEmpty(patientNumber)) patient = await FindByPatientNumber(patientNumber, true); if (patient != null) return patient; if (!string.IsNullOrEmpty(patientId)) patient = await FindByPatientId(patientId, true); if (patient != null) return patient; // No buscamos por localización por defecto if (!_findPatientByLocationWithoutId && !findPatientByLocation) return patient; //if location pointOfCare and bed != null if (location is { UnitName: not null, Bed: not null }) { var loc = await _pocMappingService.Map(location); patient = await FindByLocation(loc); if (patient is not null && loc is not null) patient.Location = loc; } return patient; } /// /// Asynchronously retrieves the patient associated with the specified point of care, returning null if an error occurs during the lookup. /// /// The point of care used to locate the associated patient. /// Indicates whether related observations should be included in the result. /// Optional list of observation identifiers used to filter observations when they are included. /// A task that resolves to the matching , or null if the lookup fails. public async Task GetByPointOfCare(PointOfCare item, bool observations = false, List? filterObservations = null) { try { return await _patientRepository.FindByPointOfCareId(item.Id); } catch (Exception ex) { Debug.WriteLine("ERROR: " + ex.Message); return null; } } /// /// Retrieves a patient associated with the specified point of care, optionally returning a locale-translated version when both unit and locale are provided. /// /// The point of care used to look up the patient. /// The unit used to determine the translation; when null, the patient is returned without translation. /// The target locale for translation; when null, the patient is returned without translation. /// The matching , or null if the patient is not found or an error occurs. public async Task GetByPointOfCareAndLocale(PointOfCare item, Unit? unit, LocaleEnum? localeEnum) { try { var patient = await _patientRepository.FindByPointOfCareId(item.Id); if (unit == null || localeEnum == null) return patient; return await _masterListServiceFactory.GetPatientTraslated(unit, localeEnum.Value, patient); } catch (Exception ex) { Debug.WriteLine("ERROR: " + ex.Message); return null; } } /// /// Retrieves a populated with information for the given point of care, including the associated patient when one exists, and optionally the patient's most recent observations. /// /// The point of care used to locate the associated patient and to populate the box unit and bed. /// When true, the most recent observations for the patient are loaded and mapped by name; when false, only patient data is returned. /// Optional list of observation names used to restrict which observations are retrieved; applies only when is true. /// A containing the populated box. The HasPatient flag is set to false if no patient is found for the point of care, and the box's Observations are populated only when requested. public async Task GetBox(PointOfCare poc, bool observations = false, List? filterObservations = null) { Box response = new(); Patient? patient = null; try { patient = await _patientRepository.FindByPointOfCareId(poc.Id); } catch (Exception ex) { Debug.WriteLine("ERROR: " + ex.Message); } response.PointOfCare = poc.UnitName; response.Bed = poc.Bed; response.IsVisible = true; if (patient == null) { response.HasPatient = false; return response; } response.HasPatient = true; response.Patientid = patient.Id; response.Patient = patient; response.AttendingDoctor = patient.AttendingDoctor; if (!observations) return response; var obs = await _observationService.Value.FindLastObservations(patient.Id, 2, filterObservations); obs.ForEach(Action); response.Observations = obs; return response; void Action(PatientObservation o) { _observationService.Value.MapObservationsByName(o); } } /// /// Broadcasts a new patient notification to all subscribers whose registered locations include the patient's point of care. /// /// The patient whose arrival should be broadcast to matching subscribers. /// A completed task once the broadcast has been dispatched. public Task SendNewPatientBroadcast(Patient patient) { var subscribers = new List(); // var section = await _sectionService.FindByPointOfCare(patient.UnitString); // if (section != null) // { // subscribers.AddRange(_subscribersService.GetSubscribers().Where(s => // (s.SubscriptionType == SubscriptionType.Box && s.Box == patient.Bed && s.Section == section.Id) || // (s.SubscriptionType == SubscriptionType.Section && s.Section == section.Id)).ToList()); // } subscribers.AddRange(_subscribersService.GetSubscribers().Where(s => !s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c => c == patient.PointOfCareId )).ToList()); foreach (var subscriber in subscribers) _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.NewPatient, patient); return Task.CompletedTask; } /// /// Broadcasts a patient update to all WebSocket subscribers associated with the patient's point of care location, grouping subscribers by locale and dispatching a translated payload per subscriber. /// If the patient has no point of care id, the update is skipped and an error is logged. /// /// The patient whose update will be broadcast; its point of care id is used to filter subscribers and its unit is used to resolve translations. public async Task SendPatientUpdateBroadcast(Patient patient) { if (!patient.PointOfCareId.HasValue) { _logger.LogError("patient pointOfCareId is null. Not sent patient id: {id} ", patient.Id); return; } var subscribersGroup = _subscribersService.GetSubscribers().Where(s => s.LocationIds.Any(c => c == patient.PointOfCareId.Value)).GroupBy(h => h.Locale); var unit = await _unitService.FindById(patient.UnitId); foreach (var group in subscribersGroup) { var locale = group.Key ?? LocaleEnum.Default; IEnumerable subscribers = group; foreach (var subscriber in subscribers) { var patientWithLocale = await _masterListServiceFactory.GetPatientTraslated(unit, locale, patient); _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.UpdatePatient, patientWithLocale); } } } /// /// Asynchronously saves the specified API request by delegating the operation to a background task. /// /// The API request to be saved. /// A that represents the asynchronous save operation. public Task SaveRequestAsync(ApiRequest apiRequest) { return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); } /// /// Processes an inbound API request, mapping locations, resolving the unit configuration, and applying the appropriate patient ADT (admit, discharge, transfer) workflow based on the request type. Handles patient admit, transfer, discharge, registration, update, cancellation, recovery, merge, identifier list, and patient number change events, as well as ICCA synchronization, delegating to patient, observation, diagnosis, and appointment services as needed. /// /// The API request containing the ADT type, patient identifiers, locations, and related clinical data to be processed. /// Thrown when both the patient number and location unit name are missing for non-ICCA requests, or when the request type is not a valid patient ADT type. public async Task SaveRequest(ApiRequest apiRequest) { if (string.IsNullOrEmpty(apiRequest.PatientNumber) && string.IsNullOrEmpty(apiRequest.Location?.UnitName) && apiRequest.Type != "ICCA") { _logger.LogDebug("person and PointOfCare are nulls"); throw new ApiRequestException("person and PointOfCare are nulls"); } _logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}", apiRequest.PatientNumber, apiRequest.Location); if (apiRequest.Location != null) { var originalPointOfCare = apiRequest.Location; apiRequest.Location = await _pocMappingService.Map(originalPointOfCare); if (apiRequest.Location == null) { _logger.LogWarning("PointOfCare not found in Mapping list: {originalPointOfCare}", originalPointOfCare.ToString()); return; } } if (apiRequest.OldLocation != null) { var originalOldPointOfCare = apiRequest.OldLocation; apiRequest.OldLocation = await _pocMappingService.Map(originalOldPointOfCare); } _logger.LogDebug("RequestType: {apiRequesttype}", apiRequest.Type); var unitConfig = await _unitService.FindByName(apiRequest.Location?.UnitName); Unit? oldUnitConfig = null; if (apiRequest.OldLocation != null) { var originalOldLocation = await _pocMappingService.Map(apiRequest.OldLocation); if (originalOldLocation != null) oldUnitConfig = await _unitService.FindByName(apiRequest.OldLocation?.UnitName); } if (apiRequest.Type != "ICCA") { if (apiRequest.PatientNumber != null && unitConfig == null && oldUnitConfig == null) { var pat = await FindByPatientNumber(apiRequest.PatientNumber); if (pat is { UnitId: not null }) unitConfig = await _unitService.FindById(pat.UnitId); } if (!Hl7Utils.ManageAutoAdt(unitConfig, oldUnitConfig, _logger, "ADT")) return; } //TODO Crear para el adt5 try { Patient? patient = null; switch (apiRequest.Type) { //* ADT EVENTS case "ADT_A01": case "ADT_A05": { //* A01 Admit/visit notification //* A05 Pre-admit a patient _logger.LogDebug("NEW ADT {apiRequestType} PATIENT NUMBER: {apiRequestPatientNumber}", apiRequest.Type, apiRequest.PatientNumber ?? "NULL"); patient = await ProcessAdtPatientAdmit(apiRequest); break; } case "ADT_A02": { //* A02 Transfer a patient if (apiRequest.Location != null) apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, apiRequest.OldLocation); await ProcessAdtMovePatient(apiRequest, patient); break; } case "ADT_A03": { // * A03 Discharge/end visit // *Ya no archiva, archiva el job del Scheduler service pasado el tiempo establecido por el hospital en SinceDischargeTimeToArchive // se le mueve a una cama temporal. _logger.LogDebug("NEW ADT {apiRequestType} PATIENT NUMBER: {apiRequestPatientNumber}", apiRequest.Type, apiRequest.PatientNumber); if (apiRequest.Location != null) apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, apiRequest.Location); if (patient != null) await RemovePatientAndSendToVirtualPoc(apiRequest, patient, VirtualPointOfCare.Deleted); else _logger.LogDebug("ADT A03 not found patient number {number}", apiRequest.PatientNumber); break; } case "ADT_A04": { // * A04 Register a patient if (apiRequest.Location != null) apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, apiRequest.Location); if (patient == null) patient = await CreatePatientFromRequest(apiRequest); else _logger.LogDebug("ADT A04 not found patient number {number}", apiRequest.PatientNumber); break; } case "ADT_A08": { //* A08 Update patient information patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, apiRequest.Location); if (patient != null) { await UpdatePatientFromRequest(patient, apiRequest, true); await Update(patient); } else { _logger.LogDebug("ADT A08 not found by patient number {number}", apiRequest.PatientNumber); if (_createPatientWithAdtA08) { _logger.LogDebug( "ProcessAdtPatientAdmit ADT_A08 Because CreatePatientWithADT_A08 is true {apiRequestpatientNumber}", apiRequest.PatientNumber); await ProcessAdtPatientAdmit(apiRequest); } } break; } // A11 Cancel admit/ visit notification/ // Remove patient case "ADT_A11": { var patientNumber = apiRequest.PatientNumber; if (patientNumber == null) { _logger.LogError("ADT A11 Without patient number {number}", apiRequest.PatientNumber); } else { patient = await _patientRepository.FindByPatientNumber(patientNumber); if (patient != null) await RemovePatientAndSendToVirtualPoc(apiRequest, patient, VirtualPointOfCare.Cancelled); else _logger.LogDebug("ADT A11 not found patient number {number}", apiRequest.PatientNumber); } break; } //* A12 Cancel patient transfer. //* Se mueve el patientNumber que viene a la cama que indica, si hay ya un paciente en esa cama ese paciente se mueve a una temporal case "ADT_A12": { var patientNumber = apiRequest.PatientNumber; if (apiRequest.Location != null) apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); if (patientNumber == null) { _logger.LogError("ADT A12 Without patient number {number}", apiRequest.PatientNumber); } else { patient = await _patientRepository.FindByPatientNumber(patientNumber); await ProcessAdtMovePatient(apiRequest, patient); } break; } //* A13 Cancel discharge/end visit //* Recuperamos el paciente, seteamos ArchiveDate a null.Lo movemos a la cama que nos indique. case "ADT_A13": { if (apiRequest.Location != null) apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); var patientNumber = apiRequest.PatientNumber; if (patientNumber == null) { _logger.LogError("ADT A13 Without patient number {number}", apiRequest.PatientNumber); } else { patient = await _patientRepository.FindByPatientNumber(patientNumber); if (patient != null) { if ("".Equals(apiRequest.Location?.Bed?.Replace("\"\"", ""))) { var unit = await _unitService.FindById(patient.UnitId); apiRequest.Location = new PatientLocation(unit?.Name, VirtualPointOfCare.Recovered.ToString(), VirtualPointOfCare.Recovered.ToString()); } await ProcessAdtMovePatient(apiRequest, patient); } else { _logger.LogDebug("ADT A13 not found patient number {number}", apiRequest.PatientNumber); } } break; } case "ADT_A31": { //* ADT_A31 - Update Person Information patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, apiRequest.Location); if (patient != null) { await UpdatePatientFromRequest(patient, apiRequest, true); await Update(patient); } else { _logger.LogDebug("ADT A31 not found patient number {number}", apiRequest.PatientNumber); } break; } //ADT_A39 - Merge Person - Patient Id //ADT_A40 - Merge patient - Patient Identifier List case "ADT_A39": case "ADT_A40": { if (apiRequest.Location != null) apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); var patientNumber = apiRequest.PatientNumber; if (patientNumber == null) { _logger.LogError("ADT A39 Without patient number {number}", apiRequest.PatientNumber); } else { patient = await _patientRepository.FindByPatientNumber(patientNumber); if (patient != null && !string.IsNullOrEmpty(apiRequest.OldPatientNumber)) await MergePatient(patient, apiRequest.OldPatientNumber); else _logger.LogDebug("ADT A39 not found patient number {number}", apiRequest.PatientNumber); } break; } //ADT A_47: actualización de lista de ids case "ADT_A47": { //Para el A47 no envía necesariamente el el location //Buscamos el paciente por patientNumber if (!string.IsNullOrEmpty(apiRequest.PatientNumber)) patient = await _patientRepository.FindByPatientNumber(apiRequest.PatientNumber); if (patient == null) { _logger.LogError("{type}: Patient not found in apiRequest: {apirequest}", apiRequest.Type, apiRequest.ToString()); break; } _logger.LogDebug("Updating patient from request. Patient: {patient} ", patient.ToString()); UpdatePatientFromRequestToIdList(patient, apiRequest); _logger.LogDebug("Updating patient. Patient: {patient}", patient.ToString()); await Update(patient); _logger.LogDebug("updated patient: {patientId}", patient.Id); break; } //ADT A_44: actualización de patient number case "ADT_A44": { //Cambiar el patientNumber var oldPatientNumber = apiRequest.OldPatientNumber; if (oldPatientNumber == null) { if (apiRequest.Location == null) { //A44 no tiene ni oldPatientNumber ni ubicación //No se procesa _logger.LogError("{type} Without old Patient Number and location ", apiRequest.Type); break; } //Buscamos al paciente por su localización patient = await _patientRepository.FindByLocation(apiRequest.Location); } else { patient = await _patientRepository.FindByPatientNumber(oldPatientNumber); } if (patient == null) { _logger.LogDebug("ADT A44 not found patient"); break; } _logger.LogDebug("Updating patient from request. Patient: {patient} ", patient.ToString()); await UpdatePatientFromRequest(patient, apiRequest); _logger.LogDebug("Updating patient. Patient: {patient}", patient.ToString()); await Update(patient); _logger.LogDebug("updated patient: {patientId}", patient.Id); break; } //* A06 Change an outpatient to an inpatient //* A07 Change an inpatient to an outpatient //* A08 Update patient information //* A09 Patient departing - tracking //* A10 Patient arriving - tracking //* A12 Cancel transfer //* A14 Pending admit //* A15 Pending transfer //* A16 Pending discharge //* A17 Swap patients //* A18 Merge patient information //* A19 QRY/ADR - Patient query //* A20 Bed status update case "ADT_A06": case "ADT_A07": case "ADT_A09": case "ADT_A10": case "ADT_A14": case "ADT_A15": case "ADT_A16": case "ADT_A17": case "ADT_A18": case "ADT_A19": case "ADT_A20": _logger.LogWarning("{apiRequesttype} is not valid for Patients", apiRequest.Type); break; //For icca db sync case "ICCA": _ = ProcessIccaSync(apiRequest); break; default: throw new ApiRequestException("ApiRequest type " + apiRequest.Type + " is not valid for Patients"); } if (patient != null) { if (apiRequest.Observations != null && apiRequest.Observations.Any()) _observationService.Value.ProcessObservations(apiRequest.Observations, patient, apiRequest.MessageTime, apiRequest.ObservationData); if (apiRequest.Diagnosis != null && apiRequest.Diagnosis.Any()) _ = _diagnosisService.ProcessDiagnosis(apiRequest.Diagnosis, patient, apiRequest.MessageTime); if (apiRequest.Appointments != null && apiRequest.Appointments.Any()) _ = _appointmentService.ProcessApiRequest(apiRequest, patient); } } catch (Exception ex) { _logger.LogError("ERROR saving patient: {exMessage} trace: {exStackTrace}", ex.Message, ex.StackTrace); } } /// /// Creates a new patient from the provided API request, populates its fields, persists it to the repository, and records an audit log entry. /// /// The API request containing the data used to initialize the new patient. /// When true, location information from the request is ignored during the patient update. /// The newly created patient. public async Task CreatePatientFromRequest(ApiRequest apiRequest, bool ignoreLocation = false) { var patient = new Patient { Id = ObjectId.GenerateNewId(), CreationDate = DateTime.UtcNow }; await UpdatePatientFromRequest(patient, apiRequest, ignoreLocation); await _patientRepository.InsertOneAsync(patient); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, patient); return patient; } /// /// Finds or creates a based on the contents of an , handling ORU-style messages (AlarisPump, ORU_R01, ORU_R42, ORU_R40). Maps the incoming point of care through the mapping service, searches by patient number or location, and may create a new patient, push an existing one, or update an existing patient's location, attending doctor, and personal data according to configuration flags. /// /// The incoming API request containing the patient identifier, location, facility, request type, and optional person/doctor information used to resolve or create the patient. /// A that resolves to the resolved or newly created , or null if the point of care cannot be mapped, the request is not handled, no matching patient is found and creation is disabled, or an error occurs while processing the request. public async Task FindPatientByApiRequest(ApiRequest apiRequest) { Patient? patient = null; if (apiRequest.Location != null) { var originalPointOfCare = apiRequest.Location; apiRequest.Location = await _pocMappingService.Map(originalPointOfCare); if (apiRequest.Location == null) { _logger.LogDebug("PointOfCare not found in Mapping list: {OriginalPointOfCare} ", originalPointOfCare); return patient; } } _logger.LogDebug("RequestType: {apiRequest}", apiRequest.Type); try { switch (apiRequest.Type) { //* ORU_R01 - Unsolicited transmission of an observation message //* ORU_R40 - Unsolicited transmission of an alert observation message //* AÑADIDO. SI EL FACILITY DEL API REQUEST NO ES EL DE ICCA BUSCA SOLO POR POINT OF CARE Y BED NO POR PATIENT ID. case "AlarisPump": case "ORU_R01": case "ORU_R42": case "ORU_R40": _logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}", apiRequest.PatientNumber, apiRequest.Location); // SEARCH PATIENT || SendingFacility.Contains(admRequest.facility) if (!string.IsNullOrWhiteSpace(apiRequest.PatientNumber) && !string.IsNullOrWhiteSpace(apiRequest.Facility) && (apiRequest.Facility.Equals(_iccaFacility) || apiRequest.Facility.Equals("obs"))) { // SEARCH PATIENT BY PATIENT NUMBER _logger.LogDebug("SEARCH PATIENT BY PATIENT NUMBER"); patient = await FindByPatientNumber(apiRequest.PatientNumber, true); if (patient == null) // PATIENT NOT FOUND OR ARCHIVED _logger.LogDebug("PATIENT NOT FOUND OR ARCHIVED"); } if (patient == null) { _logger.LogDebug("PATIENT {apiRequestpatient}", apiRequest.Patient); _logger.LogDebug("PATIENT NUMBER {apiRequestpatientNumber}", apiRequest.PatientNumber); _logger.LogDebug("PATIENT LOCATION {apiRequestlocation} EMPTY {apiRequestlocationIsEmpty}", apiRequest.Location, apiRequest.Location?.ToStr() ?? "null"); if (!string.IsNullOrEmpty(apiRequest.PatientNumber) && !string.IsNullOrWhiteSpace(apiRequest.Facility) && (apiRequest.Facility.Equals(_iccaFacility) || _sendingFacility.Contains(apiRequest .Facility))) { _logger.LogDebug("PATIENT LOCATION Find person by PatientNumber"); var patientExists = await _patientRepository.FindByPatientNumber(apiRequest.PatientNumber); if (patientExists != null) { patient = patientExists; } else { _logger.LogDebug("PATIENT LOCATION IS EMPTY.Not found Patient by PatientNumber"); if (_createPatientWithOru) { if (apiRequest.Location == null || apiRequest.Location.IsEmpty()) { _logger.LogDebug( "PATIENT LOCATION IS EMPTY. Generate temporal bed because createPatientWithoutLocation is TRUE"); apiRequest.Location = new PatientLocation( VirtualPointOfCare.Unknown.ToString(), VirtualPointOfCare.Unknown.ToString()); } if (!apiRequest.Location.IsEmpty() && string.IsNullOrEmpty(apiRequest.Location.Bed)) apiRequest.Location.Bed = VirtualPointOfCare.Unknown.ToString(); patient = new Patient { Id = ObjectId.GenerateNewId(), PatientNumber = apiRequest.PatientNumber, Person = apiRequest.Patient, AttendingDoctor = apiRequest.AttendingDoctor, Location = apiRequest.Location }; var patientLocation = await FindPatient(null, null, apiRequest.Location, true); if (patientLocation != null) { _logger.LogDebug( "PATIENT LOCATION IS OCUPED, Created UNKNOW location {patient}.", patient); var pocUnknown = await _pointOfCareService.FindByBedAndUnitId( VirtualPointOfCare.Unknown.ToString(), patientLocation.UnitId); patient.UnitId = patientLocation.UnitId; patient.PointOfCareId = pocUnknown?.Id; var un = await _unitService.FindById(patientLocation.UnitId); patient.Location = new PatientLocation( un?.Name ?? VirtualPointOfCare.Unknown.ToString(), pocUnknown?.Bed ?? VirtualPointOfCare.Unknown.ToString()); } else { var unit = await _unitService.FindByName(apiRequest.Location.UnitName); var poc = await _pointOfCareService.FindByBedAndUnitId(apiRequest.Location.Bed, unit?.Id); if (unit != null && poc != null) { patient.UnitId = poc.UnitId; patient.PointOfCareId = poc.Id; patient.Location = new PatientLocation( unit.Name, poc.Bed); } else { patient.Location = new PatientLocation( VirtualPointOfCare.Unknown.ToString(), VirtualPointOfCare.Unknown.ToString()); } } _logger.LogDebug( "CREATE PATIENT BECAUSE createPatientWithORU CONFIG IS TRUE {patient}", patient); await Insert(patient); } } } else { patient = await FindPatient(null, apiRequest.PatientNumber, apiRequest.Location, true); if (!string.IsNullOrWhiteSpace(apiRequest.PatientNumber) || (Enum.TryParse(apiRequest.Location?.Bed, out var dataPoC) && dataPoC == VirtualPointOfCare.UnitData)) { if (patient == null) { if (_createPatientWithLocation) { _logger.LogDebug( "PATIENT LOCATION NOT IS EMPTY. Generate patient because createPatientWithLocation is TRUE"); patient = new Patient { Id = ObjectId.GenerateNewId() }; await UpdatePatientFromRequest(patient, apiRequest); _logger.LogDebug( "CREATE PATIENT BECAUSE CreatePatientWithLocation CONFIG IS TRUE {patient}", patient); await Insert(patient); } } else { if (_pushPatientWithOru && apiRequest.PatientNumber != patient.PatientNumber) { var admTime = patient.AdmTime; var msgTime = apiRequest.MessageTime; //Si la fecha de admisión es anterior < a la fecha del mensaje (con un patient number distinto) // estamos recibiendo orus para un paciente que no tenemos aun insertado en esa localización entonces lo creamos desplazando al que está actualmente if (admTime < msgTime) { _logger.LogDebug( "PATIENT LOCATION IS NOT EMPTY AND PATIENT NUMBER IS NOT THE SAME. Generate patient because CreatePatientWithORU is TRUE AND ADMTIME IS OLDER THAN MSGTIME AND PUSH OLD PATIENT {patient}", patient); var unit = await _unitService.FindById(patient.UnitId); await UpdateLocation(patient.Id, new PatientLocation(unit?.Name, VirtualPointOfCare.Pushed.ToString())); patient = new Patient { Id = ObjectId.GenerateNewId() }; await UpdatePatientFromRequest(patient, apiRequest); _logger.LogDebug( "CREATE PATIENT BECAUSE CreatePatientWithORU CONFIG IS TRUE {patient}", patient); await Insert(patient); } } } } } } else { // PATIENT FOUND BY PATIENT NUMBER _logger.LogDebug("PATIENT FOUND BY PATIENT NUMBER"); if (apiRequest.Location is { Bed: not null } && !apiRequest.Location.Equals(patient.Location)) { //Al cambiar de localizacion set active nueva localizacion set innactive old localizacion // PATIENT LOCATION HAS CHANGED _logger.LogDebug("PATIENT LOCATION HAS CHANGED"); if (_updatePatientLocationWithOru) { // UPDATE PATIENT LOCATION BECAUSE updatePatientLocationWithORU CONFIG IS TRUE _logger.LogDebug( "UPDATE PATIENT LOCATION BECAUSE updatePatientLocationWithORU CONFIG IS TRUE"); patient.Location = apiRequest.Location; var patientExistsInLocation = await FindByLocation(apiRequest.Location); if (patientExistsInLocation != null && patient.PatientNumber != patientExistsInLocation.PatientNumber) { var unit = await _unitService.FindById(patientExistsInLocation.UnitId); var newLocation = new PatientLocation(unit?.Name, VirtualPointOfCare.Unknown.ToString()); await UpdateLocation(patientExistsInLocation.Id, newLocation); } // ONLY ADTS archive if (patientExistsInLocation != null) patientService.ArchivePatient(patientExistsInLocation); await ProcessUpdatePatientLocationWithOru(patient, apiRequest); } } } if (_updatePatientDataWithOru && patient != null) { if (apiRequest.AttendingDoctor != null && !apiRequest.AttendingDoctor.Equals(patient.AttendingDoctor)) { // PATIENT DOCTOR CHANGED _logger.LogDebug("PATIENT FOUND BY LOCATION"); patient.AttendingDoctor = apiRequest.AttendingDoctor; await UpdateAttendingDoctor(patient.Id, apiRequest.AttendingDoctor); } if (apiRequest.Patient != null && !apiRequest.Patient.Equals(patient.Person)) { // PATIENT DATA CHANGED _logger.LogDebug("PATIENT DATA CHANGED"); if (apiRequest.Patient?.BirthDate != null) { patient.Person ??= new Person(); patient.Person.BirthDate = apiRequest.Patient.BirthDate; } if (apiRequest.Patient?.Language != null) { patient.Person ??= new Person(); patient.Person.Language = apiRequest.Patient.Language; } if (apiRequest.Patient?.FirstName != null) { patient.Person ??= new Person(); patient.Person.FirstName = apiRequest.Patient.FirstName; } if (apiRequest.Patient?.SecondName != null) { patient.Person ??= new Person(); patient.Person.SecondName = apiRequest.Patient.SecondName; } if (apiRequest.Patient?.LastName != null) { patient.Person ??= new Person(); patient.Person.LastName = apiRequest.Patient.LastName; } if (apiRequest.Patient?.Ids is { Count: > 0 }) { patient.Person ??= new Person(); patient.Person.SetIds(apiRequest.Patient.Ids, apiRequest.MessageTime); } //bool updatePatientNumber = false; //patient.patient = admRequest.patient; //Solo actualizamos patientNumber de paciente si es de ICCA el mensaje. En central puede haber patient numbers incorrectos. if (apiRequest.PatientNumber is { Length: > 0 } && !string.IsNullOrWhiteSpace(apiRequest.Facility) && apiRequest.Facility.Equals(_iccaFacility)) patient.PatientNumber = apiRequest.PatientNumber; //UpdatePatientData(patient.id, patient.patientNumber, patient.patient); if (patient.PatientNumber != null) await UpdatePatientData(patient.Id, patient.PatientNumber, patient); } } break; } } catch (Exception ex) { _logger.LogError("ERROR SAVING REQUEST: Exception: {exMessage} ", ex.Message); return null; } return patient; } /// /// Retrieves all patients, optionally enriching each patient with location details (bed, room, and unit name) resolved from the associated point of care and unit records. /// /// When true, populates each patient's , , and by looking up the related point of care and unit; when false, returns the patients without performing those lookups. /// A task containing the list of patients, with location fields populated when is true and the corresponding identifiers are present. public async Task> FindAll(bool withLocation = false) { var patients = await _patientRepository.FindAll(); if (!withLocation) return patients; foreach (var patient in patients) { if (patient.PointOfCareId.HasValue) { var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); patient.Bed = poc?.Bed; patient.Room = poc?.Room; } if (patient.UnitId.HasValue) { var unit = await _unitService.FindById(patient.UnitId); patient.UnitString = unit?.Name; } } return patients; } /// /// Asynchronously retrieves a list of inactive patients of care (PoC) by delegating to the patient repository. /// /// A task representing the asynchronous operation, containing a list of inactive records. public async Task> FindInActivePoC() { return await _patientRepository.FindInActivePoC(); } /// /// Retrieves a list of patients currently in active Point of Care (PoC). /// /// A task that represents the asynchronous operation, containing the list of patients in active PoC. public async Task> FindInInactivePoC() { return await _patientRepository.FindInInactivePoC(); } /// /// Retrieves a paginated list of patients based on the specified filter, returning both the page data and the total document count. /// /// The pagination filter containing the page number and page size used to calculate the skip and limit for the query. /// A task that represents the asynchronous operation. The task result contains a with the patients for the requested page, along with the total count and pagination metadata. public async Task> GetPaginatedPatients(PaginationFilter filter) { var result = _patientRepository.GetPaginatedPatients(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); } /// /// Asynchronously retrieves a associated with the specified unit and point of care identifier. /// Returns if no matching patient is found. /// /// The identifier of the unit to search by. /// The identifier of the point of care to search by. /// A if a match is found; otherwise, . public async Task FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare) { return await _patientRepository.FindByUnitAndPocId(unit, pointOfCare); } /// /// Asynchronously retrieves the list of patients associated with the specified point of care by delegating to the patient repository. /// /// The point of care identifier used to filter patients. /// A task that represents the asynchronous operation, containing the list of entities matching the specified point of care. public async Task> FindByPointOfCare(string pointOfCare) { return await _patientRepository.FindByPointOfCare(pointOfCare); } /// /// Merges the data of an existing patient identified by into the provided , /// reassigning related records across multiple services and removing the old patient record. /// If the old patient is not found, the operation is skipped with a debug log; if the old patient has a point of care, the target patient is updated from the merge, a delete broadcast is sent, and the next admission for the old point of care is checked. /// /// The target patient that will absorb the old patient's data. /// The patient number of the patient to be merged into . /// A task representing the asynchronous merge operation. public async Task MergePatient(Patient patient, string oldPatientNumber) { var oldLocation = patient.PointOfCareId; // sectionService.FindByPatient(patient.id); var oldPatient = await FindByPatientNumber(oldPatientNumber); if (oldPatient == null) { _logger.LogDebug( "the patient has not been merged: {oldPatientNumber} in patient {patientpatientId} , old patient not found ", oldPatientNumber, patient.PatientId); return; } var oldId = oldPatient.Id; _logger.LogDebug("merge patient action: {oldPatientNumber} in {patientpatientId}", oldPatientNumber, patient.PatientId); //TODO: TO CHECK _ = _observationService.Value.UpdateManyObjectId("patientid", patient.Id, oldId); _ = _treatmentService.Value.UpdateManyObjectId("patientid", patient.Id, oldId); _ = _diagnosisService.UpdateManyObjectId("patientid", patient.Id, oldId); _ = _appointmentService.UpdateManyObjectId("patientid", patient.Id, oldId); _ = _pumpService.Value.UpdateManyObjectId("patientid", patient.Id, oldId); _ = _recordingAlertService.UpdateManyObjectId("patientid", patient.Id, oldId); _ = _patientCarePlanService.UpdateManyObjectId("patientId", patient.Id, oldId); _ = _patientRepository.Delete(oldId); if (oldLocation.HasValue) { await UpdatePatientFromMerge(patient, oldPatient); await SendDeletePatientBroadcast(patient.Id, oldLocation.Value); _pointOfCareService.CheckNextAdmission(oldPatient.PointOfCareId); } } /// /// Performs the discharge workflow for inactive patients by archiving discharged patients, patients not updated while in an inactive point of care, and patients without observations since the specified date. Manages a global flag indicating that the inactive patient check is in progress for the duration of the operation and logs any errors encountered. /// /// The cutoff date used to archive patients who have not had any observations since this time. /// The number of hours of inactivity used to determine which discharged and inactive point-of-care patients should be archived. public async Task DischargeInactivePatients(DateTime sinceDate, int hoursBeforeArchive) { try { GlobalData.AddData("isCheckingInactivePatients", true); await ArchiveDischargedPatients(hoursBeforeArchive); await ArchiveNotUpdatedPatientsInInactivePoCSince(hoursBeforeArchive); await ArchivePatientWithoutObservationsSinceDate(sinceDate); } catch (Exception ex) { _logger.LogError("Error discharging inactive patients, error: {exMessage}, trace: {exStackTrace}", ex.Message, ex.StackTrace); } GlobalData.AddData("isCheckingInactivePatients", false); } /// /// Updates an existing patient record in the repository and creates an audit log capturing both the previous and updated states. /// /// The patient object containing the updated information, identified by its Id. /// The updated patient, or null if no matching patient was found or the update did not produce a result. public async Task UpdateOne(Patient updatedPatient) { var oldPatient = await _patientRepository.FindById(updatedPatient.Id); var patient = await _patientRepository.UpdateOne(updatedPatient); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); return patient; } /// /// Updates the altable option of a patient, creating or removing the related discharge record when the altable type changes to or from "NotAltable". /// /// The identifier of the patient whose altable option will be updated. /// The new altable option to assign to the patient. /// The user performing the operation, used to attribute the generated nurse observation. /// The updated , or null if no patient was found with the specified id. public async Task UpdatePatientAltable(ObjectId patientId, OptionList altable, User? user) { var patient = await _patientRepository.FindByPatientId(patientId); var oldPatient = await _auditService.DeepCopyAsync(patient); if (patient == null) { _logger.LogError("Patient id: {id} not found", patientId); return null; } var aux = patient.Altable; patient.Altable = altable; await _patientRepository.Update(patient); if (oldPatient != null) await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); await SendPatientBroadcast(patient); await GenerateNurseObsAndInsert(_listSettings.AltableOptionList.ManualObservationName!, [altable], patient.Id, user); //si son iguales no actualiza if (altable.Name.Equals(aux?.Name)) return patient; if (!patient.PointOfCareId.HasValue) return patient; var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); //Si antes era NotAltable y cambia se crea un registro en discharge if (aux?.OptionType is null or "NotAltable") { var discharge = await _dischargeService.GetDischargeByPatientId(patient.Id); var unit = await _unitService.FindById(patient.UnitId); if (discharge == null) { var dischargeToInsert = new Discharge { DischargeDate = DateTime.UtcNow, Patient = patient, PatientId = patientId, PatientLocation = new PatientLocation(unit?.Name, poc?.Bed, poc?.Room), PointOfCareId = poc?.Id, UnitId = poc?.UnitId }; await _dischargeService.InsertDischarge(dischargeToInsert); } else { discharge.PatientId = patientId; discharge.Patient = patient; discharge.PatientLocation = new PatientLocation(unit?.Name, poc?.Bed, poc?.Room); discharge.PointOfCareId = poc?.Id; discharge.UnitId = poc?.UnitId; await _dischargeService.UpdateDischargeAsync(discharge); } } //Si pasa a ser NotAltable borramos registro en discharge if ("NotAltable".Equals(altable.OptionType)) { var discharge = await _dischargeService.GetDischargeByPatientId(patient.Id); if (discharge != null) await _dischargeService.DeleteDischargeAsync(discharge); } return patient; } /// /// Delete discharge and archive patient by id. /// /// /// /// public async Task ExitPatientById(ObjectId id, bool archivePatient = true) { try { var patient = await FindById(id); if (patient == null) { _logger.LogError("Patient not found by id: {id}", id); return; } var discharge = await _dischargeService.GetDischargeByPatientId(patient.Id); if (discharge != null) { await _dischargeService.DeleteDischargeAsync(discharge); _logger.LogInformation("Discharge deleted. {discharge}", discharge); } if (archivePatient) await ArchivePatient(patient); _logger.LogInformation("ExitPatientById: {id}", id); _pointOfCareService.CheckNextAdmission(patient.PointOfCareId); } catch (Exception ex) { _logger.LogError("Exception exiting patient. Exception:{ex}", ex); throw; } } /// /// Updates a specific master list section of a patient record based on the provided list type, generating associated nurse care plans or observations where applicable. Returns null if the patient cannot be found or if an exception occurs during the update. /// /// The unique identifier of the patient whose master list will be updated. /// The type of master list to update, which determines which patient property is modified and whether care plans or observations are generated. /// The new list of options to assign to the patient for the specified master list type. /// The user performing the update, used for audit purposes and observation generation. /// An optional list of care plan options used when generating care plans for Treatment, Procedure, and Test list types. /// The updated if the operation succeeds; otherwise, null if the patient is not found or an exception is caught. public async Task UpdatePatientMasterList(ObjectId patientId, MasterListType typeName, List updatedOptions, User? user, List? carePlanLog) { try { var patient = await FindById(patientId); if (patient == null) { _logger.LogError("Error updating Patient Master List. Patient not found by Id:{id} ", patientId); return null; } switch (typeName) { case MasterListType.TreatmentList: await GenerateNurseCarePlanAndInsert(MasterListType.TreatmentList, carePlanLog, patient, user); patient.Treatment = updatedOptions; break; case MasterListType.AllergyList: patient.Allergies = updatedOptions; await GenerateNurseObsAndInsert(_listSettings.AllergyList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.ProcedureList: await GenerateNurseCarePlanAndInsert(MasterListType.ProcedureList, carePlanLog, patient, user); patient.Procedures = updatedOptions; break; case MasterListType.TestList: await GenerateNurseCarePlanAndInsert(MasterListType.TestList, carePlanLog, patient, user); patient.Tests = updatedOptions; break; case MasterListType.DoctorList: patient.Doctors = updatedOptions; await GenerateNurseObsAndInsert(_listSettings.DoctorList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.LanguageBarrierList: patient.LanguageBarrier = updatedOptions; await GenerateNurseObsAndInsert(_listSettings.LanguageBarrierList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.MobilityOptionList: patient.Mobility = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.MobilityOptionList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.OriginList: patient.Origin = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.OriginList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.PatientStatusList: patient.PatientStatus = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.PatientStatusList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.TherapeuticCeilingList: patient.TherapeuticCeiling = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.TherapeuticCeilingList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.VisitOptionList: patient.Visits = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.VisitOptionList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.AccessControlList: patient.AccessControl = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.AccessControlList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.AltableOptionList: patient.Altable = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.AltableOptionList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.DiagnosisList: patient.Diagnosis = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.DiagnosisList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.DischargeStatusList: patient.DischargeStatus = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.DischargeStatusList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.InsulationList: patient.Insulation = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.InsulationList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.PassiveSittingList: patient.PassiveSitting = updatedOptions.FirstOrDefault(); await GenerateNurseObsAndInsert(_listSettings.PassiveSittingList.ManualObservationName!, updatedOptions, patient.Id, user); break; case MasterListType.GenericList: await GenerateNurseObsAndInsert(_listSettings.GenericList.ManualObservationName!, updatedOptions, patient.Id, user); break; default: await GenerateNurseObsAndInsert(updatedOptions.FirstOrDefault()?.Name ?? "UNKNOWNOBS", updatedOptions, patient.Id, user); break; } await Update(patient); return patient; } catch (Exception ex) { _logger.LogError("Exception updating patient id:{id}. Data: {type} . Exception: {ex}", patientId, typeName, ex); return null; } } /// /// Generates a nurse care plan for the specified patient by comparing the provided options against the patient's existing procedures, tests, or treatments, determines the corresponding CRUD action (Create, Update, Delete, or Archive), and persists the resulting patient care plan record(s). /// /// The type of care plan to process, selecting between procedures, tests, or treatments. /// The list of options to evaluate against the patient's current items; may be null. /// The patient whose care plan is being generated. /// The user performing the action; may be null. public async Task GenerateNurseCarePlanAndInsert(MasterListType carePlanType, List? options, Patient patient, User? user) { OptionList? addedItems = null; OptionList? removedItems = null; OptionList? updatedItems = null; OptionList? option = null; switch (carePlanType) { case MasterListType.ProcedureList: var patientProcedures = patient.Procedures ?? []; // Encuentra elementos agregados: están en optionsList pero no en patientProcedures addedItems = options?.FirstOrDefault(opt => patientProcedures.All(proc => proc.Name != opt.Name)); if (addedItems != null) { option = addedItems; break; } // Encuentra elementos actualizados: están en ambas listas pero tienen diferencias en sus propiedades updatedItems = options? .FirstOrDefault(opt => patientProcedures.Any(proc => proc.Name == opt.Name && !proc.AreEqualExcludeDescription(proc, opt))); if (updatedItems != null) { option = updatedItems; option.OptionType = "procedure"; } // Encuentra elementos eliminados: están en patientProcedures y en optionsList pero tienen una descripción if (options?.FirstOrDefault()?.Description != null) { removedItems = options.FirstOrDefault(); if (removedItems != null) removedItems.OptionType = "procedure"; } break; case MasterListType.TestList: var patientTests = patient.Tests ?? []; // Encuentra elementos agregados: están en optionsList pero no en patientProcedures addedItems = options?.FirstOrDefault(opt => patientTests.All(test => test.Name != opt.Name)); if (addedItems != null) { option = addedItems; break; } // Encuentra elementos actualizados: están en ambas listas pero tienen diferencias en sus propiedades updatedItems = options? .FirstOrDefault(opt => patientTests.Any(test => test.Name == opt.Name && !test.AreEqualExcludeDescription(test, opt))); if (updatedItems != null) { option = updatedItems; option.OptionType = "test"; } // Encuentra elementos eliminados: están en patientProcedures y en optionsList pero tienen una descripción if (options?.FirstOrDefault()?.Description != null) { removedItems = options.FirstOrDefault(); if (removedItems != null) removedItems.OptionType = "test"; } break; case MasterListType.TreatmentList: var patientTreatment = patient.Treatment ?? []; // Encuentra elementos agregados: están en optionsList pero no en patientProcedures addedItems = options?.FirstOrDefault(opt => patientTreatment.All(proc => proc.Name != opt.Name)); if (addedItems != null) { option = addedItems; option.OptionType = "treatment"; break; } // Encuentra elementos actualizados: están en ambas listas pero tienen diferencias en sus propiedades updatedItems = options? .FirstOrDefault(opt => patientTreatment.Any(proc => proc.Name == opt.Name && !proc.AreEqualExcludeDescription(proc, opt))); if (updatedItems != null) { option = updatedItems; option.OptionType = "treatment"; } // Encuentra elementos eliminados: están en patientProcedures y en optionsList pero tienen una descripción if (options?.FirstOrDefault()?.Description != null) { removedItems = options.FirstOrDefault(); if (removedItems != null) removedItems.OptionType = "treatment"; } break; } ActionsEnum.CrudAction? action = addedItems != null ? ActionsEnum.CrudAction.Create : updatedItems != null ? ActionsEnum.CrudAction.Update : removedItems?.Description is "Archive" ? ActionsEnum.CrudAction.Archive : ActionsEnum.CrudAction.Delete; var patietCarePlan = new PatientCarePlan { PatientId = patient.Id, PatientNumber = patient.PatientNumber, PointOfCareId = patient.PointOfCareId, Action = action, CarePlan = action is ActionsEnum.CrudAction.Delete or ActionsEnum.CrudAction.Archive ? removedItems : option, UserId = user?.Id, Description = action == ActionsEnum.CrudAction.Delete && string.IsNullOrEmpty(removedItems?.Description) ? "No reason given" : removedItems?.Description }; await _patientCarePlanService.InsertOneAsync(patietCarePlan); if (action != ActionsEnum.CrudAction.Delete && action != ActionsEnum.CrudAction.Archive && removedItems != null) { var secondaryAction = removedItems.Description is "Archive" ? ActionsEnum.CrudAction.Archive : ActionsEnum.CrudAction.Delete; var patietCarePlanSecondaryAction = new PatientCarePlan { PatientId = patient.Id, PatientNumber = patient.PatientNumber, PointOfCareId = patient.PointOfCareId, Action = secondaryAction, CarePlan = removedItems, UserId = user?.Id, Description = secondaryAction == ActionsEnum.CrudAction.Delete && string.IsNullOrEmpty(option?.Description) ? "No reason given" : option?.Description }; await _patientCarePlanService.InsertOneAsync(patietCarePlanSecondaryAction); } } /// /// Updates the incoming data of an existing patient, records an audit log entry for the change, and broadcasts the updated patient information. /// /// The unique identifier of the patient to update. /// The patient object containing the updated incoming data. /// Thrown when the patient cannot be found by the given identifier, or when the incoming data update operation fails. public async Task UpdatePatientIncomingData(ObjectId patientId, Patient person) { var oldPatient = await _patientRepository.FindByPatientId(patientId) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); var pat = await _patientRepository.UpdatePatientIncomingData(patientId, person) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, pat); await SendPatientBroadcast(pat); } /// /// Updates the patient record with incoming data and persists the income data as a nurse observation associated with the patient and the current user. /// /// The unique identifier of the patient being updated. /// The patient entity containing the updated information to apply. /// The patient income information to store as a new observation. /// The user performing the operation, recorded as the author of the observation. public async Task UpdatePatientIncomingData(ObjectId patientId, Patient person, PatientIncomeData patientIncomeData, User user) { await UpdatePatientIncomingData(patientId, person); var obs = new PatientObservation { Id = ObjectId.GenerateNewId(), Name = "PatientIncomingData", Value = patientIncomeData, PatientId = patientId, Time = DateTime.Now, UserId = user.Id }; await _observationService.Value.InsertNurseObservation(obs); } /// /// Updates the master list option for patients associated with the specified units, creates an audit log entry capturing the previous and updated state for each affected patient, and broadcasts the updated patient information. /// /// The master list option update data to apply to the patients. /// The collection of units whose associated patients will have the master list option updated. /// The name of the master list type used to identify which option to update. public async Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable unitList, string typeName) { // Filtrar paciente por unidad y valor del item de la lista var unitIds = unitList.Select(x => x.Id).ToList(); var oldPatientList = await _patientRepository.GetPatientsByUnitIds(unitIds, typeName); var patientUpdatedList = await _patientRepository.UpdateMasterListOption(unitIds, opt, typeName); foreach (var patient in patientUpdatedList) { var oldPatient = oldPatientList.Find(p => p.Id == patient.Id); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); await SendPatientBroadcast(patient); } } /// /// Deletes a master list option from the records of all patients associated with the specified units, /// broadcasts the update, and removes the option from any related manual observations. /// /// The master list option to be removed from patient records. /// The collection of units whose associated patients will be updated. /// The name of the master list type from which the option is being deleted. public async Task DeletePatientMasterListItem(OptionList opt, IEnumerable unitList, string typeName) { var unitIds = unitList.Select(x => x.Id).ToList(); var patientUpdatedList = await _patientRepository.DeleteMasterListOption(unitIds, opt, typeName); foreach (var patient in patientUpdatedList) { var patientUpdated = await FindById(patient.Id); if (patientUpdated == null) continue; await SendPatientBroadcast(patientUpdated); var obsName = GetManualObservationName(typeName); var obs = await _observationService.Value.FindLastObservationsByField(patientUpdated.Id, new List { new() { Name = obsName } }); if (obs.FirstOrDefault() != null) { var newObs = obs.First().Value as List; newObs?.RemoveAll(c => c.Id == opt.Id); await GenerateNurseObsAndInsert(obsName, newObs, patient.Id, null); } } } /// /// Updates the demographic data of an existing patient, synchronizes the associated allergy, language barrier, and diagnosis master lists, records an audit log entry, and broadcasts the update. Throws a conflict exception when the patient cannot be found, and gracefully handles null language barrier, diagnosis, and allergy values. /// /// The unique identifier of the patient to update. /// The patient object containing the updated demographic data. /// The user performing the update, used for audit logging; may be null. /// Thrown when no patient is found for the specified patient ID. public async Task UpdatePatientDemographicData(ObjectId patientId, Patient person, User? user) { var oldPatient = await _patientRepository.FindByPatientId(patientId) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); var pat = await _patientRepository.UpdatePatientDemographicData(patientId, person); if (pat != null) { await UpdatePatientMasterList(pat.Id, MasterListType.AllergyList, pat.Allergies ?? [], user, null); var lbl = pat.LanguageBarrier != null ? pat.LanguageBarrier : null; var dl = pat.Diagnosis != null ? new List { pat.Diagnosis } : null; await UpdatePatientMasterList(pat.Id, MasterListType.LanguageBarrierList, lbl ?? [], user, null); await UpdatePatientMasterList(pat.Id, MasterListType.DiagnosisList, dl ?? [], user, null); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, pat); await SendPatientBroadcast(pat); } } /// /// Searches for a patient by their patient number within a specific unit, ensuring the patient belongs to a different unit (distinct). /// /// The patient number to search for. /// The identifier of the unit used to find a patient that is distinct from it. /// A task containing the found , or null if no matching patient is found. public Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId) { return _patientRepository.SearchByPatientNumberAndDistinctUnit(patientNumber, unitId); } /// /// Asynchronously retrieves all patients whose procedures have finished, using the specified time threshold to determine which finished procedures are eligible for archival. /// /// The minimum age, in minutes, of a finished procedure's end date used as the cutoff for including patients in the result. /// A task that represents the asynchronous operation, containing a list of instances associated with finished procedures matching the specified threshold. public async Task> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes) { return await _patientRepository.FindAllPatientWithFinishedProcedures(archiveProcedureEndDateAfterMinutes); } /// /// Asynchronously retrieves all patients whose tests have finished, filtering based on the archive test end date threshold. /// /// The number of minutes after the archive test end date used to determine which finished tests to include. /// A task that represents the asynchronous operation, containing a list of patients with finished tests. public async Task> FindAllPatientWithFinishedTest(int archiveTestEndDateAfterMinutes) { return await _patientRepository.FindAllPatientWithFinishedTests(archiveTestEndDateAfterMinutes); } /// /// Retrieves all patients whose treatments have finished, where the treatment ended more than the specified number of minutes ago, typically for archiving purposes. /// /// The minimum number of minutes that must have elapsed since the treatment end date for a patient to be included in the result. /// A task representing the asynchronous operation, containing a list of patients with finished treatments matching the archive criteria. public async Task> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes) { return await _patientRepository.FindAllPatientWithFinishedTreatment(archiveTreatmentEndDateAfterMinutes); } /// /// Validates whether a patient can be inserted by ensuring required identifiers are present. /// Returns false if both and are null, or if is null/empty and creation without a patient number is not allowed. /// /// The patient instance to validate before insertion. /// true if the patient passes all validation checks; otherwise, false. private bool CheckPatient(Patient patient) { //patient.AdmTime ??= DateTime.UtcNow; _logger.LogDebug("Inserting patient with patientNumber: {PatientpatientNumber} {Patient}", patient.PatientNumber, patient); if (patient.PointOfCareId == null && patient.UnitId == null) { _logger.LogWarning( "Not Inserted patient with patientNumber: {PatientpatientNumber}. PointOfCare is null {Patient}", patient.PatientNumber, patient); return false; } if (string.IsNullOrEmpty(patient.PatientNumber)) { _logger.LogWarning("person with patientNumber null or empty: {Patient}", patient); if (!_cretatePatientWithoutPatientNumber) { _logger.LogWarning( "CretatePatientWithoutPatientNumber: {CretatePatientWithoutPatientNumber}: person not inserted: {Patient} ", _cretatePatientWithoutPatientNumber, patient.Id); return false; } } return true; } /// /// Sends the latest grouped observations for a patient to all subscribers associated with a new point of care (POC), /// and removes the subscription grouping for subscribers that were tied to the previous POC when one is provided. /// Subscribers are deduplicated by DisplayId, and only those whose display configuration defines a grouped /// field list receive the generated observations. /// /// The identifier of the new point of care used to look up the relevant subscribers. /// The optional identifier of the previous point of care; when present, subscribers still associated /// with it but not with are removed from the subscription group. /// The patient whose grouped observations are generated and dispatched to the subscribers. private async Task SendLastGroupedObsToSubscriberByPoc(ObjectId newPoc, ObjectId? oldPoc, Patient patient) { // Enviar y rellenar los subscriptores que necesitan las observaciones agrupadas para este paciente // Obtén la lista de suscriptores var relevantSubscribers = _subscribersService.GetByPocId(newPoc); // Filtra y selecciona los elementos con DisplayId único var filteredSubscribers = relevantSubscribers .GroupBy(subscriber => subscriber.DisplayId) // Agrupa por DisplayId .Where(group => group.Key != null) // Asegurar de que DisplayId no sea null .Select(group => group.First()) // Selecciona el primer elemento de cada grupo .ToList(); // Convierte el resultado a una lista foreach (var sub in filteredSubscribers) if (sub.DisplayId.HasValue) { var display = await _displayService.GetById(sub.DisplayId.Value); if (display is { DisplayConfigId: not null }) { var config = await _displayConfigService.GetById(display.DisplayConfigId.Value); var configGroupField = config.GroupedFieldList; if (configGroupField != null) foreach (var groupedField in configGroupField) { var groupedObservation = await _groupedObservationService.GenerateGroupedObservation( patient.Id, groupedField); _ = _clientMessageService.SendAsync(sub.Id, OperationType.GroupedObservation, groupedObservation); _subscriberGroupedService.CheckOnSubscriptionGroup(groupedField, patient.Id, "Romance Standard Time", sub.Id, groupedObservation); } } } // En este punto los subscriptores que necesitan las obs agrupadas para este paciente ya las tienen // Y la lista de subscriptorGrouped ya tiene la id del nuevo subscriptor // Ahora habria que ver que subscriptores ya no necesitan las agrupadas para este paciente if (oldPoc.HasValue) { var relevantSubscribersToDelete = _subscribersService.GetByPocId(oldPoc.Value); var relevantSubscribersToDeleteFiltered = relevantSubscribersToDelete.Where(sub => !sub.LocationIds.Contains(newPoc)); foreach (var wsSubscriber in relevantSubscribersToDeleteFiltered) _subscriberGroupedService.RemoveWsSubscriberPatientIdAndWsId(patient.Id.ToString(), wsSubscriber.Id); } } /// /// Asynchronously sends the most recent observations to all subscribers associated with the specified Point of Care. /// For each subscriber with a valid , retrieves its display configuration, loads the last observations /// for the patient matching the configured fields, orders them chronologically, and transmits them via the client message service. /// Subscribers without a or whose display lacks a DisplayConfigId are skipped. /// /// The Point of Care used to look up the list of relevant subscribers. /// The patient whose last observations are retrieved and forwarded to the subscribers. private async Task SendLastObsToSubscriberByPoc(PointOfCare newPoc, Patient patient) { // Obtén la lista de suscriptores var relevantSubscribers = _subscribersService.GetByPocId(newPoc.Id); // Filtra y selecciona los elementos con DisplayId único // var filteredSubscribers = relevantSubscribers // .GroupBy(subscriber => subscriber.DisplayId) // Agrupa por DisplayId // .Where(group => group.Key != null) // Asegúrate de que DisplayId no sea null // .Select(group => group.First()) // Selecciona el primer elemento de cada grupo // .ToList(); // Convierte el resultado a una lista foreach (var sub in relevantSubscribers) if (sub.DisplayId.HasValue) { var display = await _displayService.GetById(sub.DisplayId.Value); if (display is { DisplayConfigId: not null }) { var config = await _displayConfigService.GetById(display.DisplayConfigId.Value); var observations = await _observationService.Value.FindLastObservationsByField(patient.Id, config.FieldList); if (observations.Count > 0) { observations = observations.OrderBy(p => p.Time).ToList(); foreach (var observation in observations) { var obs = await _observationService.Value.MapObservationsByName(observation); if (obs != null) _ = _clientMessageService.SendAsync(sub.Id, OperationType.Observation, obs); } } } } } /// /// Updates the point-of-care and unit assignment of a patient, handling transitions between physical and virtual point-of-care locations (including recovery from a previously deleted virtual location), refreshing the patient's location, altable status, and audit log, and propagating the change to the point-of-care service. /// /// The identifier of the patient whose location will be updated. /// The identifier of the new unit the patient is being moved to. /// The identifier of the new point-of-care the patient is being moved to. private async Task UpdateLocation(ObjectId id, ObjectId unitId, ObjectId pocId) { try { _logger.LogDebug("Update location id: {id}", id); var patient = await _patientRepository.FindById(id); var oldPatient = await _auditService.DeepCopyAsync(patient); if (patient is not { PointOfCareId: not null }) { _logger.LogError("person is null. Not updated location for patient id: {id} ", id); return; } var newPoc = await _pointOfCareService.FindById(pocId); var newUnit = await _unitService.FindById(unitId); // El valor de bed: 04 daria como resultado NO_BED por eso comprobamos que no sea un int !int.TryParse(newPoc.Bed, out _) var isMoveToVirtualPoc = newPoc != null && !int.TryParse(newPoc.Bed, out _) && Enum.TryParse(newPoc.Bed, out _); var oldLocation = await _pointOfCareService.FindById(patient.PointOfCareId.Value); var virtualPocOldLocation = VirtualPointOfCare.Unknown; var comesFromVirtualPoc = oldLocation != null && !int.TryParse(oldLocation.Bed, out _) && Enum.TryParse(oldLocation.Bed, out virtualPocOldLocation); if (comesFromVirtualPoc) { var isRecovered = VirtualPointOfCare.Deleted.Equals(virtualPocOldLocation); // Añadir log warning if (isRecovered) { _logger.LogWarning( "Patient is recovered on UpdateLocation patientId: {id}, oldLocation: {oldLocation}, newLocation: {newPoc}", patient.Id, oldLocation?.Location.ToString(), newPoc?.Location.ToString()); patient.DisTime = null; } } patient.PointOfCareId = pocId; patient.UnitId = unitId; var bed = newPoc?.Bed; var room = newPoc?.Room; var location = new PatientLocation(newUnit?.Name, bed, room); patient.Location = location; if (isMoveToVirtualPoc && !comesFromVirtualPoc) patient.Altable = new OptionList { IconDefault = "icNotAltable", OptionType = "NotAltable", Name = "NotAltable" }; await _patientRepository.UpdateOneAsync(patient.Id, patient); if (oldPatient != null) await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); if (isMoveToVirtualPoc && !comesFromVirtualPoc) { await ExitPatientById(patient.Id, false); _pointOfCareService.CheckNextAdmission(oldLocation?.Id); } if (!isMoveToVirtualPoc && comesFromVirtualPoc) await _pointOfCareService.SetPointOfCareStatus(pocId, StatusEnum.PointOfCare.InUse); //Prio location to advertise both locations of movement between sections _logger.LogDebug( "Move patient: {id} from oldPocId: {oldPocId}, to newPocId: {newPocId}", id, oldLocation != null ? oldLocation.Id : "NULL", pocId); if (!isMoveToVirtualPoc && !comesFromVirtualPoc && oldLocation != null) { await _pointOfCareService.SetPointOfCareStatus(pocId, StatusEnum.PointOfCare.Available); await Move(patient, pocId, oldLocation.Id); } else { _logger.LogDebug( "Patient: {id} not moved because isMoveToVirtualPoc: {isMoveToVirtualPoc}, comesFromVirtualPoc: {comesFromVirtualPoc}, oldLocationIsNotNull: {oldLocation}", id, isMoveToVirtualPoc, comesFromVirtualPoc, oldLocation != null); } //TODO: Revisar // CheckLocationForWsSubscriber(location, id); } catch (Exception ex) { _logger.LogError("error updating location for patient id: {id} exception: {exMessage}", id, ex.Message); } } /// /// If location is updated and pointOfCare are DELETED or PUSHED /// Ws subscriber for GropedObs dont need to keep notificated /// Else could be that patient from Section A move to Section B /// clients from the section that is moved dont need to keep notified /// and other client will ask for grouped obs once receive SendPatientLocationBroadcast /// /// New location where patient is getting updated /// Object Id for patient private void CheckLocationForWsSubscriber(PatientLocation? newLocation, ObjectId? patientId) { if (patientId == null || newLocation == null) return; try { if (newLocation.Bed == VirtualPointOfCare.Pushed.ToString() || newLocation.Bed == VirtualPointOfCare.Deleted.ToString() || newLocation.Bed == VirtualPointOfCare.Moved.ToString()) _subscriberGroupedService.RemoveGroupedObsByPatientId(patientId.Value.ToString()); else _subscriberGroupedService.RemoveWsSubscriberByLocation(patientId.Value.ToString(), newLocation); } catch (Exception ex) { Log.Error( $"ERROR DELETING PATIENT MOVEMENT. person Id: {patientId} New location: {newLocation.ToStr()}. Exception message: {ex.Message} ex: {ex}"); } } /// method ArchiveNotUpdatedPatientsSince archive all patients who have not updated since in /// hoursBeforeArchive /// . /// public async Task ArchiveNotUpdatedPatientsInInactivePoCSince(int hoursBeforeArchive) { _logger.LogDebug( "Archiving not updated and in inactive PoC patients since older than {hoursBeforeArchive} hours", hoursBeforeArchive); try { // Patients not updated since date var dischargedPatients = await _patientRepository.FindPatientsNotUpdatedSince(DateTime.Now.AddHours(-hoursBeforeArchive)); foreach (var patient in dischargedPatients.Where(p => !p.IsInActivePoC())) await ArchivePatient(patient); } catch (Exception ex) { _logger.LogError("Error archiving not updated and in inactive PoC patients {exMessage}", ex); } } /// /// Broadcasts a patient location update to all subscribers whose registered location identifiers intersect with the specified list. Returns immediately without broadcasting when the provided location identifiers are null or empty. /// /// The patient associated with the location update. /// The patient location information to broadcast. /// The list of point-of-care location identifiers used to filter eligible subscribers. If null or empty, no broadcast is performed. public Task SendPatientLocationBroadcast(object patient, PatientLocation location, List? pocs) { if (pocs == null || pocs.Count == 0) return Task.CompletedTask; var subscribers = _subscribersService.GetSubscribers().Where (s => s.LocationIds.Any(pocs.Contains) ).ToList(); subscribers.ForEach(Action); return Task.CompletedTask; void Action(WsSubscriber subscriber) { _clientMessageService.SendAsync(subscriber.Id, OperationType.PatientLocation, new Dictionary { { "patient", patient }, { "location", location } }); } } /// /// Asynchronously broadcasts a notification indicating that a patient is now under the care of the specified attending doctor to all subscribers associated with the patient's point of care location. /// /// The unique identifier of the patient whose attending doctor assignment should be broadcast. /// The representing the doctor now attending the patient, included in the broadcast payload. private async Task SendPatientAttendingDoctorBroadcast(ObjectId patientid, Person attendingDoctor) { var patient = await FindById(patientid); if (patient == null) return; var poc = await _pointOfCareService.FindPoCByPatientId(patient.Id); if (poc == null) return; var subscribers = _subscribersService.GetSubscribers().Where(s => s.LocationIds.Any(sb => sb == poc.Id)) .ToList(); subscribers.ForEach(Action); return; void Action(WsSubscriber subscriber) { _clientMessageService.SendAsync(subscriber.Id, OperationType.PatientAttendingDoctor, new Dictionary { { "id", patientid.ToString() }, { "attendingDoctor", attendingDoctor } }); } } /// /// Broadcasts patient data to all subscribers associated with the patient's point of care. If the patient cannot be found, the method returns without performing any broadcast. /// /// The unique identifier of the patient whose data will be broadcast. /// The person data payload to be sent to the matching subscribers. private async Task SendPatientDataBroadcast(ObjectId patientid, Person data) { var patient = await FindById(patientid); if (patient == null) return; //var section = await _sectionService.FindByPointOfCare(patient.UnitString); //if (section == null) return; var subscribers = _subscribersService.GetSubscribers().Where(s => s.LocationIds.Any(c => c == patient.PointOfCareId)); foreach (var subscriber in subscribers) _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.PatientData, new Dictionary { { "id", patientid.ToString() }, { "data", data } }); } /// /// Sends an asynchronous patient update broadcast to all subscribers whose location matches the patient's point of care, grouped by their locale. Falls back to the default locale when a group has no locale key, and translates the patient data per locale before dispatching each message as a fire-and-forget send. /// /// The patient whose information is being broadcast; its PointOfCareId is used to match subscribers and its UnitId is used to resolve the unit for translation. private async Task SendPatientBroadcast(Patient patient) { var subscribersGroup = _subscribersService.GetSubscribers() .Where(s => patient.PointOfCareId != null && s.LocationIds.Any(c => c == patient.PointOfCareId.Value)).GroupBy(h => h.Locale); var unit = await _unitService.FindById(patient.UnitId); foreach (var group in subscribersGroup) { var locale = group.Key ?? LocaleEnum.Default; IEnumerable subscribers = group; foreach (var subscriber in subscribers) { var patientWithLocale = await _masterListServiceFactory.GetPatientTraslated(unit, locale, patient); _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.UpdatePatient, patientWithLocale); } } } /// /// Broadcasts a delete-patient notification to all subscribers linked to the specified point of care location, after removing the patient's grouped observations. /// /// The unique identifier of the patient whose deletion will be broadcast. /// The point of care location identifier used to filter the targeted subscribers. private Task SendDeletePatientBroadcast(ObjectId patientid, ObjectId pocId) { var subscribers = _subscribersService.GetSubscribers().Where(s => s.LocationIds.Any(x => x == pocId)).ToList(); _logger.LogDebug("Remove grouped obs for patientId: {patientid} on location: {location}", patientid, pocId); _subscriberGroupedService.RemoveGroupedObsByPatientId(patientid.ToString()); subscribers.ForEach(Action); return Task.CompletedTask; void Action(WsSubscriber subscriber) { _clientMessageService.SendAsync(subscriber.Id, OperationType.DeletePatient, new Dictionary { { "id", patientid.ToString() } }); } } /// /// Updates a entity from the data contained in an , /// applying only the fields that are explicitly provided (admission time, discharge time, patient ID, /// and demographic data) and creating the associated when missing. /// Patient identifiers are synchronized via using the request's message time, /// falling back to the current UTC time when the message time is . /// /// The patient entity to be updated in place. /// The API request providing the source data for the update. private void UpdatePatientFromRequestToIdList(Patient patient, ApiRequest apiRequest) { if (apiRequest.AdmTime.HasValue) patient.AdmTime = apiRequest.AdmTime; if (!string.IsNullOrEmpty(apiRequest.PatientId)) patient.PatientId = apiRequest.PatientId; if (apiRequest.Patient != null && !apiRequest.Patient.IsEmpty()) { // Log action to describe patient.Person ??= new Person(); patient.Person.BirthDate = apiRequest.Patient.BirthDate; patient.Person.Language = apiRequest.Patient.Language; patient.Person.FirstName = apiRequest.Patient.FirstName; patient.Person.Gender = apiRequest.Patient.Gender; patient.Person.LastName = apiRequest.Patient.LastName; patient.Person.SecondName = apiRequest.Patient.SecondName; } if (apiRequest.Patient?.Ids != null) { patient.Person ??= new Person(); patient.Person.SetIds(apiRequest.Patient.Ids, apiRequest.MessageTime == DateTime.MinValue ? DateTime.UtcNow : apiRequest.MessageTime); } if (apiRequest.DisTime.HasValue) patient.DisTime = apiRequest.DisTime; patient.UpdateDate = DateTime.UtcNow; } /// /// Removes a patient from their current location and reassigns them to a specified virtual point of care, /// falling back to auto-generated identifiers and error-marked location strings when the virtual point of care lookup fails. /// After updating the patient record, the patient is exited by id and the next admission for the previous point of care is checked. /// /// The API request containing operational data such as the discharge time used to set the patient's discharge timestamp. /// The patient to be moved, updated with the new point of care, unit, and location information. /// The virtual point of care identifier used to look up the destination bed, room, and unit. private async Task RemovePatientAndSendToVirtualPoc(ApiRequest apiRequest, Patient patient, VirtualPointOfCare virtualPocEnum) { var virtualPoc = await _pointOfCareService.FindByBedAndUnitId(virtualPocEnum.ToString(), patient.UnitId); if (virtualPoc == null) _logger.LogError( "Error getting virtual poc {virtualPocEnum} PATIENT: {patientId} on unit: {patientUnitId} continue with autogenerate pocId", virtualPocEnum.ToString(), patient.Id, patient.UnitId); var uni = await _unitService.FindById(patient.UnitId); var oldLocation = patient.PointOfCareId; patient.DisTime = apiRequest.DisTime ?? DateTime.UtcNow; patient.PointOfCareId = virtualPoc?.Id ?? new ObjectId(); patient.UnitId = virtualPoc?.UnitId ?? patient.UnitId; patient.Location = new PatientLocation(uni?.Name, virtualPoc?.Bed ?? $"Error on {virtualPocEnum.ToString()}", virtualPoc?.Room ?? $"Error on {virtualPocEnum.ToString()}"); await _patientRepository.UpdateOneAsync(patient.Id, patient); _logger.LogDebug("{virtualPocEnum} PATIENT: {patientId} ", virtualPoc?.ToString(), patient.Id); await ExitPatientById(patient.Id, false); if (virtualPoc != null && oldLocation.HasValue) _ = SendLastGroupedObsToSubscriberByPoc(virtualPoc.Id, oldLocation.Value, patient); _pointOfCareService.CheckNextAdmission(oldLocation); } // * To ensure db sync with ICCA we receive their patients data table and sync with our patients. // * GetByCodeSysAndCode all boxes from all sections and match with iccaPatients /// /// Synchronizes patient data between the ICCA system and the local database by reconciling point-of-care locations. /// Handles three main cases: creating/updating patients present in ICCA but not in the DB, matching existing patients by number, and moving patients absent from ICCA to a temporal/unknown bed. Also persists patients from ICCA locations that are outside the configured point-of-care boxes, falling back to a default unknown point-of-care when a matching location is not found. /// /// The API request containing the list of ICCA patients to synchronize with the local database. private async Task ProcessIccaSync(ApiRequest apiRequest) { //var sections = await _sectionService.GetAll(); // var boxes = sections.Where(box => box.Items != null).SelectMany(s => s.Items.SelectMany(d => d.Boxes)) // ?.ToList(); //var boxes = sections.SelectMany(s => s.items.SelectMany(d => d.boxes)).ToList(); var pocList = await _pointOfCareService.GetAllLocationInfo(); var unitList = await _unitService.GetAll(); foreach (var pointOfCare in pocList) pointOfCare.Unit = unitList.FirstOrDefault(u => u.Id == pointOfCare.UnitId); var iccaPatientsFromOtherLocations = apiRequest.IccaPatients?.Select(x => x.DeepCopy()).ToList(); foreach (var poc in pocList) { if (poc.Unit == null) continue; var iccaPatientOnBox = apiRequest.IccaPatients?.FirstOrDefault(i => i.Location.Bed == poc.Bed && i.Location.UnitName == poc.Unit.Name); if (iccaPatientOnBox != null) iccaPatientsFromOtherLocations?.RemoveAll(s => s.PatientNumber == iccaPatientOnBox.PatientNumber); //Find actual patient in poc var actualPatient = await FindByPointOfCareId(poc.Id); // 1 Exists in ICCA and not in our DB => create it and if any other patient is on bed move it to Temporal. if (iccaPatientOnBox != null && actualPatient == null && iccaPatientOnBox.PatientNumber != null) { //check if patient exists to only move var patientInOtherBox = await FindByPatientNumber(iccaPatientOnBox.PatientNumber); if (patientInOtherBox != null) await UpdateLocation(patientInOtherBox.Id, new PatientLocation(poc.Unit.Name, poc.Bed, poc.Room)); else await Insert(new Patient { AdmTime = iccaPatientOnBox.AdmitTime, PatientId = iccaPatientOnBox.PatientId, PatientNumber = iccaPatientOnBox.PatientNumber, Location = new PatientLocation(poc.Unit.Name, poc.Bed, poc.Room), UnitId = poc.UnitId, PointOfCareId = poc.Id }); } // 2 Exists in ICCA and in our DB => equal patientNumber same person, continue else move actual and create new from icca else if (iccaPatientOnBox != null && actualPatient != null) { if (iccaPatientOnBox.PatientNumber == actualPatient.PatientNumber) { //They are the same do anything. } else { await UpdateLocation(actualPatient.Id, new PatientLocation(poc.Unit.Name, VirtualPointOfCare.Moved.ToString(), VirtualPointOfCare.Moved.ToString())); await Insert(new Patient { AdmTime = iccaPatientOnBox.AdmitTime, PatientId = iccaPatientOnBox.PatientId, PatientNumber = iccaPatientOnBox.PatientNumber, Location = new PatientLocation(poc.Unit.Name, poc.Bed, poc.Room), UnitId = poc.UnitId, PointOfCareId = poc.Id }); } } // 3 Exists in our DB and not in ICCA => move it to temporal bed to match patients on layout boxes. else if (iccaPatientOnBox == null && actualPatient != null) { //move it to another bed await UpdateLocation(actualPatient.Id, new PatientLocation(poc.Unit.Name, VirtualPointOfCare.Unknown.ToString(), VirtualPointOfCare.Unknown.ToString())); } } if (iccaPatientsFromOtherLocations == null) return; foreach (var patientOutOfConfigBoxes in iccaPatientsFromOtherLocations) { //Patients from others PointsOfCares or box not in our configured sections. we save them if (patientOutOfConfigBoxes.PatientNumber == null) continue; //check exists var patientExists = await FindByPatientNumber(patientOutOfConfigBoxes.PatientNumber); if (patientExists != null) continue; var patientLocation = await FindByLocation(patientOutOfConfigBoxes.Location); //move it to another bed if (patientLocation != null) { var unitName = unitList.FirstOrDefault(u => u.Id == patientLocation.UnitId); await UpdateLocation(patientLocation.Id, new PatientLocation(unitName?.Name ?? "UNKNOWN", VirtualPointOfCare.Unknown.ToString(), VirtualPointOfCare.Unknown.ToString())); } var pocUnknown = pocList.FirstOrDefault(u => u.Bed == VirtualPointOfCare.Unknown.ToString()); var locationFromRequest = pocList.FirstOrDefault(u => u.Bed == patientOutOfConfigBoxes.Location.Bed && u.Unit?.Name == patientOutOfConfigBoxes.Location.UnitName); await Insert(new Patient { AdmTime = patientOutOfConfigBoxes.AdmitTime, PatientId = patientOutOfConfigBoxes.PatientId, PatientNumber = patientOutOfConfigBoxes.PatientNumber, PointOfCareId = locationFromRequest?.Id ?? pocUnknown?.Id ?? ObjectId.GenerateNewId(), UnitId = locationFromRequest?.UnitId ?? pocUnknown?.UnitId ?? ObjectId.GenerateNewId() }); } } /*For adt_a02 || adt_a12 */ /// /// Processes an ADT (Admit/Discharge/Transfer) move patient request, handling existing patient updates, location transitions, and new patient creation. /// Manages cases where the patient is null, where another patient already occupies the target location (moving them to the "Moved" point of care), where the bed is empty or unknown, and where the request location is null (defaulting to the unknown point of care). /// Also broadcasts a notification and marks the point of care as in use when a new patient is created from the request. /// /// The API request containing the patient number, patient data, and target location information used for the move operation. /// The optional patient to be moved; if null, a new patient is created from the request. private async Task ProcessAdtMovePatient(ApiRequest apiRequest, Patient? patient) { try { var patientExistsInLocation = await FindByLocation(apiRequest.Location); if (patient is { PatientNumber: not null } && apiRequest.Patient != null && patient.PatientNumber == apiRequest.PatientNumber && _updatePatientDataWithAdtA02) { _logger.LogDebug( "Debg MovePatient, UpdatePatientDataWithADT_A02 is enabled, patient number found {pn} and apirequest patient number {pnApirequest} are the same update demographic data", patient.PatientNumber, apiRequest.PatientNumber); //await UpdatePatientFromRequest(patient, apiRequest, true); await UpdatePatientData(patient.Id, patient.PatientNumber, apiRequest.Patient, false); } if (patientExistsInLocation != null) { if (patientExistsInLocation.PatientNumber != apiRequest.PatientNumber) { _logger.LogDebug( "Debug MovePatient, patient to move is null but patientExistsInLocation is not null moving patient: {patientExistsInLocation}, to pointOfCare MOVED", patientExistsInLocation); var movedLocationPoc = await _pointOfCareService.FindByBedAndUnitId( VirtualPointOfCare.Moved.ToString(), patientExistsInLocation.UnitId); if (movedLocationPoc != null) await UpdateLocation(patientExistsInLocation.Id, movedLocationPoc.UnitId, movedLocationPoc.Id); } else { // Demographic data only return; } } if (patient != null) { //to solve problems when empty beds with not scapped "" if (apiRequest.Location != null) { var bed = apiRequest.Location.Bed?.Replace("\"", ""); apiRequest.Location.Bed = string.IsNullOrEmpty(bed) ? VirtualPointOfCare.Unknown.ToString() : bed; if (apiRequest.Location.Bed == VirtualPointOfCare.Unknown.ToString()) { var unknownLocationPoc = await _pointOfCareService.FindByBedAndUnitId( VirtualPointOfCare.Unknown.ToString(), patient.UnitId); if (unknownLocationPoc != null) await UpdateLocation(patient.Id, unknownLocationPoc.UnitId, unknownLocationPoc.Id); } else { var mapLocation = await _pocMappingService.Map(apiRequest.Location); var newLocationUnit = await _unitService.FindByName(apiRequest.Location.UnitName); var newLocationPoc = await _pointOfCareService.FindByBedAndUnitId(mapLocation?.Bed, newLocationUnit?.Id); if (newLocationPoc != null) { await UpdateLocation(patient.Id, newLocationPoc.UnitId, newLocationPoc.Id); } else { var unknownLocationPoc = await _pointOfCareService.FindByBedAndUnitId( VirtualPointOfCare.Unknown.ToString(), patient.UnitId); if (unknownLocationPoc != null) { await UpdateLocation(patient.Id, unknownLocationPoc.UnitId, unknownLocationPoc.Id); _logger.LogError( "Unable to find new location on ProcessAdtMovePatient location: {Unit} {Bed}", apiRequest.Location.UnitName, apiRequest.Location?.Bed); } } } } else { _logger.LogDebug("Move patient to null location transfering patient to UNKNOWN bed"); var unknownLocationPoc = await _pointOfCareService.FindByBedAndUnitId( VirtualPointOfCare.Unknown.ToString(), patient.UnitId); var unitUnknowPoc = await _unitService.FindById(patient.UnitId); if (unknownLocationPoc == null || unitUnknowPoc == null) { _logger.LogError( "Unable to find new location on ProcessAdtMovePatient location: Unit: {Unit}, Bed: {Bed}", unitUnknowPoc?.Name ?? "UNKNOWN", VirtualPointOfCare.Unknown.ToString()); return; } apiRequest.Location = new PatientLocation(unitUnknowPoc.Name, unknownLocationPoc.Bed, unknownLocationPoc.Room); await UpdateLocation(patient.Id, unitUnknowPoc.Id, unknownLocationPoc.Id); } _logger.LogDebug( "Debug MovePatient, patient not null to move: {patient} Bed: {apiRequestLocationBed}, PointOfCare: {apiRequestLocationPointOfCare}", patient, apiRequest.Location?.Bed, apiRequest.Location?.UnitName); } else { // if (patientExistsInLocation != null) // { // if (patientExistsInLocation.PatientNumber != apiRequest.PatientNumber) // { // _logger.LogDebug( // "Debug MovePatient, patient to move is null but patientExistsInLocation is not null moving patient: {patientExistsInLocation}, to pointOfCare MOVED", // patientExistsInLocation); // // await UpdateLocation(patientExistsInLocation.Id,movedLocationPoc.UnitId, movedLocationPoc.Id); // } // } patient = await CreatePatientFromRequest(apiRequest); if (patient != null) { var newLocationUnit = await _unitService.FindByName(apiRequest.Location?.UnitName); var newLocationPoc = await _pointOfCareService.FindByBedAndUnitId(apiRequest.Location?.Bed, newLocationUnit?.Id); if (newLocationPoc == null) { _logger.LogError("Unable to find new location on ProcessAdtMovePatient location: {Unit} {Bed}", apiRequest.Location?.UnitName, apiRequest.Location?.Bed); return; } _logger.LogDebug( "person to move was null on ADT_A02 newPatient: {patient}, proceed to send notification to front", patient); await SendNewPatientBroadcast(patient); await _pointOfCareService.SetPointOfCareStatus(newLocationPoc.Id, StatusEnum.PointOfCare.InUse); //_pointOfCareService.CheckNextAdmission(newLocationPoc.Id); } } } catch (Exception e) { _logger.LogError("Error on ProcessAdtMovePatient trace: {eStackTrace}, message: {eMessage}", e.StackTrace, e.Message); } } /// /// Process new patient. try to find him by /// /// or /// /// if exits and was not discharged use him. /// If not insert new one in patients table /// /// /// The Inserted patient or null. private async Task ProcessAdtPatientAdmit(ApiRequest apiRequest) { Patient? patient; try { if (apiRequest.Location != null) apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, apiRequest.Location); //Null ADTA01 var unit = await _unitService.FindByName(apiRequest.Location?.UnitName); if (unit == null) { _logger.LogDebug("Process admit patient to unit name: {UnitName} NOT FOUND", apiRequest.Location?.UnitName); return null; } var poc = await _pointOfCareService.FindByBedAndUnitId(apiRequest.Location?.Bed, unit.Id); if (poc == null) { _logger.LogDebug("Process admit patient to bed name: {BedName} NOT FOUND", apiRequest.Location?.Bed); return null; } var patientExistsInLocation = await FindByUnitAndPocId(unit.Id, poc.Id); //if previous patient exists is moved to PUSHED if (patientExistsInLocation != null) { _logger.LogDebug("person Exists InLocation: {PatientExistsInLocation}", patientExistsInLocation.ToStr()); if (patient == null || patientExistsInLocation.PatientNumber != patient.PatientNumber) { _logger.LogDebug("previous patient: {PatientExistsInLocationId} moved to PUSHED ", patientExistsInLocation.Id); _logger.LogDebug( "Remove grouped obs for patientId: {ApiRequestPatientId} patient exist on location for new patient location: {PatientExistsInLocationLocation}, traceId: {ApiRequestTraceId}, traceMessageId: {ApiRequestTraceMessageId}", apiRequest.PatientId ?? "NULL", patientExistsInLocation.Location, apiRequest.TraceId ?? "NULL", apiRequest.TraceMessageId ?? "NULL"); var pocPushed = await _pointOfCareService.FindByBedAndUnitId(VirtualPointOfCare.Pushed.ToString(), unit.Id); if (pocPushed != null) { await UpdateLocation(patientExistsInLocation.Id, unit.Id, pocPushed.Id); _ = SendLastGroupedObsToSubscriberByPoc(pocPushed.Id, poc.Id, patientExistsInLocation); } else { _logger.LogError( "Unable to update location for existing patient: {Patient} on poc: {VirtualPoc} and unit: {Unit}", patientExistsInLocation.Id.ToString(), VirtualPointOfCare.Pushed.ToString(), unit.Id.ToString()); } } //patient already exists and is the same again, do nothing else if (patient.PatientNumber == patientExistsInLocation.PatientNumber && patient.Location.Bed == patientExistsInLocation.Location.Bed && patient.Location.UnitName == patientExistsInLocation.Location.UnitName) { _logger.LogDebug("patient: {PatientId} already exist, ignoring ADT", patient.Id); return null; } } _logger.LogDebug("person dsTime:{PatientDisTime}", patient?.DisTime?.ToString() ?? "NULL"); if (patient is not { DisTime: null }) //If new patient, it returns true because patient not contains a field disTime == null { var creationDate = patient?.CreationDate ?? DateTime.UtcNow; if (patient != null) { patient.CreationDate = creationDate; patient.UnitId = unit.Id; patient.DisTime = null; patient.PointOfCareId = poc.Id; patient.Altable = new OptionList { IconDefault = "icNotAltable", Name = "NotAltable", OptionType = "NotAltable" }; } else { patient = new Patient { Id = ObjectId.GenerateNewId(), CreationDate = creationDate, UnitId = unit.Id, DisTime = null, PointOfCareId = poc.Id, Altable = new OptionList { IconDefault = "icNotAltable", Name = "NotAltable", OptionType = "NotAltable" } }; } } _logger.LogDebug("Updating patient from request. person: {Patient} ", patient.ToStr()); await UpdatePatientFromRequest(patient, apiRequest); if (patient.PatientNumber != null) { var patientInAdmission = await _admissionService.Value.GetAdmissionByPatientNumber(patient.PatientNumber); if (patientInAdmission != null) { await MergePatientWithAdmission(patient, patientInAdmission); await _admissionService.Value.DeleteAdmissionByIdAsync(patientInAdmission.Id); } } _logger.LogDebug("Inserting patient. person: {Patient}", patient.ToStr()); await Update(patient); _ = SendNewPatientBroadcast(patient); await _pointOfCareService.SetPointOfCareStatus(poc.Id, StatusEnum.PointOfCare.InUse); // Enviar en base a la config de display que tenga cada subscriptor _ = SendLastObsToSubscriberByPoc(poc, patient); // Enviar las grupadas dependiendo del config del subscriptor _ = SendLastGroupedObsToSubscriberByPoc(poc.Id, null, patient); _logger.LogDebug("Updated patient: {PatientId}", patient.Id); } catch (Exception e) { _logger.LogError("Exception processing patient admission : {EMessage}", e); return null; } return patient; } /// /// Merges selected fields from an admission record into a patient record, generating and inserting nurse observations for each non-null field that has an associated manual observation configured in the list settings. /// /// The patient whose properties will be updated with the values from the admission. /// The admission record providing the source values to be merged into the patient. private async Task MergePatientWithAdmission(Patient patient, Admission patientInAdmission) { patient.Insulation = patientInAdmission.Insulation; if (patient.Insulation != null) await GenerateNurseObsAndInsert(_listSettings.InsulationList.ManualObservationName!, [patient.Insulation], patient.Id, null); patient.Allergies = patientInAdmission.Allergies; if (patient.Allergies != null) await GenerateNurseObsAndInsert(_listSettings.AllergyList.ManualObservationName!, patient.Allergies, patient.Id, null); patient.Diagnosis = patientInAdmission.Diagnosis; if (patient.Diagnosis != null) await GenerateNurseObsAndInsert(_listSettings.DiagnosisList.ManualObservationName!, [patient.Diagnosis], patient.Id, null); patient.DiagnosisAux = patientInAdmission.DiagnosisAux; patient.LanguageBarrier = patientInAdmission.LanguageBarrier; if (patient.LanguageBarrier != null) await GenerateNurseObsAndInsert(_listSettings.LanguageBarrierList.ManualObservationName!, patient.LanguageBarrier, patient.Id, null); patient.Origin = patientInAdmission.Origin; if (patient.Origin != null) await GenerateNurseObsAndInsert(_listSettings.OriginList.ManualObservationName!, [patient.Origin], patient.Id, null); patient.OriginAux = patientInAdmission.OriginAux; } /// /// Updates an existing entity from the data contained in an , /// applying only the fields that are provided, looking up the referenced unit and point of care, and /// remapping "sin cama" / empty bed values to a virtual point of care to avoid duplicate key errors. /// /// The patient entity to mutate with values coming from the request. /// The incoming API request whose non-empty fields are applied to the patient. /// When true, skips unit and point-of-care lookups and any location-based updates. private async Task UpdatePatientFromRequest(Patient patient, ApiRequest apiRequest, bool ignoreLocation = false) { if (apiRequest.AdmTime.HasValue) patient.AdmTime = apiRequest.AdmTime; if (!string.IsNullOrEmpty(apiRequest.PatientId)) patient.PatientId = apiRequest.PatientId; if (apiRequest.Patient != null && !apiRequest.Patient.IsEmpty()) { // Log action to describe patient.Person ??= new Person(); patient.Person.BirthDate = apiRequest.Patient.BirthDate; patient.Person.Language = apiRequest.Patient.Language; patient.Person.FirstName = apiRequest.Patient.FirstName; patient.Person.Gender = apiRequest.Patient.Gender; patient.Person.LastName = apiRequest.Patient.LastName; patient.Person.SecondName = apiRequest.Patient.SecondName; } var patientNumberChanged = true; if (!string.IsNullOrEmpty(apiRequest.PatientNumber)) { // Log action to describe patientNumberChanged = patient.PatientNumber != apiRequest.PatientNumber; patient.PatientNumber = apiRequest.PatientNumber; } if (patientNumberChanged && apiRequest.Patient?.Ids != null) { patient.Person ??= new Person(); patient.Person.SetIds(apiRequest.Patient.Ids, apiRequest.MessageTime); } var unit = await _unitService.FindByName(apiRequest.Location?.UnitName); if (unit == null && !ignoreLocation) _logger.LogWarning( "WARNING updating patient from request, unit not found by name: {UnitName} for patientId: {PatientId} apiRequest: {Request}", apiRequest.Location?.UnitName ?? "", patient.Id, apiRequest.ToString()); if (apiRequest.AttendingDoctor != null && !apiRequest.AttendingDoctor.IsEmpty()) patient.AttendingDoctor = apiRequest.AttendingDoctor; if (apiRequest.Location != null && !apiRequest.Location.IsEmpty() && apiRequest.Location.Bed != null && apiRequest.Location.UnitName != null && !ignoreLocation && unit != null) { var poc = await _pointOfCareService.FindByBedAndUnitId(apiRequest.Location.Bed, unit.Id); if (poc != null) { patient.Location = new PatientLocation(unit.Name, poc.Bed, poc.Room); patient.UnitId = unit.Id; patient.PointOfCareId = poc.Id; } else { _logger.LogError( "Error updating patient from request finding poc by bed name: {BedName} and unitId: {UnitId} not found request: {ApiRequest}", apiRequest.Location?.Bed, unit.Id, apiRequest); } } //To solve multi "sin cama" from ICCADB making duplicated keys error on mongodb. if (apiRequest.Location != null && patient.Location.Bed != null && (patient.Location.Bed.ToLower().Equals("sin cama") || patient.Location.Bed == string.Empty) && unit != null) { // Log action to describe var poc = await _pointOfCareService.FindByBedAndUnitId(VirtualPointOfCare.NoBed.ToString(), unit.Id); if (poc != null) { patient.Location = new PatientLocation(unit.Name, poc.Bed, poc.Room); patient.UnitId = unit.Id; patient.PointOfCareId = poc.Id; } else { _logger.LogError( "Error updating patient from request with bed empty finding poc by bed name: {BedName} and unitId: {UnitId} not found request: {ApiRequest}", apiRequest.Location.Bed, unit.Id, apiRequest); } } if (apiRequest.DisTime.HasValue) patient.DisTime = apiRequest.DisTime; patient.UpdateDate = DateTime.UtcNow; } /// /// Processes a patient location update triggered by an ORU (Observation Result) message. /// Updates the patient's location and asynchronously broadcasts the change without awaiting the broadcast operation. /// /// The patient whose location will be updated. /// The API request containing the new location information. private async Task ProcessUpdatePatientLocationWithOru(Patient patient, ApiRequest apiRequest) { await UpdateLocation(patient.Id, apiRequest.Location); _ = SendPatientUpdateBroadcast(patient); } /// /// Updates a patient by applying values from the latest nurse observations (such as visits, access control, therapeutic ceiling, mobility, passive sitting, language barrier, insulation, doctors, allergies and altable) and by merging any treatments, procedures and tests missing from the incoming patient with those of the previous patient record; then persists the patient, creates an audit log entry, and broadcasts the change. /// /// The incoming patient to be updated with merged data. /// The previous patient record whose missing treatments, procedures and tests are preserved during the merge. public async Task UpdatePatientFromMerge(Patient patient, Patient oldPatient) { var listObsName = _masterListServiceFactory.StringNurseObs(); var obs = await _observationService.Value.FindLastObservations(patient.Id, 1, listObsName); patient.Visits = obs.FirstOrDefault(o => o.Name == _listSettings.VisitOptionList.ManualObservationName)?.Value is List visitList ? visitList.FirstOrDefault() : null; patient.AccessControl = obs.FirstOrDefault(o => o.Name == _listSettings.AccessControlList.ManualObservationName)?.Value is List accessControList ? accessControList.FirstOrDefault() : null; patient.TherapeuticCeiling = obs.FirstOrDefault(o => o.Name == _listSettings.TherapeuticCeilingList.ManualObservationName)?.Value is List ceilingList ? ceilingList.FirstOrDefault() : null; patient.Mobility = obs.FirstOrDefault(o => o.Name == _listSettings.MobilityOptionList.ManualObservationName)?.Value is List mobilityList ? mobilityList.FirstOrDefault() : null; patient.PassiveSitting = obs.FirstOrDefault(o => o.Name == _listSettings.PassiveSittingList.ManualObservationName)?.Value is List passiveList ? passiveList.FirstOrDefault() : null; patient.LanguageBarrier = obs.FirstOrDefault(o => o.Name == _listSettings.LanguageBarrierList.ManualObservationName)?.Value as List; patient.Insulation = obs.FirstOrDefault(o => o.Name == _listSettings.InsulationList.ManualObservationName)?.Value is List insulationList ? insulationList.FirstOrDefault() : null; patient.Doctors = obs.FirstOrDefault(o => o.Name == _listSettings.DoctorList.ManualObservationName) ?.Value as List; patient.Allergies = obs.FirstOrDefault(o => o.Name == _listSettings.AllergyList.ManualObservationName)?.Value as List; patient.Altable = obs.FirstOrDefault(o => o.Name == _listSettings.AltableOptionList.ManualObservationName)?.Value is List altableList ? altableList.FirstOrDefault() : null; var obsPatientIncomingData = (PatientIncomeData?)obs.FirstOrDefault(o => o.Name == "PatientIncomingData")?.Value; if (obsPatientIncomingData != null) { patient.AdmTime = obsPatientIncomingData.AdmTime; patient.Origin ??= obsPatientIncomingData.Origin; patient.OriginAux ??= obsPatientIncomingData.OriginAux; patient.Diagnosis ??= obsPatientIncomingData.Diagnosis; patient.DiagnosisAux ??= obsPatientIncomingData.DiagnosisAux; } if (patient.Treatment != null) { var missingTreatments = oldPatient.Treatment? .Where(t => patient.Treatment.All(p => p.Name != t.Name)) .ToList(); if (missingTreatments != null) patient.Treatment.AddRange(missingTreatments); } else if (oldPatient.Treatment != null) { patient.Treatment = oldPatient.Treatment; } if (patient.Procedures != null) { var missingProcedures = oldPatient.Procedures? .Where(t => patient.Procedures.All(p => p.Name != t.Name)) .ToList(); if (missingProcedures != null) patient.Procedures.AddRange(missingProcedures); } else if (oldPatient.Procedures != null) { patient.Procedures = oldPatient.Procedures; } if (patient.Tests != null) { var missingTests = oldPatient.Tests? .Where(t => patient.Tests.All(p => p.Name != t.Name)) .ToList(); if (missingTests != null) patient.Tests.AddRange(missingTests); } else if (oldPatient.Tests != null) { patient.Tests = oldPatient.Tests; } await _patientRepository.Update(patient); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); await SendPatientBroadcast(patient); } /// /// Generates a nurse observation for the specified patient and inserts it using the observation service. If is null, an empty list is used; if is null, the observation's user is left unset. /// /// The name of the observation to generate. /// The list of option values for the observation, or null to default to an empty list. /// The identifier of the patient associated with the observation. /// The user creating the observation, whose identifier is assigned to the observation; may be null. private async Task GenerateNurseObsAndInsert(string name, List? value, ObjectId patientId, User? user) { var obs = GenerateObs( name, value ?? [], patientId); obs.UserId = user?.Id; await _observationService.Value.InsertNurseObservation(obs); } /// /// Generates a for a specific patient using manual insertion mode and the current timestamp. /// If is null, an empty list is used as the observation value. /// /// The name of the observation. /// The list of values for the observation, or null to use an empty list. /// The identifier of the patient associated with the observation. /// A new populated with the provided data, the current date/time, and manual insert mode. private PatientObservation GenerateObs(string name, List? value, ObjectId patientId) { var valueList = value ?? []; return new PatientObservation { Name = name, Value = valueList, PatientId = patientId, Time = DateTime.Now, InsertMode = ObservationEnum.InsertMode.Manual }; } /// /// Retrieves the manual observation name associated with a given master list name by looking up the corresponding property in the class via reflection and returning its ManualObservationName. Returns a localized error message string when the property is not found or when the resolved value is not a . /// /// The name of the property in whose associated manual observation name should be returned. Must match the property name exactly. /// The ManualObservationName of the resolved , or a descriptive error string if the property is not found or the property value is not a . public string GetManualObservationName(string masterListNameString) { // 1. Obtener el tipo de la clase ListSettings. var settingsType = typeof(ListSettings); // 2. Obtener la propiedad con el nombre que coincide con el string del enum. // El 'masterListNameString' debe coincidir exactamente con el nombre de la propiedad. var propertyInfo = settingsType.GetProperty(masterListNameString); if (propertyInfo == null) // La propiedad no se encontró (el string no coincide con un nombre de propiedad). return $"Error: Propiedad '{masterListNameString}' no encontrada en ListSettings."; // 3. Obtener la instancia de ListSettingItem (el valor) de esa propiedad, // usando la instancia 'settings' proporcionada. if (propertyInfo.GetValue(_listSettings) is not ListSettingItem listSettingItem) // Esto no debería suceder si la clase ListSettings está bien definida, // pero es una buena práctica comprobarlo. return "Error: El valor de la propiedad no es un ListSettingItem."; // 4. Devolver el valor de ManualObservationName. return listSettingItem.ManualObservationName!; } private enum OnArchiveAction { Delete, Archive } }