Files
adas-core/adas-core.Infrastructure/Repositories/UnitRepository.cs
T
2026-06-26 10:29:23 +02:00

493 lines
25 KiB
C#

using adas_core.Application.Repositories.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.DTO;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.MongoModels;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Newtonsoft.Json;
using Serilog;
using System.Text.RegularExpressions;
namespace adas_core.Infrastructure.Repositories;
/// <summary>
/// Represents a MongoDB-backed repository for <see cref="Unit"/> entities, inheriting persistence functionality from <see cref="MongoRepository{Unit}"/> and implementing the <see cref="IUnitRepository"/> contract.
/// </summary>
public class UnitRepository : MongoRepository<Unit>, IUnitRepository
{
#region Properties
private readonly ApiSettings _apiSettings;
#endregion
#region Constructor
public UnitRepository(IOptions<ApiSettings> apiSettings, IMongoDatabase database) : base(database)
{
_apiSettings = apiSettings.Value;
}
#endregion
#region Methods
#region Create
/// <summary>
/// Inserts a new <see cref="Unit"/> into the underlying collection and returns the persisted instance retrieved by its identifier.
/// If the insertion fails, the error is logged and the method returns <c>null</c> as a fallback.
/// </summary>
/// <param name="unit">The <see cref="Unit"/> to be inserted into the collection.</param>
/// <returns>The inserted <see cref="Unit"/> as returned by the lookup by identifier, or <c>null</c> if an exception occurred during insertion.</returns>
public async Task<Unit?> InsertOneUnit(Unit unit)
{
try
{
await Collection.InsertOneAsync(unit);
return await FindById(unit.Id);
}
catch (Exception ex)
{
Log.Error("Error inserting Unit: {unit}. Exception: {ex}",
JsonConvert.SerializeObject(unit, Formatting.Indented), ex);
return null;
}
}
#endregion
#region Read
/// <summary>
/// Gets the collection name for units, returning the configured value from API settings or falling back to the default "units" when not specified.
/// </summary>
/// <returns>The collection name for units, or "units" if no custom value is configured in the API settings.</returns>
public override string GetCollectionName()
{
return _apiSettings.Units ?? "units";
}
// public async Task<Unit?> FindByLocation(PatientLocation location)
// {
// var filter = Builders<Unit>.Filter.ElemMatch(x => x.PointOfCares, poc => poc.Bed == location.Bed && poc.UnitName == location.UnitName);
// var result = await Collection.Find(filter).FirstOrDefaultAsync();
//
// return result;
// }
/// <summary>
/// Asynchronously finds a <see cref="Unit"/> by its identifier in the underlying collection.
/// Returns <see langword="null"/> when no matching document is found.
/// </summary>
/// <param name="id">The identifier of the <see cref="Unit"/> to locate.</param>
/// <returns>A <see cref="Unit"/> instance if a match is found; otherwise, <see langword="null"/>.</returns>
public async Task<Unit?> FindById(object id)
{
var result = await Collection.FindAsync(Builders<Unit>.Filter.Eq(x => x.Id, id));
return await result.FirstOrDefaultAsync();
}
/// <summary>
/// Asynchronously retrieves a <see cref="Unit"/> by its unique identifier, returning <c>null</c> when no matching unit is found.
/// </summary>
/// <param name="id">The unique identifier of the unit to look up.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="Unit"/>, or <c>null</c> if no unit is found with the specified identifier.</returns>
/// <exception cref="NotImplementedException">The method is not yet implemented.</exception>
public Task<Unit?> FindById(string id)
{
throw new NotImplementedException();
}
public async Task<IEnumerable<Unit>> FindByMasterListId(ObjectId id, MasterListType masterListType)
{
try
{
var propertyName = $"{masterListType}Id"; // nombre de la propiedad dinámicamente
var filter = Builders<Unit>.Filter.Eq(propertyName, id);
var result = await Collection.Find(filter).ToListAsync();
return result;
}
catch (Exception ex)
{
Log.Error("Error searching unit by {masterlisttype} Id {id}. Exception: {ex}", masterListType.ToString(),
id, ex);
throw;
}
}
/// <summary>
/// Retrieves all <see cref="Unit"/> documents that reference the specified master list identifier in any of their associated list properties (e.g., doctor, allergy, destination, diagnosis, procedure, test, service, treatment, language barrier, and similar lists). Uses an OR-based filter to match the identifier against every list id field, returning matching units; if an error occurs during the query, an empty collection is returned after logging the exception.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the master list to search for across the unit's list reference fields.</param>
/// <returns>A task that yields an <see cref="IEnumerable{Unit}"/> containing the matching units, or an empty list if no matches are found or an error occurs.</returns>
public async Task<IEnumerable<Unit>> FindByMasterListId(ObjectId id)
{
try
{
var filterBuilder = Builders<Unit>.Filter;
var filters = new List<FilterDefinition<Unit>>
{
filterBuilder.Or(
filterBuilder.Eq(p => p.DoctorListId, id),
filterBuilder.Eq(p => p.AllergyListId, id),
filterBuilder.Eq(p => p.DestinationListId, id),
filterBuilder.Eq(p => p.DiagnosisListId, id),
filterBuilder.Eq(p => p.InsulationListId, id),
filterBuilder.Eq(p => p.OriginListId, id),
filterBuilder.Eq(p => p.ProcedureListId, id),
filterBuilder.Eq(p => p.TestListId, id),
filterBuilder.Eq(p => p.ServiceListId, id),
filterBuilder.Eq(p => p.TreatmentListId, id),
filterBuilder.Eq(p => p.LanguageBarrierListId, id),
filterBuilder.Eq(p => p.AltableOptionListId, id),
filterBuilder.Eq(p => p.DischargeStatusListId, id),
filterBuilder.Eq(p => p.DoctorTypeListId, id),
filterBuilder.Eq(p => p.InternalDestinationListId, id),
filterBuilder.Eq(p => p.PassiveSittingListId, id),
filterBuilder.Eq(p => p.GenericListId, id),
filterBuilder.Eq(p => p.VisitOptionListId, id),
filterBuilder.Eq(p => p.AccessControlListId, id),
filterBuilder.Eq(p => p.TherapeuticCeilingListId, id),
filterBuilder.Eq(p => p.MobilityOptionListId, id)
)
};
var result = await Collection.Find(Builders<Unit>.Filter.And(filters)).ToListAsync();
return result;
}
catch (Exception ex)
{
Log.Error("Error searching unit by masterlist Id {id}. Exception: {ex}", id.ToString(), ex);
return new List<Unit>();
}
}
/// <summary>
/// Asynchronously counts the number of <see cref="Unit"/> documents associated with the specified master list.
/// The filter property is dynamically constructed based on the <paramref name="masterListType"/> (e.g., "{Type}Id"), so the matching field depends on the provided master list type.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the master list used to match units.</param>
/// <param name="masterListType">The type of master list, which determines the property name used in the filter.</param>
/// <returns>The total number of <see cref="Unit"/> documents that match the filter.</returns>
public async Task<long> CountUnitsByMasterListId(ObjectId id, MasterListType masterListType)
{
var propertyName = $"{masterListType}Id";
var filter = Builders<Unit>.Filter.Eq(propertyName, id);
var result = await Collection.CountDocumentsAsync(filter);
return result;
}
/// <summary>
/// Asynchronously finds and returns a <see cref="Unit"/> matching the specified name from the collection, or <c>null</c> if no matching document is found.
/// </summary>
/// <param name="unitName">The name of the unit to look up using an exact equality match.</param>
/// <returns>A <see cref="Task{Unit?}"/> containing the matching <see cref="Unit"/> if found; otherwise, <c>null</c>.</returns>
public async Task<Unit?> FindByName(string unitName)
{
var result = await Collection.FindAsync(Builders<Unit>.Filter.Eq(x => x.Name, unitName));
return await result.FirstOrDefaultAsync();
}
// public async Task<List<Unit>> FindByPointOfCare(PointOfCare pointOfCare)
// {
// var filter = Builders<Unit>.Filter.ElemMatch(x => x.PointOfCares, poc => poc.Bed == pointOfCare.Bed && poc.Room == pointOfCare.Room && pointOfCare.unitName == poc.unitName);
// var result = await Collection.Find(filter).ToListAsync();
//
// return result;
// }
// public async Task<Unit?> FindByPointOfCare(PointOfCare pointOfCare)
// {
// var filter = Builders<Unit>.Filter.ElemMatch(x => x.PointOfCares, poc => poc.Bed == pointOfCare.Bed && poc.Room == pointOfCare.Room && pointOfCare.UnitName == poc.UnitName);
// var result = await Collection.Find(filter).FirstOrDefaultAsync();
//
// return result;
// }
// public async Task<List<Unit>> FindByUnitName(string unitName)
// {
// var filter = Builders<Unit>.Filter.ElemMatch(x => x.PointOfCares, poc => poc.UnitName == unitName);
// var result = await Collection.Find(filter).ToListAsync();
//
// return result;
// }
/// <summary>
/// Asynchronously retrieves all <see cref="Unit"/> records from the underlying collection.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of all <see cref="Unit"/> documents found in the collection.</returns>
public async Task<List<Unit>> GetAll()
{
var result = await Collection.FindAsync(Builders<Unit>.Filter.Empty);
return result.ToList();
}
/// <summary>
/// Retrieves a paginated, sortable query of <see cref="Unit"/> documents, optionally filtered by a case-insensitive text search applied to the Name and Title fields.
/// </summary>
/// <param name="filter">The pagination filter containing the optional text search criteria used to narrow the result set.</param>
/// <returns>An <see cref="IFindFluent{Unit, Unit}"/> representing the sorted and filtered query of units; if no filter request is supplied, an unfiltered query sorted by title is returned.</returns>
public IFindFluent<Unit, Unit> GetPaginatedUnits(PaginationFilter filter)
{
var filterBuilder = Builders<Unit>.Filter;
var sort = Builders<Unit>.Sort.Ascending("title");
var filters = new List<FilterDefinition<Unit>>();
if (filter.FilteredRequest == null) return CreateFindFluent(filters, sort);
if (!string.IsNullOrEmpty(filter.FilteredRequest?.Text))
{
var textFilter = filter.FilteredRequest.Text;
var textFilterEscaped = Regex.Escape(textFilter);
filters.Add(
filterBuilder.Or(
filterBuilder.Regex(p => p.Name,
new BsonRegularExpression(textFilterEscaped, "i")), // Case-insensitive regex match for name
filterBuilder.Regex(p => p.Title,
new BsonRegularExpression(textFilterEscaped, "i")) // Case-insensitive regex match for title
)
);
}
return CreateFindFluent(filters, sort);
}
/// <summary>
/// Creates a fluent find query for the <see cref="Unit"/> collection, applying the supplied filters and sort definition. When the filter list is empty, an empty filter is used so that all documents are matched.
/// </summary>
/// <param name="filters">The list of filter definitions to combine with a logical AND; if empty, no filtering is applied.</param>
/// <param name="sort">The sort definition to apply to the query results.</param>
/// <returns>A fluent find interface for <see cref="Unit"/> that can be further chained to project, limit, or execute the query.</returns>
private IFindFluent<Unit, Unit> CreateFindFluent(List<FilterDefinition<Unit>> filters, SortDefinition<Unit> sort)
{
var combinedFilter = filters.Any()
? Builders<Unit>.Filter.And(filters)
: Builders<Unit>.Filter.Empty;
return Collection.Find(combinedFilter).Sort(sort);
}
#endregion
#region Update
/// <summary>
/// Updates an existing unit document in the collection, returning the updated document.
/// Uses a filter on the unit's identifier and sets the updatable fields; returns null when no matching document is found.
/// </summary>
/// <param name="unit">The unit containing the identifier and the new field values to be persisted.</param>
/// <returns>The updated unit after the operation, or null if no document matched the filter.</returns>
public async Task<Unit?> UpdateUnit(Unit unit)
{
var filter = Builders<Unit>.Filter.Eq("_id", unit.Id);
var update = Builders<Unit>.Update
//.Set(c => c.Id, unit.Id)
.Set(c => c.Title, unit.Title)
.Set(c => c.Name, unit.Name)
.Set(c => c.Configuration, unit.Configuration)
.Set(c => c.AllergyListId, unit.AllergyListId)
.Set(c => c.DestinationListId, unit.DestinationListId)
.Set(c => c.InternalDestinationListId, unit.InternalDestinationListId)
.Set(c => c.DiagnosisListId, unit.DiagnosisListId)
.Set(c => c.DoctorListId, unit.DoctorListId)
.Set(c => c.DoctorTypeListId, unit.DoctorTypeListId)
.Set(c => c.InsulationListId, unit.InsulationListId)
.Set(c => c.MobilityOptionListId, unit.MobilityOptionListId)
.Set(c => c.OriginListId, unit.OriginListId)
.Set(c => c.PatientStatusListId, unit.PatientStatusListId)
.Set(c => c.ProcedureListId, unit.ProcedureListId)
.Set(c => c.TestListId, unit.TestListId)
.Set(c => c.ServiceListId, unit.ServiceListId)
.Set(c => c.TherapeuticCeilingListId, unit.TherapeuticCeilingListId)
.Set(c => c.TreatmentListId, unit.TreatmentListId)
.Set(c => c.VisitOptionListId, unit.VisitOptionListId)
.Set(c => c.AccessControlListId, unit.AccessControlListId)
.Set(c => c.DischargeStatusListId, unit.DischargeStatusListId);
return await Collection.FindOneAndUpdateAsync(filter, update,
new FindOneAndUpdateOptions<Unit, Unit> { ReturnDocument = ReturnDocument.After });
}
/// <summary>
/// Updates the <see cref="Unit.Name"/> and <see cref="Unit.Title"/> fields of the unit identified by <paramref name="unitId"/>.
/// Returns the updated unit document, or <c>null</c> if no unit with the specified id exists in the collection.
/// </summary>
/// <param name="unitId">The unique identifier of the unit to update.</param>
/// <param name="name">The new name to assign to the unit.</param>
/// <param name="title">The new title to assign to the unit.</param>
/// <returns>The updated <see cref="Unit"/> after the modification, or <c>null</c> if no matching unit was found.</returns>
public async Task<Unit?> UpdateUnitInfo(ObjectId unitId, string name, string title)
{
var filter = Builders<Unit>.Filter.Eq("_id", unitId);
var update = Builders<Unit>.Update
//.Set(c => c.Id, unit.Id)
.Set(c => c.Title, title)
.Set(c => c.Name, name);
return await Collection.FindOneAndUpdateAsync(filter, update,
new FindOneAndUpdateOptions<Unit, Unit> { ReturnDocument = ReturnDocument.After });
}
/// <summary>
/// Updates the master list references on a <see cref="Unit"/> document identified by the supplied id,
/// applying the provided <c>MasterListId</c> values to the appropriate fields based on each entry's
/// <see cref="MasterListType"/>. Returns the updated <see cref="Unit"/> when at least one update is applied,
/// or <c>null</c> when no valid master list entries are provided.
/// </summary>
/// <param name="updateUnitListDto">The DTO containing the target <c>UnitId</c> and the collection of master list entries to update.</param>
/// <returns>The updated <see cref="Unit"/> after the modifications, or <c>null</c> if no updates were performed.</returns>
public async Task<Unit?> UpdateUnitMasterList(UpdateUnitIdListDto updateUnitListDto)
{
var filter = Builders<Unit>.Filter.Eq("_id", updateUnitListDto.UnitId);
var update = Builders<Unit>.Update;
var updates = new List<UpdateDefinition<Unit>>();
foreach (var masterListData in updateUnitListDto.MasterListData)
if (masterListData.MasterListType.HasValue)
switch (masterListData.MasterListType.Value)
{
case MasterListType.AltableOptionList:
updates.Add(update.Set(u => u.AltableOptionListId, masterListData.MasterListId));
break;
case MasterListType.AllergyList:
updates.Add(update.Set(u => u.AllergyListId, masterListData.MasterListId));
break;
case MasterListType.DestinationList:
updates.Add(update.Set(u => u.DestinationListId, masterListData.MasterListId));
break;
case MasterListType.DiagnosisList:
updates.Add(update.Set(u => u.DiagnosisListId, masterListData.MasterListId));
break;
case MasterListType.DischargeStatusList:
updates.Add(update.Set(u => u.DischargeStatusListId, masterListData.MasterListId));
break;
case MasterListType.DoctorList:
updates.Add(update.Set(u => u.DoctorListId, masterListData.MasterListId));
break;
case MasterListType.DoctorTypeList:
updates.Add(update.Set(u => u.DoctorTypeListId, masterListData.MasterListId));
break;
case MasterListType.InternalDestinationList:
updates.Add(update.Set(u => u.InternalDestinationListId, masterListData.MasterListId));
break;
case MasterListType.InsulationList:
updates.Add(update.Set(u => u.InsulationListId, masterListData.MasterListId));
break;
case MasterListType.LanguageBarrierList:
updates.Add(update.Set(u => u.LanguageBarrierListId, masterListData.MasterListId));
break;
case MasterListType.PassiveSittingList:
updates.Add(update.Set(u => u.PassiveSittingListId, masterListData.MasterListId));
break;
case MasterListType.GenericList:
updates.Add(update.Set(u => u.GenericListId, masterListData.MasterListId));
break;
case MasterListType.MobilityOptionList:
updates.Add(update.Set(u => u.MobilityOptionListId, masterListData.MasterListId));
break;
case MasterListType.OriginList:
updates.Add(update.Set(u => u.OriginListId, masterListData.MasterListId));
break;
case MasterListType.PatientStatusList:
updates.Add(update.Set(u => u.PatientStatusListId, masterListData.MasterListId));
break;
case MasterListType.ProcedureList:
updates.Add(update.Set(u => u.ProcedureListId, masterListData.MasterListId));
break;
case MasterListType.TestList:
updates.Add(update.Set(u => u.TestListId, masterListData.MasterListId));
break;
case MasterListType.ServiceList:
updates.Add(update.Set(u => u.ServiceListId, masterListData.MasterListId));
break;
case MasterListType.TherapeuticCeilingList:
updates.Add(update.Set(u => u.TherapeuticCeilingListId, masterListData.MasterListId));
break;
case MasterListType.TreatmentList:
updates.Add(update.Set(u => u.TreatmentListId, masterListData.MasterListId));
break;
case MasterListType.VisitOptionList:
updates.Add(update.Set(u => u.VisitOptionListId, masterListData.MasterListId));
break;
case MasterListType.AccessControlList:
updates.Add(update.Set(u => u.AccessControlListId, masterListData.MasterListId));
break;
}
if (updates.Any())
{
var combinedUpdate = update.Combine(updates);
return await Collection.FindOneAndUpdateAsync(filter, combinedUpdate,
new FindOneAndUpdateOptions<Unit, Unit> { ReturnDocument = ReturnDocument.After });
}
return null;
}
/// <summary>
/// Asynchronously updates the configuration of an existing unit identified by its ID.
/// Returns <c>true</c> if the document was modified, or <c>false</c> if the update failed or no document matched.
/// </summary>
/// <param name="unitIdParsed">The <see cref="ObjectId"/> of the unit whose configuration will be updated.</param>
/// <param name="unitConfiguration">The new <see cref="UnitConfiguration"/> to apply to the unit.</param>
/// <returns>A task that resolves to <c>true</c> when the update modified a document; otherwise <c>false</c> (including when the operation throws and the error is logged).</returns>
public async Task<bool> UpdateConfiguration(ObjectId unitIdParsed, UnitConfiguration unitConfiguration)
{
var filter = Builders<Unit>.Filter.Eq("_id", unitIdParsed);
var update = Builders<Unit>.Update
.Set(c => c.Configuration, unitConfiguration);
try
{
var result = await Collection.UpdateOneAsync(filter, update);
return result.ModifiedCount > 0;
}
catch (Exception ex)
{
Log.Error("Error UpdateConfiguration from unit: {name}. Exception: {ex}", unitIdParsed, ex);
return false;
}
}
#endregion
#region Delete
/// <summary>
/// Deletes the unit identified by the specified identifier, returning the deleted entity. If the operation fails, the exception is logged and <c>null</c> is returned.
/// </summary>
/// <param name="id">The identifier of the unit to delete.</param>
/// <returns>The deleted <see cref="Unit"/> if found and removed; otherwise, <c>null</c> when an error occurs.</returns>
public new async Task<Unit?> DeleteAsync(ObjectId id)
{
var filter = Builders<Unit>.Filter.Eq(unit => unit.Id, id);
try
{
return await Collection.FindOneAndDeleteAsync(filter);
}
catch (Exception e)
{
Log.Error(e.Message);
return null;
}
}
#endregion
#endregion
}