Files
adas-core/adas-core.Infrastructure/Repositories/PatientRepository.cs
T

1247 lines
67 KiB
C#

using adas_core.Application.Repositories.Interfaces;
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.Masters;
using adas_core.Domain.Models.MongoModels;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Serilog;
using System.Text.RegularExpressions;
using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils;
namespace adas_core.Infrastructure.Repositories;
/// <summary>
/// Provides a MongoDB-backed repository implementation for managing patient data, exposing patient-specific data access operations defined by the IPatientRepository contract.
/// </summary>
/// <!-- aidoc:v1 sig=c81c577 -->
public class PatientRepository : MongoRepository<Patient>, IPatientRepository
{
private readonly ApiSettings _apiSettings;
/// <summary>
/// Initializes a new instance of <see cref="PatientRepository"/>, a MongoDB-backed data repository, capturing API configuration from <see cref="IOptions{ApiSettings}"/> and forwarding the <see cref="IMongoDatabase"/> to the base repository constructor.
/// </summary>
/// <param name="apiSettings">The <see cref="IOptions{ApiSettings}"/> whose value supplies the repository's API configuration.</param>
/// <param name="database">The <see cref="IMongoDatabase"/> connection passed to the base class constructor.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="apiSettings"/> is null.</exception>
/// <!-- aidoc:v1 sig=113519f body=d2b18a3 -->
public PatientRepository(IOptions<ApiSettings> apiSettings, IMongoDatabase database) : base(database)
{
if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings));
_apiSettings = apiSettings.Value;
} //For testing
// public PatientRepository(IOptions<ApiSettings> apiSettings, IOptions<DatabaseSettings> dbSetting) :
// base(dbSetting)
// {
// if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings));
// _apiSettings = apiSettings.Value;
// }
/// <summary>
/// Gets the collection name for patients, returning the configured value from API settings
/// or falling back to the default "patients" if the setting is null.
/// </summary>
/// <returns>The configured patients collection name, or "patients" as a default when the setting is not set.</returns>
/// <!-- aidoc:v1 sig=94e22ff body=04f5507 -->
public override string GetCollectionName()
{
return _apiSettings.Patients ?? "patients";
}
/// <summary>
/// Asynchronously finds a patient by their unique identifier in the underlying collection.
/// Returns <c>null</c> if no matching patient is found or if an error occurs while querying, with the error being logged.
/// </summary>
/// <param name="id">The unique identifier of the patient to locate.</param>
/// <returns>A <see cref="Patient"/> instance matching the provided id, or <c>null</c> if not found or on error.</returns>
/// <!-- aidoc:v1 sig=f97bb0d body=2c27a66 -->
public async Task<Patient?> FindById(ObjectId id)
{
try
{
var filter = Builders<Patient>.Filter.Eq(p => p.Id, id);
var result = await Collection.FindAsync(filter);
return await result.FirstOrDefaultAsync();
}
catch (Exception ex)
{
Log.Error("Error searching patient by id: {id}. Exception: {ex}", id, ex);
return null;
}
}
/// <summary>
/// Retrieves a <see cref="Patient"/> matching the specified point of care identifier.
/// Returns the first matching patient, or <c>null</c> if no patient is found or an error occurs while querying the data store.
/// </summary>
/// <param name="id">The point of care identifier used to locate the patient.</param>
/// <returns>A <see cref="Patient"/> instance if a match is found; otherwise, <c>null</c>.</returns>
/// <!-- aidoc:v1 sig=8a4443e body=e3fe55b -->
public async Task<Patient?> FindByPointOfCareId(ObjectId id)
{
try
{
var filter = Builders<Patient>.Filter.Eq(p => p.PointOfCareId, id);
var result = await Collection.FindAsync(filter);
return await result.FirstOrDefaultAsync();
}
catch (Exception ex)
{
Log.Error("Error searching patient by id: {id}. Exception: {ex}", id, ex);
return null;
}
}
/// <summary>
/// Finds a patient by the specified unit identifier and point of care identifier.
/// Returns the first matching patient, or null if no match is found or if an error occurs during the search.
/// </summary>
/// <param name="unit">The identifier of the unit to filter by.</param>
/// <param name="pointOfCare">The identifier of the point of care to filter by.</param>
/// <returns>The first matching <see cref="Patient"/>, or null if no patient is found or an error is encountered.</returns>
/// <!-- aidoc:v1 sig=9b461f1 body=c98f5c9 -->
public async Task<Patient> FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare)
{
try
{
var filter = Builders<Patient>.Filter.And(
Builders<Patient>.Filter.Eq(p => p.PointOfCareId, pointOfCare),
Builders<Patient>.Filter.Eq(p => p.UnitId, unit)
);
var result = await Collection.FindAsync(filter);
return await result.FirstOrDefaultAsync();
}
catch (Exception ex)
{
Log.Error("Error searching patient by unitId: {unitid} pointOfCareid: {id}. Exception: {ex}", unit,
pointOfCare, ex);
return null!;
}
}
//Deprecated
/// <summary>
/// Finds a patient based on the provided location information. Returns null if the location is null or no matching patient is found. When both unit name and bed are specified, the search filters by both criteria; otherwise, it falls back to filtering by bed only, or returns the first available patient if neither is provided.
/// </summary>
/// <param name="location">The location information used to locate the patient. Can be null.</param>
/// <returns>A <see cref="Patient"/> matching the location criteria, or null if no match is found or the location is null.</returns>
/// <!-- aidoc:v1 sig=556acef body=7ff7fec -->
public async Task<Patient?> FindByLocation(PatientLocation? location)
{
if (location == null)
return null;
// Primero encontrar la unidad
var filterBuilder = Builders<Patient>.Filter;
var filter = filterBuilder.Empty;
if (!string.IsNullOrEmpty(location.UnitName) && !string.IsNullOrEmpty(location.Bed))
filter = filterBuilder.And(
filterBuilder.Eq(p => p.UnitString, location.UnitName),
filterBuilder.Eq(p => p.Bed, location.Bed)
);
else if (!string.IsNullOrEmpty(location.Bed)) filter = filterBuilder.Eq(p => p.Bed, location.Bed);
var result = await Collection.Find(filter).Limit(1).FirstOrDefaultAsync();
return result;
}
/// <summary>
/// Asynchronously inserts a new <see cref="Patient"/> record, setting its creation date to the current UTC time. If the insertion fails but an existing patient with the same patient number is found at the same location, the exception is logged as a warning; otherwise, the error is logged and rethrown.
/// </summary>
/// <param name="patient">The <see cref="Patient"/> entity to insert into the data store.</param>
/// <!-- aidoc:v1 sig=334af9d body=f6ea95d -->
public override async Task InsertOneAsync(Patient patient)
{
try
{
patient.CreationDate = DateTime.UtcNow;
await base.InsertOneAsync(patient);
}
catch (Exception e)
{
Console.WriteLine(e);
var patientAux = await FindByLocation(patient.Location);
if (patientAux != null && patientAux.PatientNumber == patient.PatientNumber)
{
Log.Warning("Exception trying to insert an existing patient: {patient}. Exception {e}", patient, e);
}
else
{
Log.Error("Exception trying to insert patient: {patient}. Exception {e}", patient, e);
throw;
}
}
}
/// <summary>
/// Updates an existing patient record, refreshing the update timestamp to the current UTC time before persisting the changes.
/// </summary>
/// <param name="patient">The patient entity containing the updated information to be saved.</param>
/// <!-- aidoc:v1 sig=f9d52dc body=06dccd6 -->
public async Task Update(Patient patient)
{
patient.UpdateDate = DateTime.UtcNow;
await UpdateOneAsync(patient.Id, patient);
}
/// <summary>
/// Deletes a patient from the collection that matches the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the patient to remove.</param>
/// <!-- aidoc:v1 sig=3de1ad6 body=60a9b9b -->
public async Task Delete(ObjectId id)
{
var filter = Builders<Patient>.Filter.Eq(x => x.Id, id);
await Collection.DeleteOneAsync(filter, null);
}
/// <summary>
/// Updates the location of an existing patient. If no patient is found for the given identifier, the operation is skipped and a warning is logged.
/// </summary>
/// <param name="id">The unique identifier of the patient whose location will be updated.</param>
/// <param name="location">The new location to assign to the patient.</param>
/// <!-- aidoc:v1 sig=a6d84a4 body=af93f9e -->
public async Task UpdateLocation(ObjectId id, PatientLocation location)
{
var patient = await FindById(id);
if (patient == null)
{
Log.Warning("Patient not found for updating location: {id}", id);
return;
}
patient.UpdateDate = DateTime.UtcNow;
patient.Location = location;
await UpdateOneAsync(patient.Id, patient);
/*var filterBuilder = Builders<Patient>.Filter;
var filter = filterBuilder.Eq(p => p.Id, id);
var update = Builders<Patient>.Update
.Set(p => p.Bed, location?.Bed)
.Set(p => p.UnitString, location?.UnitName)
.Set(p => p.UpdateDate, DateTime.UtcNow)
.AddToSet(p => p.HistoricalLocations, new KeyValuePair<string, PatientLocation>(DateTime.UtcNow.ToString("o"), location));
var result = await Collection.UpdateOneAsync(filter, update);
Log.Debug("Update location result: {result}", result);*/
}
/// <summary>
/// Updates the point of care (location) for the specified patient, also refreshing the modification timestamp to the current UTC time.
/// If no patient is found for the given identifier, a warning is logged and the method returns without making any changes.
/// </summary>
/// <param name="id">The unique identifier of the patient whose location will be updated.</param>
/// <param name="location">The new point of care (location) identifier to assign to the patient.</param>
/// <!-- aidoc:v1 sig=ca2f737 body=b5de0cf -->
public async Task UpdateLocation(ObjectId id, ObjectId location)
{
var patient = await FindById(id);
if (patient == null)
{
Log.Warning("Patient not found for updating location: {id}", id);
return;
}
patient.UpdateDate = DateTime.UtcNow;
patient.PointOfCareId = location;
await UpdateOneAsync(patient.Id, patient);
}
/// <summary>
/// Updates the attending doctor of a patient identified by the specified identifier and refreshes the update timestamp to the current UTC date and time.
/// </summary>
/// <param name="id">The unique identifier of the patient whose attending doctor is being updated.</param>
/// <param name="attendingDoctor">The new attending doctor to assign to the patient.</param>
/// <!-- aidoc:v1 sig=724f112 body=44fd27b -->
public async Task UpdateAttendingDoctor(ObjectId id, Person attendingDoctor)
{
var update = Builders<Patient>.Update
.Set(p => p.AttendingDoctor, attendingDoctor)
.Set(p => p.UpdateDate, DateTime.UtcNow);
await Collection.UpdateOneAsync(p => p.Id == id, update);
}
/// <summary>
/// Updates the personal data and update timestamp of an existing patient identified by the supplied id.
/// When <paramref name="updatePatientNumber"/> is true (the default), the patient number is also updated; otherwise only the person data and update date are persisted.
/// </summary>
/// <param name="id">The unique identifier of the patient to update.</param>
/// <param name="patientNumber">The new patient number to apply when <paramref name="updatePatientNumber"/> is true.</param>
/// <param name="data">The new <see cref="Person"/> information to store for the patient.</param>
/// <param name="updatePatientNumber">Indicates whether the patient number should be updated as part of the operation; defaults to true.</param>
/// <!-- aidoc:v1 sig=06ac587 body=114be5c -->
public async Task UpdatePatientData(ObjectId id, string patientNumber, Person data, bool updatePatientNumber = true)
{
var filterBuilder = Builders<Patient>.Filter;
var updateBuilder = Builders<Patient>.Update.Set(p => p.Person, data).Set(p => p.UpdateDate, DateTime.UtcNow);
var filter = filterBuilder.Eq(p => p.Id, id);
var update = updateBuilder;
if (updatePatientNumber) update = update.Set(p => p.PatientNumber, patientNumber);
await Collection.UpdateOneAsync(filter, update);
}
/// <summary>
/// Finds a <see cref="Patient"/> by <paramref name="patientNumber"/>, preferring the most recently admitted active patient (one whose <see cref="Patient.DisTime"/> is null) and falling back to the <see cref="Patient"/> record with the most recent non-null <see cref="Patient.DisTime"/> when no active admission exists. Returns null when <paramref name="patientNumber"/> is null, empty, or whitespace, or when no matching record is found.
/// </summary>
/// <param name="patientNumber">The patient number used to locate the <see cref="Patient"/> record.</param>
/// <returns>A <see cref="Task{Patient}"/> that resolves to the matching <see cref="Patient"/>, or null when no record is found.</returns>
/// <!-- aidoc:v1 sig=09c89ab body=91862bd -->
public async Task<Patient?> FindByPatientNumber(string patientNumber)
{
if (string.IsNullOrWhiteSpace(patientNumber)) return null;
//Último paciente admitido
var patient = await Collection.Find(Builders<Patient>.Filter.And(
Builders<Patient>.Filter.Eq(p => p.DisTime, null),
Builders<Patient>.Filter.Eq(p => p.PatientNumber, patientNumber)
))
.Sort(Builders<Patient>.Sort.Descending(p => p.AdmTime))
.Limit(1)
.FirstOrDefaultAsync();
//Último con fecha de alta más reciente
patient ??= await Collection.Find(Builders<Patient>.Filter.And(
Builders<Patient>.Filter.Ne(p => p.DisTime, null),
Builders<Patient>.Filter.Eq(p => p.PatientNumber, patientNumber)
))
.Sort(Builders<Patient>.Sort.Descending(p => p.DisTime))
.Limit(1)
.FirstOrDefaultAsync();
return patient;
}
/// <summary>
/// Searches for a <see cref="Patient"/> by <paramref name="patientNumber"/> whose <see cref="Patient.UnitId"/> differs from <paramref name="unitId"/>, intended to locate a patient identified at a Point of Care but registered in another unit. Returns <c>null</c> when the patient number is blank, when multiple matches are found (since the patient number may be incomplete), or when no match exists; exceptions are logged and also surface as <c>null</c>.
/// </summary>
/// <param name="patientNumber">The patient number used to look up the <see cref="Patient"/>.</param>
/// <param name="unitId">The <see cref="ObjectId"/> of the unit that must be excluded from the match.</param>
/// <returns>A <see cref="Task{Patient}"/> resolving to the matching <see cref="Patient"/>, or <c>null</c> when there is no unique match.</returns>
/// <!-- aidoc:v1 sig=382b7c8 body=f2b7752 -->
public async Task<Patient?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId)
{
try
{
if (string.IsNullOrWhiteSpace(patientNumber)) return null;
// Paciente ubicado en un PoC pero en diferente unidad
var patient = await Collection.Find(Builders<Patient>.Filter.And(
Builders<Patient>.Filter.Eq(p => p.PatientNumber, patientNumber),
Builders<Patient>.Filter.Ne(p => p.UnitId, unitId)
)).ToListAsync();
// Si hay mas de una coincidencia devolvemos null por que puede no estar completo el patientNumber
return patient.Count > 1 ? null : patient.FirstOrDefault();
}
catch (Exception e)
{
Log.Error(
"Error Search By Patient Number And Distinct Unit on patient repository patientNumber: {patientNumber}, unitId: {unitId}, Excepción: {e}",
patientNumber, unitId, e.Message);
return null;
}
}
/// <summary>
/// Retrieves all <see cref="Patient"/> documents that contain at least one procedure considered finished and eligible for archival. A procedure qualifies when its <c>EndDate</c> is not null and the time elapsed since that <c>EndDate</c> exceeds the supplied grace period of <paramref name="archiveProcedureEndDateAfterMinutes"/> minutes relative to the current UTC time.
/// </summary>
/// <param name="archiveProcedureEndDateAfterMinutes">The grace period, in minutes, added to a procedure's <c>EndDate</c>; the procedure is treated as finished only when the resulting timestamp is earlier than <see cref="DateTime.UtcNow"/>.</param>
/// <returns>A <see cref="Task{List{Patient}}"/> containing the patients matching the finished-procedure criteria, or an empty list when no patient has a procedure whose archival grace period has elapsed.</returns>
/// <!-- aidoc:v1 sig=8527a2e body=acd9bfe -->
public async Task<List<Patient>> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes)
{
var currentDateTime = DateTime.UtcNow;
// Filtro para documentos que contienen al menos un procedimiento con opción "procedure"
var filter = Builders<Patient>.Filter.ElemMatch(
x => x.Procedures,
procedure =>
//procedure.OptionType == "procedure" &&
procedure.EndDate.HasValue
);
// Ejecutar la consulta inicial y traer los documentos
var patientsWithProcedures = await Collection.Find(filter).ToListAsync();
// Aplicar el filtro adicional en memoria
var patientsWithFinishedProcedures = patientsWithProcedures.Where(patient =>
patient.Procedures != null &&
patient.Procedures.Any(procedure =>
procedure is
{
//OptionType: "procedure",
EndDate: not null
} &&
procedure.EndDate.Value.AddMinutes(archiveProcedureEndDateAfterMinutes) < currentDateTime
)
).ToList();
return patientsWithFinishedProcedures;
}
/// <summary>
/// Retrieves all <see cref="Patient"/> records whose tests have finished and whose end date, offset by the specified archive threshold, is earlier than the current UTC time.
/// The initial MongoDB filter keeps tests with a non-null EndDate, and the in-memory filter then retains only those whose EndDate plus <paramref name="archiveTestEndDateAfterMinutes"/> minutes is before <see cref="DateTime.UtcNow"/>.
/// </summary>
/// <param name="archiveTestEndDateAfterMinutes">The number of minutes added to each test's EndDate to determine whether the test is eligible for archival.</param>
/// <returns>A <see cref="Task{List{Patient}}"/> containing the patients whose tests meet the finished and archive criteria.</returns>
/// <!-- aidoc:v1 sig=c7d3a85 body=8559f5d -->
public async Task<List<Patient>> FindAllPatientWithFinishedTests(int archiveTestEndDateAfterMinutes)
{
var currentDateTime = DateTime.UtcNow;
// Filtro para documentos que contienen al menos un procedimiento con opción "procedure"
var filter = Builders<Patient>.Filter.ElemMatch(
x => x.Tests,
procedure => //procedure.OptionType == "test" &&
procedure.EndDate.HasValue
);
// Ejecutar la consulta inicial y traer los documentos
var patientsWithTests = await Collection.Find(filter).ToListAsync();
// Aplicar el filtro adicional en memoria
var patientsWithFinishedTests = patientsWithTests.Where(patient =>
patient.Tests != null &&
patient.Tests.Any(procedure =>
procedure is
{
//OptionType: "test",
EndDate: not null
} &&
procedure.EndDate.Value.AddMinutes(archiveTestEndDateAfterMinutes) < currentDateTime
)
).ToList();
return patientsWithFinishedTests;
}
/// <summary>
/// Asynchronously retrieves all patients that have at least one finished <see cref="Patient.Treatment"/> whose <see cref="Treatment.EndDate"/> is older than <paramref name="archiveTreatmentEndDateAfterMinutes"/> minutes relative to the current UTC time, using a MongoDB query combined with an in-memory time threshold filter.
/// </summary>
/// <param name="archiveTreatmentEndDateAfterMinutes">The grace period in minutes that must elapse after a treatment's <see cref="Treatment.EndDate"/> before the patient qualifies for retrieval.</param>
/// <returns>A <see cref="Task{List{Patient}}"/> that resolves to the list of <see cref="Patient"/> records whose treatments satisfy the finished-treatment time threshold.</returns>
/// <!-- aidoc:v1 sig=6b43cdf body=429c102 -->
public async Task<List<Patient>> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes)
{
var currentDateTime = DateTime.UtcNow;
// Filtro para documentos que contienen al menos un procedimiento con opción "procedure"
var filter = Builders<Patient>.Filter.ElemMatch(
x => x.Treatment,
treatment => treatment.EndDate.HasValue
);
// Ejecutar la consulta inicial y traer los documentos
var patientsWithTreatment = await Collection.Find(filter).ToListAsync();
// Aplicar el filtro adicional en memoria
var patientsWithFinishedTreatments = patientsWithTreatment.Where(patient =>
patient.Treatment != null &&
patient.Treatment.Any(treatment =>
treatment.EndDate.HasValue &&
treatment.EndDate.Value.AddMinutes(archiveTreatmentEndDateAfterMinutes) < currentDateTime
)
).ToList();
return patientsWithFinishedTreatments;
}
/// <summary>
/// Updates a master list option for all patients belonging to the specified <paramref name="unitIds"/>,
/// handling <see cref="MasterListType.DiagnosisList"/>, <see cref="MasterListType.DoctorList"/>,
/// and <see cref="MasterListType.OriginList"/> cases by updating the relevant fields and auxiliary fields,
/// and returning the updated <see cref="Patient"/> documents. If <paramref name="typeName"/> cannot be parsed
/// as a <see cref="MasterListType"/> or the type is not implemented, an empty list is returned.
/// </summary>
/// <param name="unitIds">The collection of unit identifiers used to scope the update to the affected patients.</param>
/// <param name="opt">The DTO containing the existing option and the replacement option values to apply.</param>
/// <param name="typeName">The textual name of the <see cref="MasterListType"/> that determines which update path is executed.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the updated <see cref="List{Patient}"/> documents,
/// or an empty list when no update was performed.</returns>
/// <!-- aidoc:v1 sig=afe83cc body=25f24cf -->
public async Task<List<Patient>> UpdateMasterListOption(List<ObjectId> unitIds, UpdateOptionMasterListDto opt,
string typeName)
{
var isParsed = Enum.TryParse<MasterListType>(typeName, out var parsedTypeName);
if (isParsed)
{
// Tener en cuenta los datos auxiliares ya que pueden ser texto libre o asignarse el establecido en la lista
// originAux / diagnosisAux modificar en caso de ser el mismo que el padre
// Notificar a todos los fronts con el nuevo valor de cada paciente por el id de los poc's afectados(?)
var filterUnit = Builders<Patient>.Filter.In("unitId", unitIds);
switch (parsedTypeName)
{
case MasterListType.DiagnosisList:
var diagnosisFilter = Builders<Patient>.Filter.Eq(
"diagnosis.name", opt.OldOption?.Name
);
var filterUpdateDiagnosis = Builders<Patient>.Filter.And(filterUnit, diagnosisFilter);
var updateDiagnosis = Builders<Patient>.Update
.Set("diagnosis.name", opt.UpdatedOption?.Name)
.Set("diagnosis.description", opt.UpdatedOption?.Description);
await Collection.UpdateManyAsync(filterUpdateDiagnosis, updateDiagnosis);
await Collection.UpdateManyAsync(Builders<Patient>.Filter.And(filterUnit,
Builders<Patient>.Filter.Eq(
"diagnosisAux", opt.OldOption?.Name
)), Builders<Patient>.Update
.Set("diagnosisAux", opt.UpdatedOption?.Name));
// Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux)
var diagnosisFilterToReturn =
Builders<Patient>.Filter.Eq("diagnosis.name", opt.UpdatedOption?.Name);
var diagnosisAuxFilterToReturn =
Builders<Patient>.Filter.Eq("diagnosisAux", opt.UpdatedOption?.Name);
var filterToReturn = Builders<Patient>.Filter.And(
filterUnit,
Builders<Patient>.Filter.Or(diagnosisFilterToReturn, diagnosisAuxFilterToReturn)
);
// Devolver los documentos actualizados
var updatedDocuments = await Collection.Find(filterToReturn).ToListAsync();
return updatedDocuments;
case MasterListType.DoctorList:
// Filtro para encontrar el elemento en el array `doctors` que coincida con el nombre
var doctorFilter = Builders<Patient>.Filter.ElemMatch(
"doctors",
Builders<Patient>.Filter.Eq("name", opt.OldOption?.Name)
);
var filterUpdateDoctor = Builders<Patient>.Filter.And(filterUnit, doctorFilter);
// Actualizar todos los elementos de la lista `doctors` que coincidan
var updateDoctor = Builders<Patient>.Update.Set(
"doctors.$[nameElem].name", opt.UpdatedOption?.Name
);
// Definir filtros únicos para cada campo utilizado
var arrayFilters = new List<ArrayFilterDefinition>
{
new BsonDocumentArrayFilterDefinition<BsonDocument>(
new BsonDocument("nameElem.name", opt.OldOption?.Name))
};
// Agregar filtro para `optionType` solo si es necesario
if (!string.IsNullOrEmpty(opt.OldOption?.OptionType))
{
updateDoctor = updateDoctor.Set(
"doctors.$[typeElem].optionType", opt.UpdatedOption?.OptionType
);
arrayFilters.Add(
new BsonDocumentArrayFilterDefinition<BsonDocument>(
new BsonDocument("typeElem.optionType", opt.OldOption.OptionType)));
}
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
// Ejecutar la actualización
await Collection.UpdateManyAsync(filterUpdateDoctor, updateDoctor, updateOptions);
// Devolver los documentos actualizados
var doctorFilterToReturn = Builders<Patient>.Filter.ElemMatch(
"doctors",
Builders<Patient>.Filter.Eq("name", opt.UpdatedOption?.Name)
);
var filterToReturnDoctor = Builders<Patient>.Filter.And(filterUnit, doctorFilterToReturn);
var updatedDoctorDocuments = await Collection.Find(filterToReturnDoctor).ToListAsync();
return updatedDoctorDocuments;
case MasterListType.InsulationList:
break;
case MasterListType.OriginList:
var originFilter = Builders<Patient>.Filter.Eq(
"origin.name", opt.OldOption?.Name
);
var filterUpdateOrigin = Builders<Patient>.Filter.And(filterUnit, originFilter);
var updateOrigin = Builders<Patient>.Update
.Set("origin.name", opt.UpdatedOption?.Name);
await Collection.UpdateManyAsync(filterUpdateOrigin, updateOrigin);
await Collection.UpdateManyAsync(Builders<Patient>.Filter.And(filterUnit,
Builders<Patient>.Filter.Eq(
"originAux", opt.OldOption?.Name
)), Builders<Patient>.Update
.Set("originAux", opt.UpdatedOption?.Name));
// Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux)
var originFilterToReturn = Builders<Patient>.Filter.Eq("origin.name", opt.UpdatedOption?.Name);
var originAuxFilterToReturn = Builders<Patient>.Filter.Eq("originAux", opt.UpdatedOption?.Name);
var originfilterToReturn = Builders<Patient>.Filter.And(
filterUnit,
Builders<Patient>.Filter.Or(originFilterToReturn, originAuxFilterToReturn)
);
// Devolver los documentos actualizados
var updatedDocumentsorigin = await Collection.Find(originfilterToReturn).ToListAsync();
return updatedDocumentsorigin;
case MasterListType.DoctorTypeList:
case MasterListType.AllergyList:
case MasterListType.DestinationList:
case MasterListType.ProcedureList:
case MasterListType.ServiceList:
case MasterListType.TreatmentList:
case MasterListType.AltableOptionList:
case MasterListType.DischargeStatusList:
case MasterListType.InternalDestinationList:
case MasterListType.LanguageBarrierList:
case MasterListType.MobilityOptionList:
case MasterListType.PassiveSittingList:
case MasterListType.PatientStatusList:
case MasterListType.TherapeuticCeilingList:
case MasterListType.VisitOptionList:
case MasterListType.AccessControlList:
break;
}
}
return [];
}
/// <summary>
/// Retrieves a list of patients whose unit identifier is contained in the specified collection of unit identifiers.
/// Validates the provided type name against the MasterListType enumeration; if the type name cannot be parsed, an empty list is returned.
/// </summary>
/// <param name="unitIds">The list of unit identifiers used to filter patients.</param>
/// <param name="typeName">The name of the type to validate against the MasterListType enumeration.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of Patient objects matching the specified unit identifiers, or an empty list if the type name is invalid.</returns>
/// <!-- aidoc:v1 sig=6f3d195 body=74ee8d7 -->
public async Task<List<Patient>> GetPatientsByUnitIds(List<ObjectId> unitIds, string typeName)
{
var isParsed = Enum.TryParse<MasterListType>(typeName, out _);
if (!isParsed) return [];
var filterUnit = Builders<Patient>.Filter.In("unitId", unitIds);
return await Collection.Find(filterUnit).ToListAsync();
}
/// <summary>
/// Asynchronously counts the number of patients associated with the specified unit identifier.
/// Returns 0 and logs the error if an exception occurs during the operation.
/// </summary>
/// <param name="unitId">The unit identifier used to filter patients.</param>
/// <returns>The number of patients matching the specified unit identifier, or 0 if an error occurs.</returns>
/// <!-- aidoc:v1 sig=baa8175 body=e692f35 -->
public async Task<long> CountByUnitId(ObjectId unitId)
{
try
{
var filter = Builders<Patient>.Filter.Eq(p => p.UnitId, unitId);
return await Collection.CountDocumentsAsync(filter);
}
catch (Exception e)
{
Log.Logger.Error(e.Message);
return 0;
}
}
/// <summary>
/// Deletes a master list option from <see cref="Patient"/> documents belonging to the specified units, applying the appropriate update logic based on the resolved <see cref="MasterListType"/>. Returns the affected patients for the supported types (<see cref="MasterListType.DiagnosisList"/>, <see cref="MasterListType.DoctorList"/>, and <see cref="MasterListType.OriginList"/>), or an empty collection when <paramref name="typeName"/> cannot be parsed or the type has no handling logic.
/// </summary>
/// <param name="unitIds">The collection of <see cref="ObjectId"/> values identifying the units whose patients will be affected by the deletion.</param>
/// <param name="opt">The <see cref="OptionList"/> option to remove, matched by its <see cref="OptionList.Name"/> (for diagnosis and origin lists) or its <see cref="OptionList.Id"/> (for the doctor list).</param>
/// <param name="typeName">The textual name of the master list type, parsed via <see cref="Enum.TryParse{T}"/> with <typeparamref name="T"/> = <see cref="MasterListType"/> to select the update strategy.</param>
/// <returns>A <see cref="Task"/> that yields the <see cref="Patient"/> documents modified for the supported <see cref="MasterListType"/> values, or an empty <see cref="List{Patient}"/> when no updates are performed.</returns>
/// <!-- aidoc:v1 sig=800d406 body=d26e8f1 -->
public async Task<IEnumerable<Patient>> DeleteMasterListOption(List<ObjectId> unitIds, OptionList opt,
string typeName)
{
var isParsed = Enum.TryParse<MasterListType>(typeName, out var parsedTypeName);
if (isParsed)
{
// Tener en cuenta los datos auxiliares ya que pueden ser texto libre o asignarse el establecido en la lista
// originAux / diagnosisAux modificar en caso de ser el mismo que el padre
// Notificar a todos los fronts con el nuevo valor de cada paciente por el id de los poc's afectados(?)
var filterUnit = Builders<Patient>.Filter.In("unitId", unitIds);
switch (parsedTypeName)
{
case MasterListType.DiagnosisList:
// Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux)
var diagnosisFilterToReturn = Builders<Patient>.Filter.Eq("diagnosis.name", opt.Name);
var diagnosisAuxFilterToReturn = Builders<Patient>.Filter.Eq("diagnosisAux", opt.Name);
var filterToReturn = Builders<Patient>.Filter.And(
filterUnit,
Builders<Patient>.Filter.Or(diagnosisFilterToReturn, diagnosisAuxFilterToReturn)
);
// Devolver los documentos actualizados
var updatedDocuments = await Collection.Find(filterToReturn).ToListAsync();
var diagnosisFilter = Builders<Patient>.Filter.Eq(
"diagnosis.name", opt.Name
);
var filterUpdateDiagnosis = Builders<Patient>.Filter.And(filterUnit, diagnosisFilter);
var updateDiagnosis = Builders<Patient>.Update
.Set(x => x.Diagnosis, null);
await Collection.UpdateManyAsync(filterUpdateDiagnosis, updateDiagnosis);
await Collection.UpdateManyAsync(Builders<Patient>.Filter.And(filterUnit,
Builders<Patient>.Filter.Eq(
"diagnosisAux", opt.Name
)), Builders<Patient>.Update
.Set(x => x.DiagnosisAux, null));
return updatedDocuments;
case MasterListType.DoctorList:
// Devolvemos los datos
var doctorFilterToReturn = Builders<Patient>.Filter.ElemMatch(
"doctors",
Builders<Patient>.Filter.Eq("_id", opt.Id)
);
var filterToReturnDoctor = Builders<Patient>.Filter.And(filterUnit, doctorFilterToReturn);
var updatedDoctorDocuments = await Collection.Find(filterToReturnDoctor).ToListAsync();
// Filtro para encontrar el elemento en el array `doctors` que coincida con el nombre
var doctorFilter = Builders<Patient>.Filter.ElemMatch(
"doctors",
Builders<Patient>.Filter.Eq("_id", opt.Id)
);
var filterUpdateDoctor = Builders<Patient>.Filter.And(filterUnit, doctorFilter);
// Actualización del campo `name` en el array `doctors`
var updateDoctor = Builders<Patient>.Update.PullFilter(
"doctors", Builders<BsonDocument>.Filter.And(
Builders<BsonDocument>.Filter.Eq("_id", opt.Id)
));
// Ejecutar la actualización
await Collection.UpdateManyAsync(filterUpdateDoctor, updateDoctor);
return updatedDoctorDocuments;
case MasterListType.InsulationList:
break;
case MasterListType.OriginList:
// Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux)
var originFilterToReturn = Builders<Patient>.Filter.Eq("origin.name", opt.Name);
var originAuxFilterToReturn = Builders<Patient>.Filter.Eq("originAux", opt.Name);
var filterOriginToReturn = Builders<Patient>.Filter.And(
filterUnit,
Builders<Patient>.Filter.Or(originFilterToReturn, originAuxFilterToReturn)
);
// Devolver los documentos actualizados
var updatedDocumentsOrigin = await Collection.Find(filterOriginToReturn).ToListAsync();
var originFilter = Builders<Patient>.Filter.Eq(
"origin.name", opt.Name
);
var filterUpdateOrigin = Builders<Patient>.Filter.And(filterUnit, originFilter);
var updateOrigin = Builders<Patient>.Update
.Set(x => x.Diagnosis, null);
await Collection.UpdateManyAsync(filterUpdateOrigin, updateOrigin);
await Collection.UpdateManyAsync(Builders<Patient>.Filter.And(filterUnit,
Builders<Patient>.Filter.Eq(
"originAux", opt.Name
)), Builders<Patient>.Update
.Set(x => x.OriginAux, null));
return updatedDocumentsOrigin;
case MasterListType.DoctorTypeList:
case MasterListType.AllergyList:
case MasterListType.DestinationList:
case MasterListType.ProcedureList:
case MasterListType.ServiceList:
case MasterListType.TreatmentList:
case MasterListType.AltableOptionList:
case MasterListType.DischargeStatusList:
case MasterListType.InternalDestinationList:
case MasterListType.LanguageBarrierList:
case MasterListType.MobilityOptionList:
case MasterListType.PassiveSittingList:
case MasterListType.PatientStatusList:
case MasterListType.TherapeuticCeilingList:
case MasterListType.VisitOptionList:
case MasterListType.AccessControlList:
break;
}
}
return new List<Patient>();
}
/// <summary>
/// Finds the <see cref="Patient"/> associated with the specified <see cref="Patient.PatientId"/>, preferring an active admission (no <see cref="Patient.DisTime"/>) sorted by most recent <see cref="Patient.AdmTime"/>, and falling back to the most recently discharged record when no active admission exists.
/// </summary>
/// <param name="patientId">The identifier of the patient to locate.</param>
/// <returns>A <see cref="Patient"/> instance if found; otherwise, <see langword="null"/> when <paramref name="patientId"/> is null, empty, or whitespace, or when no matching record exists.</returns>
/// <!-- aidoc:v1 sig=fca9190 body=71f3c84 -->
public async Task<Patient?> FindByPatientId(string patientId)
{
if (string.IsNullOrWhiteSpace(patientId)) return null;
//Último paciente admitido
var patient = await Collection.Find(p => !p.DisTime.HasValue && patientId == p.PatientId)
.Sort(Builders<Patient>.Sort.Descending(p => p.AdmTime)).Limit(1).FirstOrDefaultAsync();
//Último con fecha de alta más reciente
patient ??= await Collection.Find(p => p.DisTime.HasValue && patientId == p.PatientId)
.Sort(Builders<Patient>.Sort.Descending(p => p.DisTime)).Limit(1).FirstOrDefaultAsync();
return patient;
}
/// <summary>
/// Retrieves the most relevant <see cref="Patient"/> record for the specified identifier, prioritizing an active admission (no discharge time) ordered by the latest <see cref="Patient.AdmTime"/>, and falling back to the most recently discharged patient ordered by <see cref="Patient.DisTime"/> when no active admission exists.
/// </summary>
/// <param name="patientId">The <see cref="ObjectId"/> of the <see cref="Patient"/> to look up.</param>
/// <returns>The matching <see cref="Patient"/> if one is found; otherwise, <see langword="null"/>.</returns>
/// <!-- aidoc:v1 sig=b9e9549 body=8f9c8ea -->
public async Task<Patient?> FindByPatientId(ObjectId patientId)
{
//Último paciente admitido
var patient = await Collection.Find(p => !p.DisTime.HasValue && patientId == p.Id)
.Sort(Builders<Patient>.Sort.Descending(p => p.AdmTime)).Limit(1).FirstOrDefaultAsync();
//Último con fecha de alta más reciente
patient ??= await Collection.Find(p => p.DisTime.HasValue && patientId == p.Id)
.Sort(Builders<Patient>.Sort.Descending(p => p.DisTime)).Limit(1).FirstOrDefaultAsync();
return patient;
}
/// <summary>
/// Retrieves all <see cref="Patient"/> records from the data store.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of all patients.</returns>
/// <!-- aidoc:v1 sig=e8bb639 body=c6cbf36 -->
public async Task<List<Patient>> FindAll()
{
return (await Collection.FindAsync(Builders<Patient>.Filter.Empty)).ToList();
//return (await Collection.FindAsync(_ => true)).ToList();
}
/// <summary>
/// Asynchronously retrieves all patients associated with the specified point of care from the collection.
/// </summary>
/// <param name="pointOfCare">The unit or point of care used to filter the patients.</param>
/// <returns>A task containing a list of <see cref="Patient"/> objects whose unit matches the specified point of care; an empty list is returned when no matches are found.</returns>
/// <!-- aidoc:v1 sig=c4523a3 body=eff26de -->
public async Task<List<Patient>> FindByPointOfCare(string pointOfCare)
{
var filterBuilder = Builders<Patient>.Filter;
var filter = filterBuilder.Eq(pa => pa.UnitString, pointOfCare);
var result = await Collection.FindAsync(filter);
return await result.ToListAsync();
}
/// <summary>
/// Asynchronously retrieves all patients associated with the specified point of care.
/// </summary>
/// <param name="pointOfCare">The identifier of the point of care used to filter the patient records.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of patients matching the specified point of care.</returns>
/// <!-- aidoc:v1 sig=1cea52d body=b9dfd95 -->
public async Task<List<Patient>> FindByPointOfCare(ObjectId pointOfCare)
{
var filterBuilder = Builders<Patient>.Filter;
var filter = filterBuilder.Eq(pa => pa.PointOfCareId, pointOfCare);
var result = await Collection.FindAsync(filter);
return await result.ToListAsync();
}
/// <summary>
/// Asynchronously retrieves all <see cref="Patient"/> records whose associated point of care is not a virtual <see cref="VirtualPointOfCare"/>, by performing a lookup against the <c>pointOfCares</c> collection and excluding any bed whose value matches a virtual point of care.
/// </summary>
/// <returns>A <see cref="Task"/> that resolves to a <see cref="List{Patient}"/> containing the patients located in active (non-virtual) points of care.</returns>
/// <!-- aidoc:v1 sig=2479e43 body=f5b7981 -->
public async Task<List<Patient>> FindInActivePoC()
{
var virtualPointOfCareValues = Enum.GetValues(typeof(VirtualPointOfCare)).Cast<VirtualPointOfCare>()
.Select(p => p.ToString())
.ToList();
//var filterBuilder = Builders<Patient>.Filter;
// Define el pipeline de agregación
var pipeline = new[]
{
new BsonDocument("$lookup", new BsonDocument
{
{ "from", "pointOfCares" }, // Colección de PointOfCare
{ "localField", "pointOfCareId" },
{ "foreignField", "_id" },
{ "as", "pointOfCareInfo" }
}),
new BsonDocument("$unwind", "$pointOfCareInfo"),
new BsonDocument("$match", new BsonDocument
{
{ "pointOfCareInfo.bed", new BsonDocument("$nin", new BsonArray(virtualPointOfCareValues)) }
})
};
var result = await Collection.Aggregate<Patient>(pipeline).ToListAsync();
return result;
}
/// <summary>
/// Asynchronously retrieves the <see cref="Patient"/> records whose associated point of care has a bed value matching one of the <see cref="VirtualPointOfCare"/> enum values.
/// The lookup is performed through a MongoDB aggregation pipeline that joins the patients collection with the point of care collection and filters by the <c>pointOfCareInfo.bed</c> field.
/// </summary>
/// <returns>A <see cref="Task"/> that yields a <see cref="List{Patient}"/> containing the patients linked to a point of care whose bed matches any <see cref="VirtualPointOfCare"/> value.</returns>
/// <!-- aidoc:v1 sig=1a67064 body=3dc3451 -->
public async Task<List<Patient>> FindInInactivePoC()
{
var virtualPointOfCareValues = Enum.GetValues(typeof(VirtualPointOfCare)).Cast<VirtualPointOfCare>()
.Select(p => p.ToString())
.ToList();
//var filterBuilder = Builders<Patient>.Filter;
// Define el pipeline de agregación
var pipeline = new[]
{
new BsonDocument("$lookup", new BsonDocument
{
{ "from", "pointOfCares" }, // Colección de PointOfCare
{ "localField", "pointOfCareId" },
{ "foreignField", "_id" },
{ "as", "pointOfCareInfo" }
}),
new BsonDocument("$unwind", "$pointOfCareInfo"),
new BsonDocument("$match", new BsonDocument
{
{ "pointOfCareInfo.bed", new BsonDocument("$in", new BsonArray(virtualPointOfCareValues)) }
})
};
var result = await Collection.Aggregate<Patient>(pipeline).ToListAsync();
return result;
}
/// <summary>
/// Retrieves a paginated, sorted (by admission time descending) queryable of patients, applying optional
/// filters for text search across patient names and patient number (and by identifier when the text parses
/// as an ObjectId), time range, unit, and point of care, and falling back to an unfiltered query when no
/// filtered request is provided.
/// </summary>
/// <param name="filter">The pagination and filtering criteria, including optional text, time range, unit, and point of care filters.</param>
/// <returns>A find fluent for the filtered and sorted patient query.</returns>
/// <!-- aidoc:v1 sig=6790d47 body=f65667b -->
public IFindFluent<Patient, Patient> GetPaginatedPatients(PaginationFilter filter)
{
var filterBuilder = Builders<Patient>.Filter;
var sort = Builders<Patient>.Sort.Descending("admTime");
var filters = new List<FilterDefinition<Patient>>();
if (filter.FilteredRequest != null)
{
AddDefaultFilters(filters, filter, filterBuilder);
// return CreateFindFluent(filters, sort);
}
var requestFilter = filter.FilteredRequest;
if(requestFilter == null) return CreateFindFluent(filters, sort);
AddTimeFilters(requestFilter, filters, filterBuilder);
if (!string.IsNullOrEmpty(filter.FilteredRequest?.Text))
{
var textFilter = filter.FilteredRequest.Text;
var textFilterEscaped = Regex.Escape(textFilter);
var orFilters = new List<FilterDefinition<Patient>>
{
filterBuilder.Regex(p => p.Person!.FirstName, new BsonRegularExpression(textFilterEscaped, "i")),
filterBuilder.Regex(p => p.Person!.SecondName, new BsonRegularExpression(textFilterEscaped, "i")),
filterBuilder.Regex(p => p.Person!.LastName, new BsonRegularExpression(textFilterEscaped, "i")),
filterBuilder.Regex(p => p.PatientNumber, new BsonRegularExpression(textFilterEscaped, "i"))
};
if (ObjectId.TryParse(textFilter, out var id)) orFilters.Add(filterBuilder.Eq("_id", id));
filters.Add(filterBuilder.Or(orFilters));
}
if (filter.FilteredRequest?.UnitId != null && ObjectId.TryParse(filter.FilteredRequest.UnitId, out var unitId))
filters.Add(filterBuilder.Eq("unitId", unitId));
if (filter.FilteredRequest?.PocId != null && ObjectId.TryParse(filter.FilteredRequest.PocId, out var pocId))
filters.Add(filterBuilder.Eq("pointOfCareId", pocId));
return CreateFindFluent(filters, sort);
}
/// <summary>
/// Retrieves all patients whose <c>UpdateDate</c> is older than the specified date or has never been set (null), identifying records that have not been updated since the given threshold.
/// </summary>
/// <param name="date">The cutoff date; patients with an <c>UpdateDate</c> strictly earlier than this value, or with no <c>UpdateDate</c> set, will be returned.</param>
/// <returns>A task that resolves to a list of <see cref="Patient"/> instances matching the filter criteria.</returns>
/// <!-- aidoc:v1 sig=42988dc body=d938adb -->
public async Task<List<Patient>> FindPatientsNotUpdatedSince(DateTime date)
{
var filter = Builders<Patient>.Filter.Or(
Builders<Patient>.Filter.Lt(p => p.UpdateDate, date),
Builders<Patient>.Filter.Eq(p => p.UpdateDate, null)
);
return await Collection.Find(filter).ToListAsync();
//return await Collection.Find(p => p.UpdateDate < date).ToListAsync();
}
/// <summary>
/// Retrieves all patients whose discharge time (<see cref="Patient.DisTime"/>) has been recorded,
/// indicating they have been discharged. If an error occurs during the database operation,
/// the exception is logged and an empty list is returned instead of propagating the failure.
/// </summary>
/// <returns>A task that resolves to a list of discharged <see cref="Patient"/> records,
/// or an empty list if the operation fails.</returns>
/// <!-- aidoc:v1 sig=c56ad55 body=600ef72 -->
public async Task<List<Patient>> FindDischargedPatients()
{
try
{
var filter = Builders<Patient>.Filter.Ne(p => p.DisTime, null);
var cursor = await Collection.FindAsync(filter);
var patients = await cursor.ToListAsync();
return patients;
}
catch (Exception ex)
{
Log.Error("Error finding discharged patients {exMessage}", ex.Message);
return [];
}
}
/// <summary>
/// Updates an existing patient record in the data store, refreshing the modification timestamp, and returns the updated document.
/// Returns <c>null</c> when no patient matching the provided identifier is found.
/// </summary>
/// <param name="updatedPatient">The patient entity containing the updated values and the identifier of the record to modify.</param>
/// <returns>The updated <see cref="Patient"/> after the modification is applied, or <c>null</c> if no matching document exists.</returns>
/// <!-- aidoc:v1 sig=5cf3377 body=b2c2664 -->
public async Task<Patient?> UpdateOne(Patient updatedPatient)
{
var filter = Builders<Patient>.Filter.Eq("_id", updatedPatient.Id);
var update = Builders<Patient>.Update
.Set(p => p.PatientNumber, updatedPatient.PatientNumber)
.Set(p => p.Person, updatedPatient.Person)
.Set(p => p.UpdateDate, DateTime.UtcNow);
return await Collection.FindOneAndUpdateAsync(filter, update,
new FindOneAndUpdateOptions<Patient, Patient> { ReturnDocument = ReturnDocument.After });
}
/// <summary>
/// Updates the incoming data fields of an existing patient in the database, refreshing the update timestamp.
/// Returns the updated patient document, or <c>null</c> if no patient matches the specified identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient to update.</param>
/// <param name="person">The patient object containing the new values for the incoming data fields.</param>
/// <returns>The updated <see cref="Patient"/> document after the modification, or <c>null</c> if no document was found.</returns>
/// <!-- aidoc:v1 sig=c195b1f body=20e831b -->
public async Task<Patient?> UpdatePatientIncomingData(ObjectId patientId, Patient person)
{
var filter = Builders<Patient>.Filter.Eq("_id", patientId);
var update = Builders<Patient>.Update
.Set(p => p.OriginAux, person.OriginAux)
.Set(p => p.DiagnosisAux, person.DiagnosisAux)
.Set(p => p.Diagnosis, person.Diagnosis)
.Set(p => p.UpdateDate, DateTime.UtcNow)
.Set(p => p.AdmTime, person.AdmTime)
.Set(p => p.Origin, person.Origin);
return await Collection.FindOneAndUpdateAsync(filter, update,
new FindOneAndUpdateOptions<Patient, Patient> { ReturnDocument = ReturnDocument.After });
}
/// <summary>
/// Updates the demographic and clinical information of an existing patient, including allergies, language barrier, diagnosis, and person data, while setting the update timestamp to the current UTC time.
/// Returns the updated patient document after the modification has been applied.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose data will be updated.</param>
/// <param name="person">The patient object containing the new demographic and clinical values to persist.</param>
/// <returns>The updated <see cref="Patient"/> document, or <c>null</c> if no patient with the specified identifier was found.</returns>
/// <!-- aidoc:v1 sig=a6fa876 body=4fc291c -->
public async Task<Patient?> UpdatePatientDemographicData(ObjectId patientId, Patient person)
{
var filter = Builders<Patient>.Filter.Eq("_id", patientId);
var update = Builders<Patient>.Update
.Set(p => p.UpdateDate, DateTime.UtcNow)
.Set(p => p.Allergies, person.Allergies)
.Set(p => p.LanguageBarrier, person.LanguageBarrier)
.Set(p => p.DiagnosisAux, person.DiagnosisAux)
.Set(p => p.Diagnosis, person.Diagnosis)
.Set(p => p.Person, person.Person);
return await Collection.FindOneAndUpdateAsync(filter, update,
new FindOneAndUpdateOptions<Patient, Patient> { ReturnDocument = ReturnDocument.After });
}
/// <summary>
/// Creates MongoDB indexes for the Patient collection, including a unique index on the <c>patientNumber</c> field and a non-unique index on the <c>admTime</c> field, using background index creation.
/// </summary>
/// <!-- aidoc:v1 sig=4955da2 body=6e16010 -->
public override async Task CreateIndexes()
{
var options = new CreateIndexOptions<Patient> { Background = true, Unique = false };
var optionsUq = new CreateIndexOptions<Patient>
{
Background = true,
Unique = true
//PartialFilterExpression = Builders<Patient>.Filter.Exists(p => p.PointOfCareId) &
// Builders<Patient>.Filter.Exists(p => p.UnitId)
};
var indexes = new List<CreateIndexModel<Patient>>
{
new("{ patientNumber: 1 }", optionsUq),
new("{ admTime: 1 }", options)
//new("{ pointOfCareId: 1, unitId: 1 }", optionsUq),
//new("{ pointOfCareId: 1 }", optionsUq)
};
await MongoUtils.EnsureIndexes(Collection, indexes);
}
/// <summary>
/// Appends time-range filter definitions to the provided filter list based on the request criteria, covering admission time, discharge time, last observation time, creation date, update date, and birth date. For birth date filtering, an existence filter on the related Person entity is added first to ensure the range conditions are only applied when Person is not null.
/// </summary>
/// <param name="requestFilter">The request containing the optional start and end time values used to build each range filter.</param>
/// <param name="filters">The collection of filter definitions to which the constructed filters are added.</param>
/// <param name="filterBuilder">The builder used to create the Gte and Lte filter definitions for each time range.</param>
/// <!-- aidoc:v1 sig=095d341 body=8b4f9c0 -->
private static void AddTimeFilters(FilteredRequest requestFilter, List<FilterDefinition<Patient>> filters,
FilterDefinitionBuilder<Patient> filterBuilder)
{
// Filter by admission time
if (requestFilter.StartAdmTime.HasValue)
filters.Add(filterBuilder.Gte(p => p.AdmTime, requestFilter.StartAdmTime.Value));
if (requestFilter.EndAdmTime.HasValue)
filters.Add(filterBuilder.Lte(p => p.AdmTime, requestFilter.EndAdmTime.Value));
// Filter by discharge time
if (requestFilter.StartDischargeTime.HasValue)
filters.Add(filterBuilder.Gte(p => p.DisTime, requestFilter.StartDischargeTime.Value));
if (requestFilter.EndDischargeTime.HasValue)
filters.Add(filterBuilder.Lte(p => p.DisTime, requestFilter.EndDischargeTime.Value));
// Filter by last observation time
if (requestFilter.StartLastObsTime.HasValue)
filters.Add(filterBuilder.Gte(p => p.LastObservationDate, requestFilter.StartLastObsTime.Value));
if (requestFilter.EndLastObsTime.HasValue)
filters.Add(filterBuilder.Lte(p => p.LastObservationDate, requestFilter.EndLastObsTime.Value));
// Filter by creation date
if (requestFilter.StartCreationDateTime.HasValue)
filters.Add(filterBuilder.Gte(p => p.CreationDate, requestFilter.StartCreationDateTime.Value));
if (requestFilter.EndCreationDateTime.HasValue)
filters.Add(filterBuilder.Lte(p => p.CreationDate, requestFilter.EndCreationDateTime.Value));
// Filter by update date
if (requestFilter.StartUpDateTime.HasValue)
filters.Add(filterBuilder.Gte(p => p.UpdateDate, requestFilter.StartUpDateTime.Value));
if (requestFilter.EndUpDateTime.HasValue)
filters.Add(filterBuilder.Lte(p => p.UpdateDate, requestFilter.EndUpDateTime.Value));
// Filter by birth date (with Person null check)
if (requestFilter.StartBirthDate.HasValue || requestFilter.EndBirthDate.HasValue)
{
// Check if Person is not null
filters.Add(filterBuilder.Exists(p => p.Person));
// Apply birth date filters only if Person is not null
if (requestFilter.StartBirthDate.HasValue)
filters.Add(filterBuilder.Gte(p => p.Person!.BirthDate, requestFilter.StartBirthDate.Value));
if (requestFilter.EndBirthDate.HasValue)
filters.Add(filterBuilder.Lte(p => p.Person!.BirthDate, requestFilter.EndBirthDate.Value));
}
}
/// <summary>
/// Asynchronously retrieves all patients associated with the specified point of care identifier.
/// </summary>
/// <param name="pointOfCare">The ObjectId of the point of care used to filter the patients.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of patients matching the specified point of care.</returns>
/// <!-- aidoc:v1 sig=4945a00 body=b9dfd95 -->
public async Task<List<Patient>> FindAllByPointOfCareId(ObjectId pointOfCare)
{
var filterBuilder = Builders<Patient>.Filter;
var filter = filterBuilder.Eq(pa => pa.PointOfCareId, pointOfCare);
var result = await Collection.FindAsync(filter);
return await result.ToListAsync();
}
/// <summary>
/// Adds default filter definitions to the specified filters list based on the provided pagination filter.
/// Filters by <see cref="Patient.Id"/> when <c>PatientId</c> is supplied and successfully parsed as an <see cref="ObjectId"/>; otherwise, filters by <c>PatientNumber</c> when it is supplied.
/// </summary>
/// <param name="filters">The list of filter definitions to which the new filter will be appended.</param>
/// <param name="filter">The pagination filter containing the request criteria used to build the default filter.</param>
/// <param name="filterBuilder">The builder used to construct the MongoDB filter definitions for the <see cref="Patient"/> entity.</param>
/// <!-- aidoc:v1 sig=6206f14 body=1a8e3bd -->
private void AddDefaultFilters(List<FilterDefinition<Patient>> filters, PaginationFilter filter,
FilterDefinitionBuilder<Patient> filterBuilder)
{
// Verifica si PatientId tiene valor
if (filter.FilteredRequest?.PatientId != null)
{
if(ObjectId.TryParse(filter.FilteredRequest.PatientId, out var patientId))
filters.Add(
filterBuilder.Eq(p => p.Id, patientId)
);
}
// Verifica si PatientNumber tiene valor
else if (filter.FilteredRequest?.PatientNumber != null)
filters.Add(
filterBuilder.Eq(p => p.PatientNumber, filter.FilteredRequest.PatientNumber)
);
}
/// <summary>
/// Creates a MongoDB find fluent query for <see cref="Patient"/> documents by combining the provided filters with a logical AND, or applying no filter when none are supplied, and applying the specified sort order.
/// </summary>
/// <param name="filters">The list of filter definitions to combine; when empty, an empty filter (match-all) is used.</param>
/// <param name="sort">The sort definition to apply to the query results.</param>
/// <returns>An <see cref="IFindFluent{TSource, TDocument}"/> configured with the combined filter and sort.</returns>
/// <!-- aidoc:v1 sig=6ee44b3 body=7b7fa02 -->
private IFindFluent<Patient, Patient> CreateFindFluent(List<FilterDefinition<Patient>> filters,
SortDefinition<Patient> sort)
{
var combinedFilter = filters.Any()
? Builders<Patient>.Filter.And(filters)
: Builders<Patient>.Filter.Empty;
return Collection.Find(combinedFilter).Sort(sort);
}
}