=== Summary: 675 files | 7 generated | 484 fresh | 4605 untracked | 4268 adopted | 355 marked | 466 validated-ok | 2+0 stale (sig+body) | 0 skipped | 0 failed | elapsed 11:08:11.442 (40091.44s) ===

This commit is contained in:
julian
2026-06-28 02:50:05 -07:00
parent a19fb90902
commit 586f02a2ca
654 changed files with 5260 additions and 0 deletions
@@ -21,6 +21,7 @@ namespace adas_core.Infrastructure.Repositories;
/// Repository implementation for managing <see cref="PatientObservation"/> entities in MongoDB.
/// Provides specialized query, aggregation, retention, and expiration operations for patient clinical observations.
/// </summary>
/// <!-- aidoc:v1 sig=70efcb8 -->
public class ObservationRepository : MongoRepository<PatientObservation>, IObservationRepository
{
private readonly ApiSettings _apiSettings;
@@ -34,6 +35,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="database">The MongoDB database instance used to access the collection.</param>
/// <param name="logger">The logger used to record diagnostic and error information.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="apiSettings"/> is <see langword="null"/>.</exception>
/// <!-- aidoc:v1 sig=5a66d34 body=2164183 -->
public ObservationRepository(
IOptions<ApiSettings>? apiSettings,
IMongoDatabase database,
@@ -56,6 +58,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// Falls back to the default "patients_observations" collection name when not configured in the API settings.
/// </summary>
/// <returns>The collection name retrieved from the API settings, or "patients_observations" if not configured.</returns>
/// <!-- aidoc:v1 sig=94e22ff body=23fb24b -->
public override string GetCollectionName()
{
return _apiSettings.PatientsObservations ?? "patients_observations";
@@ -70,6 +73,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="code">The code identifying the observation type within the coding system.</param>
/// <param name="num">The maximum number of recent observations to return. Defaults to 2.</param>
/// <returns>An <see cref="IEnumerable{PatientObservation}"/> containing the matching observations ordered from newest to oldest.</returns>
/// <!-- aidoc:v1 sig=404377e body=0480966 -->
public async Task<IEnumerable<PatientObservation>> FindLastObservations(ObjectId patientId, string codingSystem,
string code, int num = 2)
{
@@ -96,6 +100,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="codingSystem">The coding system to filter observations by.</param>
/// <param name="num">The maximum number of recent observations to return. Defaults to 10.</param>
/// <returns>A <see cref="List{PatientObservation}"/> containing the matching observations ordered from newest to oldest.</returns>
/// <!-- aidoc:v1 sig=09172c7 body=f40336d -->
public async Task<List<PatientObservation>> FindLastObservationsByCodingSystem(ObjectId patientId,
string codingSystem, int num = 10)
{
@@ -121,6 +126,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="num">The maximum number of observations to return per observation name.</param>
/// <param name="filterObservations">Optional list of observation names to restrict the query to. When <see langword="null"/>, all distinct names are discovered.</param>
/// <returns>A <see cref="List{PatientObservation}"/> containing the aggregated latest observations across all matching names.</returns>
/// <!-- aidoc:v1 sig=0f4e126 body=0b43f32 -->
public async Task<List<PatientObservation>> AggregatedPatientLastObservations(ObjectId patientId, int num,
List<string>? filterObservations = null)
{
@@ -157,6 +163,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="lastDate">The inclusive upper bound (UTC) for the observation <c>time</c> field.</param>
/// <param name="filterObservations">Optional list of observation names to restrict the query to. When <see langword="null"/>, all distinct names are discovered.</param>
/// <returns>A <see cref="List{PatientObservation}"/> containing the matching observations.</returns>
/// <!-- aidoc:v1 sig=605f95b body=2f3441d -->
public async Task<List<PatientObservation>> AggregatedPatientLastObservationsByLastDate(ObjectId patientId, int num,
DateTime lastDate, List<string>? filterObservations = null)
{
@@ -195,6 +202,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// A <see cref="List{PatientObservation}"/> containing the matching observations.
/// Returns an empty list when an exception occurs while querying the database.
/// </returns>
/// <!-- aidoc:v1 sig=442f63d body=77c0486 -->
public async Task<List<PatientObservation>> AggregatedPatientLastObservationsByField(ObjectId patientId,
List<Field>? filterObservations)
{
@@ -266,6 +274,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// A <see cref="List{BsonDocument}"/> with one document per time bucket.
/// Returns an empty list when an exception occurs while executing the pipeline.
/// </returns>
/// <!-- aidoc:v1 sig=43cc5aa body=792314e -->
public async Task<List<BsonDocument>> AggregatedPatientGroupedObservations(ObjectId patientId,
GroupedField groupedField)
{
@@ -550,6 +559,8 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>A <see cref="Task"/> representing the asynchronous insert operation.</returns>
/// <exception cref="MongoWriteException">Rethrown after the maximum number of retries has been reached when a duplicate-key error keeps occurring.</exception>
/// <exception cref="Exception">Rethrown when an unexpected error occurs during the insert operation.</exception>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "The summary states the method retries 'with a new ObjectId' on duplicate-key errors, but the catch block never assigns or generates a new ObjectId — the same patientObservation is passed to InsertOneAsync unchanged on each retry." -->
public new async Task InsertOneAsync(PatientObservation patientObservation)
{
const int maxRetries = 2; // Número máximo de reintentos
@@ -586,6 +597,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the patient whose observations should be removed.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous delete operation.</returns>
/// <!-- aidoc:v1 sig=09e4e41 body=0e8105c -->
public async Task DeleteByPatientId(ObjectId id)
{
var filter = Builders<PatientObservation>.Filter.Eq(po => po.PatientId, id);
@@ -599,6 +611,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>
/// An <see cref="IAsyncCursor{PatientObservation}"/> that can be enumerated to retrieve the patient's observations.
/// </returns>
/// <!-- aidoc:v1 sig=3727a3e body=9f79ea7 -->
public async Task<IAsyncCursor<PatientObservation>> FindByPatientIdAsync(ObjectId patientId)
{
var filter = Builders<PatientObservation>.Filter.Eq(ob => ob.PatientId, patientId);
@@ -615,6 +628,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// An <see cref="IAsyncCursor{PatientObservation}"/> containing the matching observations,
/// retrieved with a server-side batch size of 100.
/// </returns>
/// <!-- aidoc:v1 sig=6e7afc6 body=3957510 -->
public async Task<IAsyncCursor<PatientObservation>> FindByPatientIdAndCodingSystemAsync(ObjectId patientId,
string codingSystem, string name)
{
@@ -636,6 +650,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the observation to delete.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous delete operation.</returns>
/// <!-- aidoc:v1 sig=78c6c5d body=8d17292 -->
public new async Task DeleteAsync(ObjectId id)
{
var filter = Builders<PatientObservation>.Filter.Eq(obs => obs.Id, id);
@@ -650,6 +665,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="name">The observation name to filter by.</param>
/// <param name="retentionPolicyValue">The retention window expressed in days. Observations older than <c>UtcNow - retentionPolicyValue</c> days are removed.</param>
/// <returns>A <see cref="List{PatientObservation}"/> containing the documents that were deleted.</returns>
/// <!-- aidoc:v1 sig=db9f78a body=38e1006 -->
public async Task<List<PatientObservation>> DeleteOlderDaysAsync(string name, int retentionPolicyValue)
{
var dateLimit = DateTime.UtcNow.AddDays(-1 * retentionPolicyValue);
@@ -672,6 +688,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="name">The observation name to filter by.</param>
/// <param name="retentionPolicyValue">The retention window expressed in seconds. Observations older than <c>UtcNow - retentionPolicyValue</c> seconds are removed.</param>
/// <returns>A <see cref="List{PatientObservation}"/> containing the documents that were deleted.</returns>
/// <!-- aidoc:v1 sig=9b16832 body=8605782 -->
public async Task<List<PatientObservation>> DeleteOlderSecondsAsync(string name, int retentionPolicyValue)
{
var dateLimit = DateTime.UtcNow.AddSeconds(-1 * retentionPolicyValue);
@@ -694,6 +711,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="name">The observation name to apply the retention policy to.</param>
/// <param name="retentionPolicyValue">The maximum number of observations to retain. The remainder are deleted.</param>
/// <returns>A <see cref="List{PatientObservation}"/> containing the documents that were deleted. Returns an empty list when nothing had to be deleted.</returns>
/// <!-- aidoc:v1 sig=c385e27 body=8410fad -->
public async Task<List<PatientObservation>> DeleteOlderNumberAsync(string name, int retentionPolicyValue)
{
var builder = Builders<PatientObservation>.Filter;
@@ -724,6 +742,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="patientid">The unique identifier of the patient.</param>
/// <param name="systemId">The external system identifier to look up.</param>
/// <returns><see langword="true"/> if a matching observation exists; otherwise, <see langword="false"/>.</returns>
/// <!-- aidoc:v1 sig=039b8ae body=13a2d36 -->
public async Task<bool> ExistBySystemId(ObjectId patientid, string systemId)
{
return await Collection.Find(Builders<PatientObservation>.Filter.And(
@@ -745,6 +764,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// A <see cref="Task{PatientObservation}"/> representing the asynchronous operation.
/// The task result contains the most recent matching observation, or <see langword="null"/> if no observation matches.
/// </returns>
/// <!-- aidoc:v1 sig=6152e35 body=332a506 -->
public async Task<PatientObservation?> FindLastObservationBeforeDate(ObjectId patientId, string? name,
DateTime date)
{
@@ -772,6 +792,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// A <see cref="Task{List{PatientObservation}}"/> representing the asynchronous operation.
/// The task result contains a list of matching observations, which may be empty.
/// </returns>
/// <!-- aidoc:v1 sig=87691b0 body=2ac50f6 -->
public async Task<List<PatientObservation>?> FindAnyWithSameDate(ObjectId patientId, string? name, DateTime date)
{
var builder = Builders<PatientObservation>.Filter;
@@ -792,6 +813,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>
/// A <see cref="List{PatientObservation}"/> containing the matching observations, which may be empty.
/// </returns>
/// <!-- aidoc:v1 sig=c25141b body=58a25f4 -->
public async Task<List<PatientObservation>> FindAnyBeforeDate(ObjectId patientId, DateTime date)
{
var filterBuilder = Builders<PatientObservation>.Filter;
@@ -817,6 +839,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>
/// A <see cref="List{PatientObservation}"/> containing the most recent observation for each distinct value.
/// </returns>
/// <!-- aidoc:v1 sig=fbdab74 body=42ee291 -->
public async Task<List<PatientObservation>> FindLatestUniqueValuesByName(ObjectId patientId, string name,
int? expires)
{
@@ -867,6 +890,8 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>
/// A <see cref="List{PatientObservation}"/> containing one observation per unique (Location, Type) combination.
/// </returns>
/// <!-- aidoc-review:v1 severity=low kind=wrong_returns
/// "The actual return type is List<PatientObservation?> (nullable elements due to LastOrDefault()), but the doc references List{PatientObservation} without indicating nullability." -->
public async Task<List<PatientObservation?>> AggregatedPatientActiveIntravenousLinesObservations(ObjectId patientId)
{
var results = new List<PatientObservation>();
@@ -899,6 +924,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>
/// A <see cref="Dictionary{ObjectId, DateTime}"/> mapping each patient's <see cref="ObjectId"/> to the UTC timestamp of their latest observation.
/// </returns>
/// <!-- aidoc:v1 sig=115ac00 body=29b2957 -->
public async Task<Dictionary<ObjectId, DateTime>> FindAllLastPatientObservationTime()
{
var group = new BsonDocument
@@ -950,6 +976,8 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// A <see cref="Task{PatientObservation}"/> representing the asynchronous operation.
/// The task result contains the <see cref="PatientObservation"/> if found; otherwise, <see langword="null"/>.
/// </returns>
/// <!-- aidoc-review:v1 severity=low kind=wrong_returns
/// "The cref references Task{PatientObservation} but the actual return type is Task<PatientObservation?}; the textual description correctly notes null behavior but the cref itself is missing the nullable annotation." -->
public async Task<PatientObservation?> FindById(ObjectId id)
{
var filter = Builders<PatientObservation>.Filter.Eq(o => o.Id, id);
@@ -967,6 +995,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// A <see cref="Task{List{PatientObservation}}"/> representing the asynchronous operation.
/// The task result contains a list of observations, which may be empty.
/// </returns>
/// <!-- aidoc:v1 sig=4837300 body=9950647 -->
public async Task<List<PatientObservation>?> FindByPatientId(ObjectId id)
{
var filter = Builders<PatientObservation>.Filter.Eq(o => o.PatientId, id);
@@ -982,6 +1011,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// </summary>
/// <param name="observation">The <see cref="PatientObservation"/> whose <see cref="ObjectId"/> identifies the document to update.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
/// <!-- aidoc:v1 sig=738ce4b body=caca1ff -->
public async Task Update(PatientObservation observation)
{
await UpdateOneAsync(observation.Id, observation);
@@ -995,6 +1025,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="id">The new <see cref="ObjectId"/> value to assign.</param>
/// <param name="oldId">The existing <see cref="ObjectId"/> value to replace.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
/// <!-- aidoc:v1 sig=72ce1ca body=b9b81e9 -->
public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
{
await UpdateManyObjectIdAsync(nameId, id, oldId);
@@ -1005,6 +1036,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// </summary>
/// <param name="expiredObservations">The list of <see cref="PatientObservation"/> instances to mark as expired. The set of identifiers is used to build the update filter.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
/// <!-- aidoc:v1 sig=c0d4f8c body=2cbb9ec -->
public async Task UpdateExpiredObservations(List<PatientObservation> expiredObservations)
{
var filter = Builders<PatientObservation>.Filter
@@ -1020,6 +1052,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <param name="patientObservations">The observations whose identifiers form the target set of the update.</param>
/// <param name="update">The <see cref="UpdateDefinition{PatientObservation}"/> describing the changes to apply to each matching document.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
/// <!-- aidoc:v1 sig=9ef4ae8 body=5f5bfaf -->
public async Task UpdateMany(IEnumerable<PatientObservation> patientObservations,
UpdateDefinition<PatientObservation> update)
{
@@ -1034,6 +1067,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>
/// An <see cref="IEnumerable{PatientObservation}"/> containing all observations.
/// </returns>
/// <!-- aidoc:v1 sig=8ac3146 body=bff76bc -->
public async Task<IEnumerable<PatientObservation>> FindAll()
{
var result = await Collection.FindAsync(_ => true);
@@ -1051,6 +1085,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// (in minutes). Observations whose <c>time</c> is older than <c>DateTime.Now - expires</c> minutes are marked as expired.
/// </param>
/// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
/// <!-- aidoc:v1 sig=3c6bca9 body=e2d9f44 -->
public async Task ExpireExpiredObservations(
List<ConfigObservation> configObservationsToExpire)
{
@@ -1095,6 +1130,10 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>
/// An <see cref="IEnumerable{PatientObservation}"/> containing the matching non-expired observations.
/// </returns>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "Summary states 'Observations with a null name are excluded from the result' unconditionally, but the null-name filter (Not(Eq(o => o.Name, null))) is only added inside the `if (filterObservations != null)` branch. When filterObservations is null, null-named observations are NOT excluded." -->
/// <!-- aidoc-review:v1 severity=high kind=mentions_removed_behavior
/// "Param description claims 'When null, all non-null named observations are considered', but when filterObservations is null the code adds no name-related filters at all - null-named observations are also returned (subject only to the not-expired filter)." -->
public async Task<IEnumerable<PatientObservation>> FindNotExpired(List<string?>? filterObservations)
{
var filterBuilder = Builders<PatientObservation>.Filter;
@@ -1129,6 +1168,10 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// Thrown when <paramref name="name"/> is null, empty, or whitespace,
/// or when its length exceeds 100 characters.
/// </exception>
/// <!-- aidoc-review:v1 severity=high kind=wrong_param_role
/// "<paramref name=\"fromDate\"/> is documented as 'inclusive lower bound', but the code uses Builders<>.Filter.Gt (strict greater-than), making it an exclusive lower bound." -->
/// <!-- aidoc-review:v1 severity=high kind=wrong_param_role
/// "<paramref name=\"name\"/> is documented as 'The regex pattern to match', but Regex.Escape(name) is applied, so it is matched as an escaped literal substring, not as a raw regex pattern." -->
public async Task<IEnumerable<PatientObservation>> FindByName(string name, DateTime? fromDate = null)
{
if (string.IsNullOrWhiteSpace(name))
@@ -1167,6 +1210,8 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>
/// An <see cref="IFindFluent{PatientObservation, PatientObservation}"/> instance that can be used to further refine and execute the query.
/// </returns>
/// <!-- aidoc-review:v1 severity=medium kind=wrong_summary
/// "The summary describes the query as 'paginated', but the code applies only sort and filter; no Skip/Limit (or other pagination) is performed on the IFindFluent." -->
public IFindFluent<PatientObservation, PatientObservation> GetPaginatedObservations(PaginationFilter filter)
{
// Crear variable con la clase que construye los filtros
@@ -1214,6 +1259,10 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// <returns>
/// An <see cref="IEnumerable{PatientObservation}"/> containing the matching observations ordered from newest to oldest.
/// </returns>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "The summary states results are 'optionally bounded to a recent time window', but the code never applies the computed date filter to the query: `filterBuilder.And(dateFilter)` is invoked inside the `if (endAfter.HasValue)` block but its return value is discarded, so `endAfter` has no effect on the returned observations." -->
/// <!-- aidoc-review:v1 severity=high kind=mentions_removed_behavior
/// "The <param name=\"endAfter\"> description claims only observations with time >= UtcNow - endAfter are returned, yet the constructed `dateFilter` is never combined into the final `filters` passed to `FindAsync`." -->
public async Task<IEnumerable<PatientObservation>> FindLastNotExpiredObservatonsByPatient(ObjectId patientId,
string name, int? endAfter = null, int? num = null)
{
@@ -1254,6 +1303,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous index creation operation.</returns>
/// <exception cref="Exception">Rethrown when an error occurs while creating the indexes.</exception>
/// <!-- aidoc:v1 sig=4955da2 body=2d25714 -->
public override async Task CreateIndexes()
{
try
@@ -1296,6 +1346,7 @@ public class ObservationRepository : MongoRepository<PatientObservation>, IObser
/// A <see cref="List{String}"/> containing the observation names to aggregate over.
/// Returns an empty list if no observations exist for the patient.
/// </returns>
/// <!-- aidoc:v1 sig=b7f7b8f body=54374c4 -->
private async Task<List<string>> AggregatePatientObservations(ObjectId patientId,
List<string>? filterObservations = null)
{