Files
2026-06-26 10:29:23 +02:00

3667 lines
189 KiB
C#

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;
/// <summary>
/// Provides a concrete implementation of the <see cref="IPatientService"/> interface,
/// encapsulating patient-related service operations.
/// </summary>
public class PatientService : IPatientService
{
private readonly Lazy<IAdmissionService> _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<PatientService> _logger;
private readonly IMasterListServiceFactory _masterListServiceFactory;
private readonly Lazy<IObservationService> _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<IPumpService> _pumpService;
private readonly bool _pushPatientWithOru;
private readonly IRecordingAlertService _recordingAlertService;
private readonly List<string> _sendingFacility;
private readonly ISubscriberGroupedService _subscriberGroupedService;
private readonly ISubscribersService _subscribersService;
private readonly Lazy<ITreatmentService> _treatmentService;
private readonly IUnitService _unitService;
private readonly bool _updatePatientDataWithAdtA02;
private readonly bool _updatePatientDataWithOru;
private readonly bool _updatePatientLocationWithOru;
public PatientService(
IPatientRepository patientRepository,
IPatientArchiveRepository patientArchiveRepository,
Lazy<IObservationService> observationService,
Lazy<ITreatmentService> treatmentService,
IPoCMappingService pocMappingService,
IDiagnosisService diagnosisService,
IAppointmentService appointmentService,
Lazy<IPumpService> pumpService,
IRecordingAlertService recordingAlertService,
IDischargeService dischargeService,
IOptions<ApiSettings> apiSettings,
IOptions<ListSettings> listSettings,
ILogger<PatientService> logger,
IClientMessageService clientMessageService,
ISubscribersService subscribersService,
ISubscriberGroupedService subscriberGroupedService,
IUnitService unitService,
IDisplayService displayService,
IPointOfCareService pointOfCareService,
Lazy<IAdmissionService> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">The patient entity to be inserted into the repository.</param>
public async Task Insert(Patient patient)
{
if (!CheckPatient(patient))
return;
await _patientRepository.InsertOneAsync(patient);
_ = SendNewPatientBroadcast(patient);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patientId">The string representation of the patient identifier to parse and look up.</param>
/// <param name="localeEnum">The locale used to translate the patient data via the master list service.</param>
/// <returns>A task containing the translated patient, the untranslated patient if the unit is not found, or null if the patient has no associated unit.</returns>
public async Task<Patient?> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">The patient entity to be inserted into the system.</param>
public async Task InsertAsync(Patient patient)
{
if (!CheckPatient(patient))
return;
await _patientRepository.InsertOneAsync(patient);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, patient);
_ = SendNewPatientBroadcast(patient);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">The patient to be moved to the new point of care.</param>
/// <param name="newPocId">The identifier of the destination point of care.</param>
/// <param name="oldPocId">The identifier of the source point of care that the patient is leaving.</param>
/// <returns>A task that resolves to <c>true</c> when the patient is successfully moved; otherwise, <c>false</c> 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.</returns>
public async Task<bool> 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;
}
/// <summary>
/// Retrieves a patient by their unique identifier, optionally enriching the result with location details
/// (point of care, bed, room, and unit) when available. Returns <c>null</c> if the identifier is empty
/// or the patient cannot be found.
/// </summary>
/// <param name="id">The unique identifier of the patient to retrieve.</param>
/// <param name="withLocation">When <c>true</c>, enriches the patient with location information such as bed, room, and unit name.</param>
/// <returns>A <see cref="Task{Patient}"/> containing the found patient, or <c>null</c> if no patient matches the identifier or the identifier is empty.</returns>
/// <exception cref="NotFoundException">Thrown when the patient has an associated point of care that cannot be found.</exception>
public async Task<Patient?> 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;
}
/// <summary>
/// Retrieves a <see cref="Patient"/> 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 <see langword="null"/> 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.
/// </summary>
/// <param name="location">The patient location containing the unit name and bed used to locate the patient.</param>
/// <returns>A <see cref="Patient"/> if one is found for the given location; otherwise, <see langword="null"/>.</returns>
public async Task<Patient?> 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);
}
/// <summary>
/// Retrieves a patient by their point of care identifier by delegating the lookup to the patient repository.
/// Returns <c>null</c> when no matching patient is found.
/// </summary>
/// <param name="pocId">The point of care identifier used to locate the patient.</param>
/// <returns>A <see cref="Patient"/> if one is found with the specified point of care identifier; otherwise, <c>null</c>.</returns>
public async Task<Patient?> FindByPointOfCareId(ObjectId pocId)
{
return await _patientRepository.FindByPointOfCareId(pocId);
}
/// <summary>
/// Asynchronously retrieves the total number of patients associated with the specified unit identifier by delegating to the patient repository.
/// </summary>
/// <param name="unitId">The <see cref="ObjectId"/> of the unit whose patients should be counted.</param>
/// <returns>A <see cref="Task{TResult}"/> that resolves to the number of patients linked to the given unit.</returns>
public async Task<long> CountPatientsByUnitId(ObjectId unitId)
{
return await _patientRepository.CountByUnitId(unitId);
}
/// <summary>
/// Archives a patient according to the configured <see cref="OnArchiveAction"/>: 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.
/// </summary>
/// <param name="patient">The patient to archive. If its <c>DisTime</c> is null, it is set to the current UTC time before further processing.</param>
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);
}
}
/// <summary>
/// Archives or deletes all data associated with a patient based on the configured <c>OnArchiveAction</c>. When the action is set to <c>Archive</c> (the default), patient observations, treatments, diagnoses, appointments, pumps, recording alerts, and care plans are archived; when set to <c>Delete</c>, observations, treatments, diagnoses, appointments, pumps, and recording alerts are deleted (care plan archival is not performed in this case).
/// </summary>
/// <param name="patientid">The <see cref="ObjectId"/> of the patient whose related data should be archived or deleted.</param>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">The patient entity to update.</param>
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);
}
}
/// <summary>
/// Updates the location of a patient identified by <paramref name="id"/>, moving them to the specified
/// <paramref name="location"/>. 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.
/// </summary>
/// <param name="id">The identifier of the patient whose location will be updated.</param>
/// <param name="location">The new patient location, or <c>null</c> to abort the update.</param>
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<VirtualPointOfCare>(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<ObjectId> { 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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="id">The unique identifier of the patient whose attending doctor is being updated.</param>
/// <param name="doctor">The new attending doctor to assign to the patient.</param>
/// <exception cref="ConflictException">Thrown when the patient cannot be found by id before or after the update operation.</exception>
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);
}
/// <summary>
/// Updates the data of an existing patient identified by <paramref name="id"/>, optionally updating the patient number when <paramref name="updatePatientNumber"/> is <c>true</c>. 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.
/// </summary>
/// <param name="id">The unique identifier of the patient to update.</param>
/// <param name="patientNumber">The patient number associated with the patient.</param>
/// <param name="data">The new <see cref="Person"/> data to apply to the patient.</param>
/// <param name="updatePatientNumber">When <c>true</c>, the patient number is also updated; otherwise only the personal data is changed.</param>
/// <exception cref="ConflictException">Thrown when the patient cannot be found by <paramref name="id"/> either before or after the update operation.</exception>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="id">The ObjectId of the patient to update.</param>
/// <param name="patientNumber">The patient number associated with the patient.</param>
/// <param name="patient">The patient entity containing the updated data.</param>
/// <param name="updatePatientNumber">Indicates whether the patient number should be updated as part of the operation.</param>
/// <exception cref="ConflictException">Thrown when the patient cannot be found before or after the update operation.</exception>
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);
}
/// <summary>
/// Retrieves a patient by their unique patient identifier, optionally enriching the result with location details such as bed, room, and unit information.
/// </summary>
/// <param name="patientId">The unique identifier of the patient to retrieve.</param>
/// <param name="withLocation">When <c>true</c>, additional lookups are performed to populate the patient's bed, room, and unit information; otherwise only the patient record is returned.</param>
/// <returns>The <see cref="Patient"/> matching the specified identifier, or <c>null</c> if no patient is found.</returns>
public async Task<Patient?> 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;
}
/// <summary>
/// Retrieves a patient by their unique patient number, optionally enriching the result with location details such as bed, room, and unit name when the <paramref name="withLocation"/> flag is enabled.
/// </summary>
/// <param name="patientNumber">The unique identifier of the patient to look up.</param>
/// <param name="withLocation">When set to <c>true</c>, 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.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the matching <see cref="Patient"/> if found; otherwise, <c>null</c>.</returns>
public async Task<Patient?> 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;
}
/// <summary>
/// Retrieves an archived patient by their unique patient number from the archive repository.
/// Returns null if no archived patient matches the provided patient number.
/// </summary>
/// <param name="patientNumber">The unique patient number used to look up the archived patient.</param>
/// <returns>The archived <see cref="Patient"/> if found; otherwise, <c>null</c>.</returns>
public async Task<Patient?> FindByPatientNumberArchived(string patientNumber)
{
return await _patientArchiveRepository.FindByPatientNumber(patientNumber);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="date">The cutoff date; patients with no observations updated after this date will be archived.</param>
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<Patient>();
var notFoundPatients = new HashSet<ObjectId>();
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;
}
}
/// <summary>
/// Archives discharged patients whose discharge time is older than the specified number of hours.
/// Only patients with a non-null <c>DisTime</c> earlier than the calculated cutoff date are processed,
/// and any error encountered during archiving is logged rather than propagated.
/// </summary>
/// <param name="hoursBeforeArchive">The number of hours that must elapse after a patient's discharge time before they are eligible for archiving.</param>
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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patientId">The unique patient identifier used as a secondary lookup criterion when the patient number is not provided or yields no result.</param>
/// <param name="patientNumber">The patient number used as the primary lookup criterion.</param>
/// <param name="location">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.</param>
/// <param name="findPatientByLocation">When <c>true</c>, allows a location-based lookup even if the corresponding configuration option is disabled.</param>
/// <returns>The matching <see cref="Patient"/> if found by any of the attempted criteria, or <c>null</c> when no patient can be located.</returns>
public async Task<Patient?> 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;
}
/// <summary>
/// Asynchronously retrieves the patient associated with the specified point of care, returning null if an error occurs during the lookup.
/// </summary>
/// <param name="item">The point of care used to locate the associated patient.</param>
/// <param name="observations">Indicates whether related observations should be included in the result.</param>
/// <param name="filterObservations">Optional list of observation identifiers used to filter observations when they are included.</param>
/// <returns>A task that resolves to the matching <see cref="Patient"/>, or null if the lookup fails.</returns>
public async Task<Patient?> GetByPointOfCare(PointOfCare item, bool observations = false,
List<string>? filterObservations = null)
{
try
{
return await _patientRepository.FindByPointOfCareId(item.Id);
}
catch (Exception ex)
{
Debug.WriteLine("ERROR: " + ex.Message);
return null;
}
}
/// <summary>
/// Retrieves a patient associated with the specified point of care, optionally returning a locale-translated version when both unit and locale are provided.
/// </summary>
/// <param name="item">The point of care used to look up the patient.</param>
/// <param name="unit">The unit used to determine the translation; when <c>null</c>, the patient is returned without translation.</param>
/// <param name="localeEnum">The target locale for translation; when <c>null</c>, the patient is returned without translation.</param>
/// <returns>The matching <see cref="Patient"/>, or <c>null</c> if the patient is not found or an error occurs.</returns>
public async Task<Patient?> 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;
}
}
/// <summary>
/// Retrieves a <see cref="Box"/> populated with information for the given point of care, including the associated patient when one exists, and optionally the patient's most recent observations.
/// </summary>
/// <param name="poc">The point of care used to locate the associated patient and to populate the box unit and bed.</param>
/// <param name="observations">When true, the most recent observations for the patient are loaded and mapped by name; when false, only patient data is returned.</param>
/// <param name="filterObservations">Optional list of observation names used to restrict which observations are retrieved; applies only when <paramref name="observations"/> is true.</param>
/// <returns>A <see cref="Task{Box}"/> containing the populated box. The <c>HasPatient</c> flag is set to false if no patient is found for the point of care, and the box's <c>Observations</c> are populated only when requested.</returns>
public async Task<Box?> GetBox(PointOfCare poc, bool observations = false,
List<string>? 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);
}
}
/// <summary>
/// Broadcasts a new patient notification to all subscribers whose registered locations include the patient's point of care.
/// </summary>
/// <param name="patient">The patient whose arrival should be broadcast to matching subscribers.</param>
/// <returns>A completed task once the broadcast has been dispatched.</returns>
public Task SendNewPatientBroadcast(Patient patient)
{
var subscribers = new List<WsSubscriber>();
// 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">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.</param>
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<WsSubscriber> subscribers = group;
foreach (var subscriber in subscribers)
{
var patientWithLocale = await _masterListServiceFactory.GetPatientTraslated(unit, locale, patient);
_ = _clientMessageService.SendAsync(subscriber.Id, OperationType.UpdatePatient, patientWithLocale);
}
}
}
/// <summary>
/// Asynchronously saves the specified API request by delegating the operation to a background task.
/// </summary>
/// <param name="apiRequest">The API request to be saved.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous save operation.</returns>
public Task SaveRequestAsync(ApiRequest apiRequest)
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="apiRequest">The API request containing the ADT type, patient identifiers, locations, and related clinical data to be processed.</param>
/// <exception cref="ApiRequestException">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.</exception>
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);
}
}
/// <summary>
/// Creates a new patient from the provided API request, populates its fields, persists it to the repository, and records an audit log entry.
/// </summary>
/// <param name="apiRequest">The API request containing the data used to initialize the new patient.</param>
/// <param name="ignoreLocation">When <c>true</c>, location information from the request is ignored during the patient update.</param>
/// <returns>The newly created patient.</returns>
public async Task<Patient?> 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;
}
/// <summary>
/// Finds or creates a <see cref="Patient"/> based on the contents of an <see cref="ApiRequest"/>, 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.
/// </summary>
/// <param name="apiRequest">The incoming API request containing the patient identifier, location, facility, request type, and optional person/doctor information used to resolve or create the patient.</param>
/// <returns>A <see cref="Task{Patient}"/> that resolves to the resolved or newly created <see cref="Patient"/>, or <c>null</c> 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.</returns>
public async Task<Patient?> 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<VirtualPointOfCare>(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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="withLocation">When <c>true</c>, populates each patient's <see cref="Patient.Bed"/>, <see cref="Patient.Room"/>, and <see cref="Patient.UnitString"/> by looking up the related point of care and unit; when <c>false</c>, returns the patients without performing those lookups.</param>
/// <returns>A task containing the list of patients, with location fields populated when <paramref name="withLocation"/> is <c>true</c> and the corresponding identifiers are present.</returns>
public async Task<List<Patient>> 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;
}
/// <summary>
/// Asynchronously retrieves a list of inactive patients of care (PoC) by delegating to the patient repository.
/// </summary>
/// <returns>A task representing the asynchronous operation, containing a list of inactive <see cref="Patient"/> records.</returns>
public async Task<List<Patient>> FindInActivePoC()
{
return await _patientRepository.FindInActivePoC();
}
/// <summary>
/// Retrieves a list of patients currently in active Point of Care (PoC).
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing the list of patients in active PoC.</returns>
public async Task<List<Patient>> FindInInactivePoC()
{
return await _patientRepository.FindInInactivePoC();
}
/// <summary>
/// Retrieves a paginated list of patients based on the specified filter, returning both the page data and the total document count.
/// </summary>
/// <param name="filter">The pagination filter containing the page number and page size used to calculate the skip and limit for the query.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="PaginationResponse{Patient}"/> with the patients for the requested page, along with the total count and pagination metadata.</returns>
public async Task<PaginationResponse<Patient>> 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<Patient>(dataList, filter.PageNumber, filter.PageSize, count);
}
/// <summary>
/// Asynchronously retrieves a <see cref="Patient"/> associated with the specified unit and point of care identifier.
/// Returns <see langword="null"/> if no matching patient is found.
/// </summary>
/// <param name="unit">The identifier of the unit to search by.</param>
/// <param name="pointOfCare">The identifier of the point of care to search by.</param>
/// <returns>A <see cref="Patient"/> if a match is found; otherwise, <see langword="null"/>.</returns>
public async Task<Patient?> FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare)
{
return await _patientRepository.FindByUnitAndPocId(unit, pointOfCare);
}
/// <summary>
/// Asynchronously retrieves the list of patients associated with the specified point of care by delegating to the patient repository.
/// </summary>
/// <param name="pointOfCare">The point of care identifier used to filter patients.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of <see cref="Patient"/> entities matching the specified point of care.</returns>
public async Task<List<Patient>> FindByPointOfCare(string pointOfCare)
{
return await _patientRepository.FindByPointOfCare(pointOfCare);
}
/// <summary>
/// Merges the data of an existing patient identified by <paramref name="oldPatientNumber"/> into the provided <paramref name="patient"/>,
/// 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.
/// </summary>
/// <param name="patient">The target patient that will absorb the old patient's data.</param>
/// <param name="oldPatientNumber">The patient number of the patient to be merged into <paramref name="patient"/>.</param>
/// <returns>A task representing the asynchronous merge operation.</returns>
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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sinceDate">The cutoff date used to archive patients who have not had any observations since this time.</param>
/// <param name="hoursBeforeArchive">The number of hours of inactivity used to determine which discharged and inactive point-of-care patients should be archived.</param>
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);
}
/// <summary>
/// Updates an existing patient record in the repository and creates an audit log capturing both the previous and updated states.
/// </summary>
/// <param name="updatedPatient">The patient object containing the updated information, identified by its Id.</param>
/// <returns>The updated patient, or null if no matching patient was found or the update did not produce a result.</returns>
public async Task<Patient?> 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;
}
/// <summary>
/// Updates the altable option of a patient, creating or removing the related discharge record when the altable type changes to or from "NotAltable".
/// </summary>
/// <param name="patientId">The identifier of the patient whose altable option will be updated.</param>
/// <param name="altable">The new altable option to assign to the patient.</param>
/// <param name="user">The user performing the operation, used to attribute the generated nurse observation.</param>
/// <returns>The updated <see cref="Patient"/>, or <c>null</c> if no patient was found with the specified id.</returns>
public async Task<Patient?> 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;
}
/// <summary>
/// Delete discharge and archive patient by id.
/// </summary>
/// <param name="id"></param>
/// <param name="archivePatient"></param>
/// <returns></returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose master list will be updated.</param>
/// <param name="typeName">The type of master list to update, which determines which patient property is modified and whether care plans or observations are generated.</param>
/// <param name="updatedOptions">The new list of options to assign to the patient for the specified master list type.</param>
/// <param name="user">The user performing the update, used for audit purposes and observation generation.</param>
/// <param name="carePlanLog">An optional list of care plan options used when generating care plans for Treatment, Procedure, and Test list types.</param>
/// <returns>The updated <see cref="Patient"/> if the operation succeeds; otherwise, <c>null</c> if the patient is not found or an exception is caught.</returns>
public async Task<Patient?> UpdatePatientMasterList(ObjectId patientId, MasterListType typeName,
List<OptionList> updatedOptions, User? user, List<OptionList>? 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;
}
}
/// <summary>
/// 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).
/// </summary>
/// <param name="carePlanType">The type of care plan to process, selecting between procedures, tests, or treatments.</param>
/// <param name="options">The list of options to evaluate against the patient's current items; may be null.</param>
/// <param name="patient">The patient whose care plan is being generated.</param>
/// <param name="user">The user performing the action; may be null.</param>
public async Task GenerateNurseCarePlanAndInsert(MasterListType carePlanType, List<OptionList>? 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);
}
}
/// <summary>
/// Updates the incoming data of an existing patient, records an audit log entry for the change, and broadcasts the updated patient information.
/// </summary>
/// <param name="patientId">The unique identifier of the patient to update.</param>
/// <param name="person">The patient object containing the updated incoming data.</param>
/// <exception cref="ConflictException">Thrown when the patient cannot be found by the given identifier, or when the incoming data update operation fails.</exception>
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);
}
/// <summary>
/// Updates the patient record with incoming data and persists the income data as a nurse observation associated with the patient and the current user.
/// </summary>
/// <param name="patientId">The unique identifier of the patient being updated.</param>
/// <param name="person">The patient entity containing the updated information to apply.</param>
/// <param name="patientIncomeData">The patient income information to store as a new observation.</param>
/// <param name="user">The user performing the operation, recorded as the author of the observation.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="opt">The master list option update data to apply to the patients.</param>
/// <param name="unitList">The collection of units whose associated patients will have the master list option updated.</param>
/// <param name="typeName">The name of the master list type used to identify which option to update.</param>
public async Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable<Unit> 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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="opt">The master list option to be removed from patient records.</param>
/// <param name="unitList">The collection of units whose associated patients will be updated.</param>
/// <param name="typeName">The name of the master list type from which the option is being deleted.</param>
public async Task DeletePatientMasterListItem(OptionList opt, IEnumerable<Unit> 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<Field> { new() { Name = obsName } });
if (obs.FirstOrDefault() != null)
{
var newObs = obs.First().Value as List<OptionList>;
newObs?.RemoveAll(c => c.Id == opt.Id);
await GenerateNurseObsAndInsert(obsName, newObs, patient.Id, null);
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patientId">The unique identifier of the patient to update.</param>
/// <param name="person">The patient object containing the updated demographic data.</param>
/// <param name="user">The user performing the update, used for audit logging; may be null.</param>
/// <exception cref="ConflictException">Thrown when no patient is found for the specified patient ID.</exception>
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<OptionList> { 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);
}
}
/// <summary>
/// Searches for a patient by their patient number within a specific unit, ensuring the patient belongs to a different unit (distinct).
/// </summary>
/// <param name="patientNumber">The patient number to search for.</param>
/// <param name="unitId">The identifier of the unit used to find a patient that is distinct from it.</param>
/// <returns>A task containing the found <see cref="Patient"/>, or <c>null</c> if no matching patient is found.</returns>
public Task<Patient?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId)
{
return _patientRepository.SearchByPatientNumberAndDistinctUnit(patientNumber, unitId);
}
/// <summary>
/// Asynchronously retrieves all patients whose procedures have finished, using the specified time threshold to determine which finished procedures are eligible for archival.
/// </summary>
/// <param name="archiveProcedureEndDateAfterMinutes">The minimum age, in minutes, of a finished procedure's end date used as the cutoff for including patients in the result.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Patient"/> instances associated with finished procedures matching the specified threshold.</returns>
public async Task<List<Patient>> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes)
{
return await _patientRepository.FindAllPatientWithFinishedProcedures(archiveProcedureEndDateAfterMinutes);
}
/// <summary>
/// Asynchronously retrieves all patients whose tests have finished, filtering based on the archive test end date threshold.
/// </summary>
/// <param name="archiveTestEndDateAfterMinutes">The number of minutes after the archive test end date used to determine which finished tests to include.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of patients with finished tests.</returns>
public async Task<List<Patient>> FindAllPatientWithFinishedTest(int archiveTestEndDateAfterMinutes)
{
return await _patientRepository.FindAllPatientWithFinishedTests(archiveTestEndDateAfterMinutes);
}
/// <summary>
/// Retrieves all patients whose treatments have finished, where the treatment ended more than the specified number of minutes ago, typically for archiving purposes.
/// </summary>
/// <param name="archiveTreatmentEndDateAfterMinutes">The minimum number of minutes that must have elapsed since the treatment end date for a patient to be included in the result.</param>
/// <returns>A task representing the asynchronous operation, containing a list of patients with finished treatments matching the archive criteria.</returns>
public async Task<List<Patient>> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes)
{
return await _patientRepository.FindAllPatientWithFinishedTreatment(archiveTreatmentEndDateAfterMinutes);
}
/// <summary>
/// Validates whether a patient can be inserted by ensuring required identifiers are present.
/// Returns false if both <see cref="PointOfCareId"/> and <see cref="UnitId"/> are null, or if <see cref="PatientNumber"/> is null/empty and creation without a patient number is not allowed.
/// </summary>
/// <param name="patient">The patient instance to validate before insertion.</param>
/// <returns><c>true</c> if the patient passes all validation checks; otherwise, <c>false</c>.</returns>
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;
}
/// <summary>
/// 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 <c>DisplayId</c>, and only those whose display configuration defines a grouped
/// field list receive the generated observations.
/// </summary>
/// <param name="newPoc">The identifier of the new point of care used to look up the relevant subscribers.</param>
/// <param name="oldPoc">The optional identifier of the previous point of care; when present, subscribers still associated
/// with it but not with <paramref name="newPoc"/> are removed from the subscription group.</param>
/// <param name="patient">The patient whose grouped observations are generated and dispatched to the subscribers.</param>
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);
}
}
/// <summary>
/// Asynchronously sends the most recent observations to all subscribers associated with the specified Point of Care.
/// For each subscriber with a valid <see cref="DisplayId"/>, 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 <see cref="DisplayId"/> or whose display lacks a <c>DisplayConfigId</c> are skipped.
/// </summary>
/// <param name="newPoc">The Point of Care used to look up the list of relevant subscribers.</param>
/// <param name="patient">The patient whose last observations are retrieved and forwarded to the subscribers.</param>
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);
}
}
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="id">The identifier of the patient whose location will be updated.</param>
/// <param name="unitId">The identifier of the new unit the patient is being moved to.</param>
/// <param name="pocId">The identifier of the new point-of-care the patient is being moved to.</param>
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<VirtualPointOfCare>(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);
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="newLocation">New location where patient is getting updated</param>
/// <param name="patientId">Object Id for patient</param>
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}");
}
}
/// <summary>method <c>ArchiveNotUpdatedPatientsSince</c> archive all patients who have not updated since in
/// <param>hoursBeforeArchive</param>
/// .
/// </summary>
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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">The patient associated with the location update.</param>
/// <param name="location">The patient location information to broadcast.</param>
/// <param name="pocs">The list of point-of-care location identifiers used to filter eligible subscribers. If null or empty, no broadcast is performed.</param>
public Task SendPatientLocationBroadcast(object patient, PatientLocation location,
List<ObjectId>? 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<string, object>
{
{ "patient", patient },
{ "location", location }
});
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patientid">The unique identifier of the patient whose attending doctor assignment should be broadcast.</param>
/// <param name="attendingDoctor">The <see cref="Person"/> representing the doctor now attending the patient, included in the broadcast payload.</param>
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<string, object>
{
{ "id", patientid.ToString() },
{ "attendingDoctor", attendingDoctor }
});
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patientid">The unique identifier of the patient whose data will be broadcast.</param>
/// <param name="data">The person data payload to be sent to the matching subscribers.</param>
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<string, object>
{
{ "id", patientid.ToString() },
{ "data", data }
});
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">The patient whose information is being broadcast; its <c>PointOfCareId</c> is used to match subscribers and its <c>UnitId</c> is used to resolve the unit for translation.</param>
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<WsSubscriber> subscribers = group;
foreach (var subscriber in subscribers)
{
var patientWithLocale = await _masterListServiceFactory.GetPatientTraslated(unit, locale, patient);
_ = _clientMessageService.SendAsync(subscriber.Id, OperationType.UpdatePatient, patientWithLocale);
}
}
}
/// <summary>
/// Broadcasts a delete-patient notification to all subscribers linked to the specified point of care location, after removing the patient's grouped observations.
/// </summary>
/// <param name="patientid">The unique identifier of the patient whose deletion will be broadcast.</param>
/// <param name="pocId">The point of care location identifier used to filter the targeted subscribers.</param>
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<string, object>
{
{ "id", patientid.ToString() }
});
}
}
/// <summary>
/// Updates a <see cref="Patient"/> entity from the data contained in an <see cref="ApiRequest"/>,
/// applying only the fields that are explicitly provided (admission time, discharge time, patient ID,
/// and demographic data) and creating the associated <see cref="Person"/> when missing.
/// Patient identifiers are synchronized via <see cref="Person.SetIds"/> using the request's message time,
/// falling back to the current UTC time when the message time is <see cref="DateTime.MinValue"/>.
/// </summary>
/// <param name="patient">The patient entity to be updated in place.</param>
/// <param name="apiRequest">The API request providing the source data for the update.</param>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="apiRequest">The API request containing operational data such as the discharge time used to set the patient's discharge timestamp.</param>
/// <param name="patient">The patient to be moved, updated with the new point of care, unit, and location information.</param>
/// <param name="virtualPocEnum">The virtual point of care identifier used to look up the destination bed, room, and unit.</param>
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
/// <summary>
/// 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.
/// </summary>
/// <param name="apiRequest">The API request containing the list of ICCA patients to synchronize with the local database.</param>
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
*/
/// <summary>
/// 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.
/// </summary>
/// <param name="apiRequest">The API request containing the patient number, patient data, and target location information used for the move operation.</param>
/// <param name="patient">The optional patient to be moved; if null, a new patient is created from the request.</param>
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);
}
}
/// <summary>
/// Process new patient. try to find him by
/// <paramref name="apiRequest.patientNumber"></paramref>
/// or
/// <paramref name="apiRequest.patientId"></paramref>
/// if exits and was not discharged use him.
/// If not insert new one in patients table
/// </summary>
/// <param name="apiRequest"></param>
/// <returns>The Inserted patient or null.</returns>
private async Task<Patient?> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">The patient whose properties will be updated with the values from the admission.</param>
/// <param name="patientInAdmission">The admission record providing the source values to be merged into the patient.</param>
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;
}
/// <summary>
/// Updates an existing <see cref="Patient"/> entity from the data contained in an <see cref="ApiRequest"/>,
/// 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.
/// </summary>
/// <param name="patient">The patient entity to mutate with values coming from the request.</param>
/// <param name="apiRequest">The incoming API request whose non-empty fields are applied to the patient.</param>
/// <param name="ignoreLocation">When <c>true</c>, skips unit and point-of-care lookups and any location-based updates.</param>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">The patient whose location will be updated.</param>
/// <param name="apiRequest">The API request containing the new location information.</param>
private async Task ProcessUpdatePatientLocationWithOru(Patient patient, ApiRequest apiRequest)
{
await UpdateLocation(patient.Id, apiRequest.Location);
_ = SendPatientUpdateBroadcast(patient);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="patient">The incoming patient to be updated with merged data.</param>
/// <param name="oldPatient">The previous patient record whose missing treatments, procedures and tests are preserved during the merge.</param>
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<OptionList> visitList
? visitList.FirstOrDefault()
: null;
patient.AccessControl =
obs.FirstOrDefault(o => o.Name == _listSettings.AccessControlList.ManualObservationName)?.Value is
List<OptionList> accessControList
? accessControList.FirstOrDefault()
: null;
patient.TherapeuticCeiling =
obs.FirstOrDefault(o => o.Name == _listSettings.TherapeuticCeilingList.ManualObservationName)?.Value is
List<OptionList> ceilingList
? ceilingList.FirstOrDefault()
: null;
patient.Mobility =
obs.FirstOrDefault(o => o.Name == _listSettings.MobilityOptionList.ManualObservationName)?.Value is
List<OptionList> mobilityList
? mobilityList.FirstOrDefault()
: null;
patient.PassiveSitting =
obs.FirstOrDefault(o => o.Name == _listSettings.PassiveSittingList.ManualObservationName)?.Value is
List<OptionList> passiveList
? passiveList.FirstOrDefault()
: null;
patient.LanguageBarrier =
obs.FirstOrDefault(o => o.Name == _listSettings.LanguageBarrierList.ManualObservationName)?.Value as List<OptionList>;
patient.Insulation =
obs.FirstOrDefault(o => o.Name == _listSettings.InsulationList.ManualObservationName)?.Value is
List<OptionList> insulationList
? insulationList.FirstOrDefault()
: null;
patient.Doctors =
obs.FirstOrDefault(o => o.Name == _listSettings.DoctorList.ManualObservationName)
?.Value as List<OptionList>;
patient.Allergies =
obs.FirstOrDefault(o => o.Name == _listSettings.AllergyList.ManualObservationName)?.Value as
List<OptionList>;
patient.Altable =
obs.FirstOrDefault(o => o.Name == _listSettings.AltableOptionList.ManualObservationName)?.Value is
List<OptionList> 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);
}
/// <summary>
/// Generates a nurse observation for the specified patient and inserts it using the observation service. If <paramref name="value"/> is null, an empty list is used; if <paramref name="user"/> is null, the observation's user is left unset.
/// </summary>
/// <param name="name">The name of the observation to generate.</param>
/// <param name="value">The list of option values for the observation, or null to default to an empty list.</param>
/// <param name="param name="patientId">The identifier of the patient associated with the observation.</param>
/// <param name="user">The user creating the observation, whose identifier is assigned to the observation; may be null.</param>
private async Task GenerateNurseObsAndInsert(string name, List<OptionList>? value, ObjectId patientId, User? user)
{
var obs = GenerateObs(
name,
value ?? [],
patientId);
obs.UserId = user?.Id;
await _observationService.Value.InsertNurseObservation(obs);
}
/// <summary>
/// Generates a <see cref="PatientObservation"/> for a specific patient using manual insertion mode and the current timestamp.
/// If <paramref name="value"/> is <c>null</c>, an empty list is used as the observation value.
/// </summary>
/// <param name="name">The name of the observation.</param>
/// <param name="value">The list of <see cref="OptionList"/> values for the observation, or <c>null</c> to use an empty list.</param>
/// <param name="patientId">The identifier of the patient associated with the observation.</param>
/// <returns>A new <see cref="PatientObservation"/> populated with the provided data, the current date/time, and manual insert mode.</returns>
private PatientObservation GenerateObs(string name, List<OptionList>? value, ObjectId patientId)
{
var valueList = value ?? [];
return new PatientObservation
{
Name = name,
Value = valueList,
PatientId = patientId,
Time = DateTime.Now,
InsertMode = ObservationEnum.InsertMode.Manual
};
}
/// <summary>
/// Retrieves the manual observation name associated with a given master list name by looking up the corresponding property in the <see cref="ListSettings"/> class via reflection and returning its <c>ManualObservationName</c>. Returns a localized error message string when the property is not found or when the resolved value is not a <see cref="ListSettingItem"/>.
/// </summary>
/// <param name="masterListNameString">The name of the property in <see cref="ListSettings"/> whose associated manual observation name should be returned. Must match the property name exactly.</param>
/// <returns>The <c>ManualObservationName</c> of the resolved <see cref="ListSettingItem"/>, or a descriptive error string if the property is not found or the property value is not a <see cref="ListSettingItem"/>.</returns>
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
}
}