Conflicto de fusión en adas-core.LdapLogin/LdapLoginService.cs

This commit is contained in:
jrojas
2026-07-06 18:07:25 +02:00
2810 changed files with 1927407 additions and 25397 deletions
@@ -8,6 +8,12 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.BD;
/// <summary>
/// Represents a service that provides calculated observations, implementing the <see cref="ICalculatedObservations"/> interface.
/// </summary>
/// <remarks>
/// This type is instantiated with a primary constructor that accepts an <see cref="IServiceProvider"/> to resolve its dependencies.
/// </remarks>
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly ILogger<CalculatedObservations> _logger =
@@ -16,21 +22,43 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
private readonly Lazy<IObservationService> _observationService =
serviceProvider.GetRequiredService<Lazy<IObservationService>>();
/// <summary>
/// Maps the specified patient observation by returning it unchanged within a completed task.
/// The <paramref name="onlyByName"/> parameter is accepted to indicate whether the mapping should match observations by name only.
/// </summary>
/// <param name="obs">The patient observation to map.</param>
/// <param name="onlyByName">Indicates whether the mapping should match observations by name only.</param>
/// <returns>A <see cref="Task{T}"/> containing the provided observation.</returns>
public Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
return Task.FromResult(obs)!;
}
/// <summary>
/// Returns the provided <see cref="PatientTreatment"/> as a completed task without modification.
/// </summary>
/// <param name="treatment">The patient treatment instance to map.</param>
/// <returns>A completed <see cref="Task{PatientTreatment}"/> containing the provided treatment.</returns>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
return Task.FromResult(treatment);
}
/// <summary>
/// Maps the provided <see cref="PatientDiagnosis"/> instance to a completed <see cref="Task{TResult}"/>, returning the same diagnosis unchanged.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> instance to map.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the provided <see cref="PatientDiagnosis"/>.</returns>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
/// <summary>
/// Maps a pump observation, creating an associated alarm observation when the observation code is "IHE PCD-04".
/// </summary>
/// <param name="pumpObservation">The pump observation to be mapped.</param>
/// <returns>The mapped pump observation.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
if (pumpObservation.Code == "IHE PCD-04")
@@ -38,41 +66,92 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return pumpObservation;
}
/// <summary>
/// Calculates medicine observations for the specified patient using the provided list of active medicines.
/// </summary>
/// <param name="activeMedicines">The list of active medicines associated with the patient.</param>
/// <param name="patientId">The unique identifier of the patient for whom the observations are calculated.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active bolus is being calculated.</param>
/// <returns>A task that represents the asynchronous calculation of the active bolus.</returns>
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Retrieves the collection of active treatments associated with the specified patient identifier.
/// </summary>
/// <param name="id">The unique <see cref="ObjectId"/> identifier of the patient whose active treatments are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing an <see cref="IEnumerable{PatientTreatment}"/> of nullable <see cref="PatientTreatment"/> entries for the patient's active treatments.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the method is not yet implemented.</exception>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
throw new NotImplementedException();
}
/// <summary>
/// Resolves time inconsistencies between the provided new patient observation and the previously recorded one, returning a corrected observation when applicable.
/// </summary>
/// <param name="newObservation">The new patient observation to be reconciled against the last stored observation.</param>
/// <returns>A task that yields the time-corrected <see cref="PatientObservation"/>, or <c>null</c> when no correction is required or no prior observation exists.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
throw new NotImplementedException();
}
/// <summary>
/// Performs a pre-mapping operation on the list of patient observations before further processing.
/// In the base implementation, the list is returned unchanged, serving as a pass-through that may be overridden to apply custom transformations or validations.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped before insertion.</param>
/// <returns>A task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source <see cref="PatientObservationAlarm"/> onto an existing <see cref="PatientObservation"/>, producing an updated observation that incorporates the alarm data.
/// </summary>
/// <param name="obs">The existing patient observation to which the alarm will be mapped.</param>
/// <param name="alarmToInsert">The source alarm to be inserted/mapped into the observation.</param>
/// <returns>A <see cref="Task{PatientObservation}"/> that represents the asynchronous mapping operation, returning the resulting patient observation.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
throw new NotImplementedException();
}
/// <summary>
/// Sends an alarm notification based on a patient observation, associated name, and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code that categorizes the alarm; may be null.</param>
/// <returns>A task that represents the asynchronous alarm sending operation.</returns>
/// <exception cref="NotImplementedException">Thrown because the method is not yet implemented.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Creates and inserts a patient observation representing a pump alarm event.
/// The observation value is populated with the device identifier when available, otherwise the infusion identifier,
/// and the patient identifier is only assigned if present on the source pump observation.
/// Any exception raised while inserting the observation is logged and swallowed rather than rethrown.
/// </summary>
/// <param name="pumpObservation">The pump observation containing the alarm state, type, timestamps, and identifiers used to build the alarm patient observation.</param>
/// <returns>A completed <see cref="Task"/> representing the insert operation, which never faults since exceptions are caught internally.</returns>
private Task CreatePumpAlarmObservation(PumpObservation pumpObservation)
{
PatientObservation patientObservationAlarm = new()
@@ -85,10 +164,10 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
: $"Infusion Id {pumpObservation.InfusionId}",
Time = pumpObservation.Time
};
if (pumpObservation.PatientId.HasValue) patientObservationAlarm.PatientId = pumpObservation.PatientId.Value;
//CheckAlarmConfig(patientObservationAlarm);
try
{
@@ -98,7 +177,7 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
{
_logger.LogError("Error inserting Pump Observation Alarm. Exception: {e}", e);
}
return Task.CompletedTask;
}
}
@@ -8,37 +8,75 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.CHUO;
/// <summary>
/// Provides calculated observation data by implementing the <see cref="ICalculatedObservations"/> contract.
/// </summary>
/// <remarks>
/// Instances are constructed with an <see cref="IServiceProvider"/> used to resolve dependencies required by the implementation.
/// </remarks>
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly ILogger<CalculatedObservations> _logger =
serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient for whom the active bolus is being calculated.</param>
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Asynchronously maps the provided <see cref="PumpObservation"/> instance, returning the same instance as the result.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A <see cref="Task{PumpObservation}"/> containing the provided pump observation.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Calculates medicine observations for the specified patient based on the provided active medicines.
/// </summary>
/// <param name="activeMedicines">The list of medicines currently active for the patient.</param>
/// <param name="patientId">The identifier of the patient for whom the observation is calculated.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Resolves time inconsistencies between a new patient observation and the most recent one, returning a corrected observation when a discrepancy is detected.
/// </summary>
/// <param name="newObservation">The new patient observation to compare and reconcile against the last stored observation.</param>
/// <returns>A task that yields the corrected <see cref="PatientObservation"/>, or <c>null</c> when no time inconsistency is found.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
throw new NotImplementedException();
}
/// <summary>
/// Retrieves the active treatment records associated with the specified patient.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments are being requested.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of nullable <see cref="PatientTreatment"/> objects representing the patient's active treatments.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the method has not yet been implemented.</exception>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a patient observation by returning it as-is, logging the operation as a trace entry.
/// </summary>
/// <param name="obs">The patient observation to map.</param>
/// <param name="onlyByName">Indicates whether mapping should be performed by name only.</param>
/// <returns>A task containing the provided observation.</returns>
public Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
_logger.LogTrace("Mapping {name} observation {obs}", obs.Name, obs);
@@ -47,26 +85,56 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Maps the specified <see cref="PatientTreatment"/> to a resulting <see cref="PatientTreatment"/> instance asynchronously.
/// </summary>
/// <param name="treatment">The patient treatment to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the resulting <see cref="PatientTreatment"/>.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the mapping has not been implemented.</exception>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a <see cref="PatientDiagnosis"/> instance to a <see cref="PatientDiagnosis"/> result.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to be mapped.</param>
/// <returns>A task representing the asynchronous operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown because the mapping logic has not yet been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Performs a pre-mapping step on a list of patient observations before they are inserted.
/// Currently acts as a pass-through, returning the provided list unchanged as a completed task, but can be overridden to apply transformations or validations.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped prior to insertion.</param>
/// <returns>A completed task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm onto a <see cref="PatientObservation"/>, returning the observation as-is without applying the alarm's changes.
/// </summary>
/// <param name="obs">The existing patient observation to be returned.</param>
/// <param name="alarmToInsert">The alarm to be associated with the observation (currently not applied).</param>
/// <returns>A completed <see cref="Task{PatientObservation}"/> containing the original observation.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm notification for the specified patient observation, identified by the given name and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name of the alarm to send.</param>
/// <param name="code">The optional alarm code providing additional classification for the alarm.</param>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
return Task.CompletedTask;
@@ -10,6 +10,13 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.H12O.UCIN;
/// <summary>
/// Implements the UCIN-specific clinical calculations that derive secondary
/// <see cref="PatientObservation"/> values (Complexity, Oxygenation_Index, Respiratory, IntravenousLines,
/// Medication, ERMedication, Monitor, Surgery, Temp_Gradient, TAm alert, etc.) from incoming
/// raw observations and active <see cref="PatientTreatment"/> entries, using the catalogue of
/// codes, groups, and types configured in <see cref="ApiSettings"/>.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private readonly List<string> _complexityObservations =
@@ -72,6 +79,14 @@ public class CalculatedObservations : ICalculatedObservations
private readonly Lazy<ITreatmentService> _treatmentService;
/// <summary>
/// Initializes a new instance of the <see cref="CalculatedObservations"/> class,
/// resolving its dependencies (observation, medicine, and treatment services, plus the logger)
/// from the supplied <see cref="IServiceProvider"/> and loading the configured code catalogues
/// (EEG, bolus medications, transcutaneous, regional brain saturation, surgery, respiratory,
/// ONi, ventilation modes, ECMO, and notes indicating medication) from <see cref="ApiSettings"/>.
/// </summary>
/// <param name="serviceProvider">The application's service provider used to resolve the required dependencies and configuration.</param>
public CalculatedObservations(IServiceProvider serviceProvider)
{
_observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>(); //observationService;
@@ -127,58 +142,82 @@ public class CalculatedObservations : ICalculatedObservations
ecmo?.ForEach(x => _ecmo.Add(x.Trim()));
}
/// <summary>
/// Applies the appropriate clinical calculations to a raw patient observation based on its
/// <see cref="BasePatientObservation.Name"/>, code, coding system, and expiration state, producing
/// derived observations (respiratory assistance, oxygenation index, ventilation mode, TAm alert,
/// parsed weight, temperature gradient, intravenous lines, monitor, surgery expiration, complexity,
/// and hourly/temperature time-increment handling).
/// </summary>
/// <typeparam name="T">The concrete observation type, deriving from <see cref="BasePatientObservation"/>.</typeparam>
/// <param name="obs">The observation to map or transform in-place.</param>
/// <param name="onlyByName">
/// Reserved for future use. When <see langword="true"/>, restricts the mapping strategy to
/// name-based lookups only.
/// </param>
/// <returns>
/// A <see cref="Task{T}"/> that resolves to the (possibly transformed) observation, or
/// <see langword="null"/> when the input observation has no name.
/// </returns>
public async Task<T?> Map<T>(T obs, bool onlyByName) where T : BasePatientObservation
{
// Early exit: if the observation has no name, there's nothing to map
if (string.IsNullOrEmpty(obs.Name)) return obs;
//Llega Vías o algo en Vías dictionary
//Solo tenemos en cuenta las observaciones de ICA
// Respiratory observations: only process SNOMED-coded respiratory metrics
if (!string.IsNullOrEmpty(obs.Code) && _respiratory.Contains(obs.Code) && obs.CodingSystem == "SNM")
{
_logger.LogDebug("Mapping observation Respiratorio {obs}", obs);
await CalculateAsistResp(obs);
}
// Oxygenation index calculation: triggered by air pressure, FiO2, or PaO2 readings
if (obs.Name is "AirPressure_Mean" or "FiO2" or "PaO2") await CalculateOxygenationIndex(obs);
//Solo tenemos en cuenta las observaciones de Central
// Ventilation mode evaluation: processes respiratory mode changes
if (obs.Name == "Resp_Mode")
{
_logger.LogDebug("Mapping Resp mode observation {obs}", obs);
await CalculateVentilationMode(obs);
}
// Mean arterial pressure (TAm) alerting: includes gestational age considerations
if (obs.Name is "TAm" or "Age_Gestational_Fixed" or "Age_Gestational") await CalculateTAmAlert(obs);
// Weight parsing: only for active (non-expired) patient observations
if (obs.Name is "Weight_Newborn" or "Weight_Current")
if (obs is PatientObservation { Expired: false })
obs = (T)await ParseWeight(obs);
// Temperature gradient calculation between incubator and patient
if (obs is { Name: "Temp_Incubator" } or { Name: "Temp_Patient" }) await CalculateTempGradient(obs);
// Intravenous lines observation processing with potential transformation
if (obs is { Name: "IntravenousLinesObs" })
{
var intraObs = await CalculateIntravenousLineObservation(obs);
// Apply the transformed observation if calculation produced a result
if (intraObs != null)
obs = (T)intraObs;
}
// Regional brain saturation monitoring: pCO2tc or transcutaneous O2 with specific codes
if (obs.Name is "pCO2tc" or "Transcutaneous_O2")
if (obs.Code != null && _regionalBrainSaturation.Contains(obs.Code))
await CalculateMonitor(obs);
// Surgery-related expiration check
if (obs.Name == "Surgery") await CheckSurgeryExpired(obs);
// Complexity calculations for predefined complex observation types
if (obs.Name != null && _complexityObservations.Contains(obs.Name))
{
_logger.LogDebug("Mapping observation Complexity {obs}", obs);
await CalculateComplexity(obs);
}
// Hourly accumulation observations: diuresis, drainages, hydric balance, and fluid entries
// These check for existing observations at the same time and increment values
if (obs.Name != null && (obs.Name.Equals("Diuresis_Hour") ||
obs.Name.Equals("Drainages_Hour") ||
obs.Name.Equals("Hydric_Balance") ||
@@ -186,13 +225,25 @@ public class CalculatedObservations : ICalculatedObservations
obs.Name.Equals("Enteral_Entries_Hour")))
obs = (T)await CheckObsExistsAndIncrementTime(obs);
// Temperature observations: handle duplicate time entries by incrementing time
if (obs.Name != null && (obs.Name.Equals("Temp_Patient") ||
obs.Name.Equals("Temp_Incubator")
))
obs = (T)await CheckObsWithSameTimeExistsAndIncrementTime(obs);
return obs;
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> to its processed form, computing the end time of single-dose
/// treatments, evaluating the medicines associated with the treatment's codes/notes, and dispatching
/// the appropriate downstream calculations (bolus, monitor, surgery, ECMO complexity, and ONi observations)
/// based on the configured code catalogues.
/// </summary>
/// <param name="treatment">The <see cref="PatientTreatment"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation. The task result contains the processed <see cref="PatientTreatment"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="treatment"/> parameter is <c>null</c>.</exception>
public async Task<PatientTreatment> Map(PatientTreatment treatment)
{
if (treatment.SingleDose)
@@ -258,18 +309,39 @@ public class CalculatedObservations : ICalculatedObservations
return treatment;
}
/// <summary>
/// Identity mapping for a <see cref="PumpObservation"/>: returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract; pump observations
/// do not currently require UCIN-specific calculation.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A task containing the same <see cref="PumpObservation"/> instance that was passed in.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Convenience overload that recalculates the "Medication" and "ERMedication" observations for a
/// patient using the supplied list of active medicines, building a transient <see cref="PatientTreatment"/>
/// for the underlying call.
/// </summary>
/// <param name="activeMedicines">The list of active <see cref="Medicine"/> instances currently associated with the patient.</param>
/// <param name="patientId">The unique identifier of the patient.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
await CalculateMedicineObservation(activeMedicines, [], new PatientTreatment { PatientId = patientId });
}
/// <summary>
/// Recalculates the "OpiateBoluses" observation for a patient based on the bolus treatments
/// returned by <c>GetActiveBolus</c>. Skips persistence when the value is unchanged from the
/// last stored observation.
/// </summary>
/// <param name="patientId">The unique identifier of the patient.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateActiveBolus(ObjectId patientId)
{
// SIN RXA
@@ -317,22 +389,42 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(bolusObs, mapObs: false);
}
/// <summary>
/// Retrieves all treatments currently considered active for the specified patient, delegating
/// the actual retrieval to the configured <see cref="ITreatmentService"/>.
/// </summary>
/// <param name="id">The unique identifier of the patient.</param>
/// <returns>A collection of active <see cref="PatientTreatment"/> objects for the patient.</returns>
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
return activeTreatments;
}
/// <summary>
/// Identity mapping for a <see cref="PatientDiagnosis"/>: returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract; diagnoses do not
/// currently require UCIN-specific calculation.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to map.</param>
/// <returns>A task containing the same <see cref="PatientDiagnosis"/> instance that was passed in.</returns>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
/*
* Designed to fix observations like intravenous with the possibility to receive multiple intravenous observations in the same hl7 message.
* Sometimes ADAS calculates many in the same second. causing inconsistencies when retrieving last observation.
*/
/// <summary>
/// Resolves time-inconsistencies for a new patient observation when it arrives with the same
/// (down-to-the-second) timestamp as the latest stored observation of the same name. The new
/// observation's <c>Time</c> is shifted forward by one second, and a fresh <see cref="ObjectId"/>
/// is generated to avoid duplicate-key collisions.
/// </summary>
/// <param name="newObservation">The new patient observation to evaluate for time inconsistencies.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains the fixed
/// <see cref="PatientObservation"/> if the input had a valid name; otherwise, <see langword="null"/>.
/// </returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -361,21 +453,54 @@ public class CalculatedObservations : ICalculatedObservations
return newObservation;
}
/// <summary>
/// Identity mapping for a list of <see cref="PatientObservation"/> instances: returns the supplied
/// list unchanged, wrapped in a completed task. Provided to satisfy the customization contract;
/// the pre-mapping phase does not currently require UCIN-specific transformation.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped.</param>
/// <returns>A task containing the original list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Identity mapping for a source alarm observation paired with a <see cref="PatientObservationAlarm"/>:
/// returns the supplied observation unchanged, wrapped in a completed task. Provided to satisfy the
/// customization contract; alarms are not currently transformed in the UCIN customization.
/// </summary>
/// <param name="obs">The patient observation that triggered the alarm.</param>
/// <param name="alarmToInsert">The alarm metadata to be inserted alongside the observation.</param>
/// <returns>A task containing the same <see cref="PatientObservation"/> instance that was passed in.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Placeholder alarm dispatch hook used by the customization contract. The UCIN customization
/// does not currently implement custom alarm dispatching; calling this method always throws.
/// </summary>
/// <param name="obs">The patient observation that triggered the alarm.</param>
/// <param name="name">The alarm display name.</param>
/// <param name="code">The optional alarm code from <see cref="AlarmEnum.Name"/>.</param>
/// <returns>Never returns a result.</returns>
/// <exception cref="NotImplementedException">Always thrown because alarm dispatch is not implemented in the UCIN customization.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// When a "Surgery" observation is marked as expired, inserts a follow-up "Surgery" observation
/// with value <c>0</c> at the same time plus one second, signaling that the surgery complexity
/// contribution has been removed. Errors are logged and rethrown.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with <c>Name == "Surgery"</c>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
/// <exception cref="InvalidCastException">Thrown when <paramref name="obs"/> cannot be cast to <see cref="PatientObservation"/>.</exception>
/// <exception cref="Exception">Rethrown after the underlying exception is written to the console for diagnostics.</exception>
public async Task CheckSurgeryExpired(BasePatientObservation obs)
{
try
@@ -404,9 +529,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/*
*Used for observations that can come with the same time, the last which enter is the newest. So increment time so that the system can identify which is the last.
*/
/// <summary>
/// For hourly accumulation observations, checks whether another observation with the same name
/// already exists within the same hour and, if so, shifts this observation's <c>Time</c> forward
/// by one second so the system can identify the most recent entry.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the (possibly time-shifted) base patient observation.</returns>
public async Task<BasePatientObservation> CheckObsExistsAndIncrementTime(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -423,7 +552,13 @@ public class CalculatedObservations : ICalculatedObservations
return pobs;
}
/// <summary>
/// For temperature observations, checks whether another observation with the same name already
/// exists at the exact same timestamp and, if so, shifts this observation's <c>Time</c> forward
/// by one second so the system can identify the most recent entry.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the (possibly time-shifted) base patient observation.</returns>
public async Task<BasePatientObservation> CheckObsWithSameTimeExistsAndIncrementTime(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -435,13 +570,15 @@ public class CalculatedObservations : ICalculatedObservations
return pobs;
}
/// <summary>
/// Oxygenation index is calculated => PMAP x FiO2 x 100 / PaO2
/// the method is called when it receives pmap or fio or pao2 and tries to take the other values to perform the
/// calculation if they are not
/// in database then does nothing.
/// Computes the Oxygenation Index (PMAP × FiO2 × 100 / PaO2) using the supplied observation plus
/// the latest stored <c>AirPressure_Mean</c>, <c>FiO2</c>, and <c>PaO2</c> values. Persists the
/// resulting <c>Oxygenation_Index</c> observation with a 10-second expiration matching FiO2's
/// expiration window. Skipped gracefully when PaO2 is zero, when any of the inputs cannot be
/// parsed, or when an exception is caught and logged.
/// </summary>
/// <param name="obs">The observation that triggered the calculation (one of <c>AirPressure_Mean</c>, <c>FiO2</c>, or <c>PaO2</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateOxygenationIndex(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -512,7 +649,12 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Sets the <c>EndTime</c> of a single-dose treatment to 8 hours after its <c>OrderTime</c>
/// (or 8 hours from now when <c>OrderTime</c> is <see langword="null"/>).
/// </summary>
/// <param name="treatment">The single-dose treatment whose end time is to be calculated.</param>
/// <returns>The same <see cref="PatientTreatment"/> instance with its <c>EndTime</c> updated.</returns>
private static PatientTreatment CalculateSingleDoseEndDate(PatientTreatment treatment)
{
/* Deprecated calc finish treatment date on shift end. Now every treatment single dose is last 8 Hours
@@ -540,6 +682,14 @@ public class CalculatedObservations : ICalculatedObservations
return treatment;
}
/// <summary>
/// Resolves the medicines associated with a treatment (by matching its codes and notes against the
/// medicine catalogue, falling back to <c>NotesIndicatingMedication</c> patterns), aggregates the
/// patient's active treatments and medicines, detects parenteral nutrition (NPT) treatments, and
/// triggers <c>CalculateMedicineObservation</c> to update the Medication and ERMedication observations.
/// </summary>
/// <param name="treatment">The treatment whose medicines should be checked.</param>
/// <returns>A task that represents the asynchronous check operation.</returns>
public async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
//sI TIENE UNA NOTA CON NPT SON DE TIPO NUTRICIÓN PARENTERAL Y SUMAN 1
@@ -627,6 +777,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Builds a synthetic <see cref="Medicine"/> representing parenteral nutrition (NPT) when the
/// treatment's notes indicate a NPT order. Lipid-based NPTs are tagged with
/// <c>ParenteralNutritionLipids</c>; all other NPTs are tagged with <c>ParenteralNutrition</c>.
/// </summary>
/// <param name="treatment">The treatment to inspect for NPT indicators, or <see langword="null"/>.</param>
/// <returns>A task that resolves to a <see cref="Medicine"/> instance describing the parenteral nutrition, or an unnamed, untyped instance when no NPT notes are present.</returns>
private static Task<Medicine> CalculateParentalNutritionMedicine(PatientTreatment? treatment)
{
List<string> medicineType = [];
@@ -651,13 +808,16 @@ public class CalculatedObservations : ICalculatedObservations
return Task.FromResult(medicine);
}
/*
* Cada vez que entra un tratamiento recalcular medicación y riesgo de medicación: recuperamos todos los tratamientos activos del paciente,
* siendo activos: todos aquellos que no han sido cancelados DC Y SIENDO NW NUEVO,
* Medicación: Si treatment.orderControl = NW entra nuevo sumamos 1 si no hay ninguno de ese tipo ya activo
* si entra treatment.orderControl = DC restamos 1 si no hay ninguno de ese tipo medición.type
* y
*/
/// <summary>
/// Maintains the patient's active medicine list according to the treatment's <c>OrderControl</c>
/// (<c>NW</c> adds, <c>XO</c> adds if missing, <c>DC</c> removes), counts the distinct medicine
/// types, persists the <c>Medication</c> observation if its value changed, and then recalculates
/// the <c>ERMedication</c> observation.
/// </summary>
/// <param name="activeMedicines">The list of active medicines for the patient. Modified in-place.</param>
/// <param name="medicines">The medicines to add or remove depending on <c>OrderControl</c>.</param>
/// <param name="treatment">The treatment that triggered the recalculation.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, List<Medicine> medicines,
PatientTreatment treatment)
{
@@ -708,7 +868,14 @@ public class CalculatedObservations : ICalculatedObservations
await CalculateErMedication(activeMedicines, treatment);
}
/// <summary>
/// Calculates the patient's risk level from their active medicines and persists the
/// <c>ERMedication</c> observation with min/max bounds of 0 and 5, skipping persistence
/// when the value matches the last stored observation.
/// </summary>
/// <param name="activeMedicines">The list of active medicines used to derive the risk level.</param>
/// <param name="treatment">The treatment that triggered the recalculation.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
private async Task CalculateErMedication(List<Medicine> activeMedicines, PatientTreatment treatment)
{
//var activeTreatments = treatmentService.Value.GetActiveTreatmentsByPatient(treatment.patientid);
@@ -733,7 +900,14 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(erMedicationObs, mapObs: false);
}
/// <summary>
/// Maps the patient's active medicine list to a discrete risk level (0 to 5) based on medicine
/// types and groups: lipidic parenteral nutrition or multiple high-risk medicines map to 5,
/// non-lipid NPT or one or two high-risk medicines map to 4, metabolic medicines map to 2,
/// remaining medicines map to 1, and a list containing only iron/vitamin D maps to 0.
/// </summary>
/// <param name="activeMedicines">The list of active medicines to evaluate.</param>
/// <returns>A task that resolves to the integer risk level (05).</returns>
private static Task<int> CalculateErMedicineLevels(List<Medicine> activeMedicines)
{
var ironVitaminDCodes = new List<string> { "374424002", "175041000140104" };
@@ -784,8 +958,12 @@ public class CalculatedObservations : ICalculatedObservations
return Task.FromResult(1);
}
//TODO revisar con los valores reales cuando se sepan
/// <summary>
/// Maps a <c>Resp_Mode</c> observation's value to a discrete <c>Resp_Type</c> (HighFrequencyVentilation,
/// Invasive, NonInvasive, or None) by matching it against the configured catalogues for each mode.
/// </summary>
/// <param name="obs">The respiratory-mode observation expected to be a <see cref="PatientObservation"/>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
public async Task CalculateVentilationMode(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -811,6 +989,15 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
}
/// <summary>
/// Calculates the patient's respiratory assistance score from the supplied observation's
/// <c>Value</c> (matched against the configured respiratory-type catalogue) or from the latest
/// stored <c>Resp_Mode</c> when the supplied observation is <c>ONi</c>. Active ONi treatment
/// (or a non-expired last ONi observation) forces the score to 10. Persists the resulting
/// <c>Respiratory</c> observation and returns the original input unchanged.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (expected to be a <see cref="PatientObservation"/>).</param>
/// <returns>A task that resolves to the same base patient observation that was passed in.</returns>
public async Task<BasePatientObservation> CalculateAsistResp(BasePatientObservation obs)
{
//Comprobar si hay un resp_type más nuevo y si no lo hay ignorar
@@ -862,7 +1049,14 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Updates the synthetic <c>ECMO</c> observation according to the treatment's <c>OrderControl</c>:
/// <c>NW</c> sets it to <c>NW</c>, <c>DC</c> sets it to <c>XO</c> if any other ECMO treatment
/// remains active or to <c>DC</c> otherwise, and any other <c>OrderControl</c> is ignored.
/// In every case the change is followed by a complexity recalculation via <c>CalculateComplexity</c>.
/// </summary>
/// <param name="treatment">The treatment whose ECMO state is being applied.</param>
/// <returns>A task that represents the asynchronous recalculation operation.</returns>
private async Task CalculateComplexityOnEcmo(PatientTreatment treatment)
{
var ecmoObs = new PatientObservation
@@ -896,12 +1090,18 @@ public class CalculatedObservations : ICalculatedObservations
await CalculateComplexity(ecmoObs);
}
/**
* Calculamos la complejidad basándonos en el valor de PESO/VÍAS/ASSIST.RESP/MONIT./CIRUGÍA/MEDICACIÓN
* cuando entra una nueva observación que afecte a complejidad, se recalcula de 0 para no tener que estar pendiente de qué puntos corresponden a qué observación
* La complejidad va de 0 a 5. Guardamos la observación que nos llega y recalculamos complejidad.
*/
/// <summary>
/// Recalculates the patient's overall <c>Complexity</c> observation from scratch using the latest
/// values for the configured complexity-contributing observations (weight, medication, surgery,
/// intravenous lines, monitor, and respiratory), the active ECMO treatment state, and the
/// optionally supplied <paramref name="obs"/> which is preferred over the database value when
/// it is newer and not expired. The result is persisted only when its integer value differs
/// from the last stored complexity observation, with time inconsistencies resolved via
/// <c>FixTimeInconsistencyWithLast</c>. Returns the original observation unchanged.
/// </summary>
/// <param name="obs">The base patient observation that triggered the recalculation.</param>
/// <param name="ignoreObs">When <see langword="true"/>, the supplied <paramref name="obs"/> is not added to the calculation inputs (useful for synthetic observations like ECMO).</param>
/// <returns>A task that resolves to the same base patient observation that was passed in.</returns>
public async Task<BasePatientObservation> CalculateComplexity(BasePatientObservation obs, bool ignoreObs = false)
{
try
@@ -909,7 +1109,11 @@ public class CalculatedObservations : ICalculatedObservations
var logUid = Guid.NewGuid();
var complexity = new PatientObservation
{
PatientId = obs.PatientId, Time = DateTime.UtcNow, CodingSystem = "ADAS", Name = "Complexity", Value = 0
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = "ADAS",
Name = "Complexity",
Value = 0
};
var complexityValue = 0;
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(obs.PatientId);
@@ -1059,15 +1263,21 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Calculate intravenous line when new intravenous observation enters. Get all intravenous obs and filtering actives.
/// Si existe ya un catéter del mismo tipo, apuntando a la misma localización con una fecha que ya existe y no la ha
/// sido retirada y vuelve a venir
/// una inserción, no hacemos caso porque es una actualización.
/// Recomputes the <c>IntravenousLines</c> complexity score by reconciling the incoming
/// <c>IntravenousLinesObs</c> against the patient's active intravenous lines. Detects and
/// corrects common ICCA mis-clicks (inserts incorrectly carrying a remove time), updates an
/// existing line in place when only the duration changed, and otherwise adds the new line to
/// the active set. The aggregated score is capped at 10 and persisted with bounds [0, 10].
/// <c>InvalidCastException</c>s are logged and swallowed.
/// </summary>
/// <param name="obs"></param>
/// <returns></returns>
/// <param name="obs">The base patient observation that triggered the calculation (expected to be a <see cref="PatientObservation"/> whose <c>Value</c> is a <see cref="PatientIntravenousLinesValue"/>).</param>
/// <returns>
/// A task that resolves to the original base patient observation when processing succeeds or
/// fails gracefully with logging; resolves to <see langword="null"/> when the function returns
/// early because the incoming observation is a duplicate insert (in which case the caller should
/// not overwrite the stored observation).
/// </returns>
public async Task<BasePatientObservation?> CalculateIntravenousLineObservation(BasePatientObservation obs)
{
try
@@ -1194,7 +1404,15 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Derives the weight-related complexity contribution from a weight observation: values expressed
/// in kilograms are first converted to grams via <c>ParseWeight</c>, and the final integer complexity
/// contribution is mapped from the gram value using the standard UCIN brackets
/// (&lt; 750 g → 7, 750999 → 5, 10001249 → 2, 12501999 → 1, ≥ 2000 → 0). Returns 0 when the
/// observation value cannot be parsed as a number.
/// </summary>
/// <param name="obs">The base patient observation whose value represents the patient's weight.</param>
/// <returns>A task that resolves to the integer complexity contribution (07).</returns>
private async Task<int> CalculateWeight(BasePatientObservation obs)
{
var complexityValueOfWeight = 0;
@@ -1219,7 +1437,14 @@ public class CalculatedObservations : ICalculatedObservations
return complexityValueOfWeight;
}
/// <summary>
/// Converts a weight observation expressed in kilograms to grams in-place (updates <c>Units</c> to
/// <c>"gr"</c> and multiplies <c>Value</c> by 1000). Returns the observation unchanged when it is
/// not a <see cref="PatientObservation"/> or when its value cannot be parsed; the error is also
/// logged.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with a numeric value.</param>
/// <returns>A task that resolves to the (possibly converted) base patient observation.</returns>
public Task<BasePatientObservation> ParseWeight(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs || !double.TryParse(pobs.Value.ToString(), out var dValue))
@@ -1233,9 +1458,19 @@ public class CalculatedObservations : ICalculatedObservations
return Task.FromResult(obs);
}
//Monitor observation can be a observation or treatment
//TODO como saber si tiene fecha de fin o frecuencia
/// <summary>
/// Recalculates the <c>Monitor</c> observation by adding 1 point for each active
/// transcutaneous / regional-brain-saturation / EEG monitoring signal (codes from the configured
/// catalogues) found in the most recent observations and active treatments. Persists the resulting
/// observation with bounds [0, 3]. Returns early with a warning when no recent monitor observations
/// are found.
/// </summary>
/// <param name="monitorObservation">
/// The trigger for the calculation. Accepts either a <see cref="PatientObservation"/>
/// (preferred when available) or a <see cref="PatientTreatment"/>. The patient identifier is
/// derived from whichever type is supplied.
/// </param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateMonitor(object monitorObservation)
{
var monitorValue = 0;
@@ -1302,7 +1537,14 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(monitorObs, mapObs: false);
}
/// <summary>
/// Recalculates the <c>Surgery</c> complexity score by reconciling the supplied treatment's
/// <c>OrderControl</c> (<c>NW</c> adds, <c>DC</c> removes by placer-order identifier) against the
/// patient's other active surgery treatments, then persisting a <c>Surgery</c> observation
/// with a score of 5 when any active surgery treatment remains, or 0 otherwise.
/// </summary>
/// <param name="treatment">The treatment that triggered the recalculation.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
private async Task CalculateSurgery(PatientTreatment treatment)
{
var surgeryScore = 0;
@@ -1315,7 +1557,7 @@ public class CalculatedObservations : ICalculatedObservations
switch (treatment.OrderControl)
{
//Refactor solo con los tratamientos activos de cirugía, un DC cancela a su entityIdentifier correspondiente.
case OrderControlType.Nw:
activeSurgeryTreatments.Add(treatment);
break;
@@ -1342,7 +1584,18 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(surgeryObs, mapObs: false);
}
/// <summary>
/// Computes the patient's active bolus treatments by:
/// (1) optionally including the newly arrived treatment, (2) grouping all bolus-eligible treatments
/// by placer-order <c>NamespaceId</c>, (3) discarding treatments whose administration time was
/// later overridden by an end-time, (4) keeping only the latest message-time entry per distinct
/// administration time, and (5) filtering to the configured medication-bolus catalogue, an
/// administration time within the last 12 hours, and a non-empty RXA status contained in
/// <c>_rxaStatus</c>.
/// </summary>
/// <param name="patientId">The unique identifier of the patient.</param>
/// <param name="newTreatment">The optional new treatment to be included in the active-bolus set.</param>
/// <returns>A task that resolves to the list of treatments currently counted as active boluses.</returns>
private async Task<List<PatientTreatment>> GetActiveBolus(ObjectId patientId, PatientTreatment? newTreatment)
{
var treatmentsProcessed = new List<PatientTreatment>();
@@ -1401,7 +1654,12 @@ public class CalculatedObservations : ICalculatedObservations
p.RequestedGiveCodesStatus.Any(rxa => rxa.Status != null && _rxaStatus.Contains(rxa.Status)));
}
/// <summary>
/// Persists the <c>OpiateBoluses</c> observation whose value is the count of currently active bolus
/// treatments for the patient (as computed by <c>GetActiveBolus</c> including the new treatment).
/// </summary>
/// <param name="treatment">The treatment that triggered the recalculation and should be included in the active-bolus set.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateBolus(PatientTreatment treatment)
{
var activeBolus = await GetActiveBolus(treatment.PatientId, treatment);
@@ -1417,9 +1675,17 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(bolusObs);
}
/*
* En rojo si TAm < Edad Gestacional en semanas en la primera semana y luego edad corregida
*/
/// <summary>
/// Applies the red-alert rule for mean arterial pressure (TAm) in neonates: when TAm is below
/// the patient's gestational age (in weeks) for the first week, or below the fixed gestational
/// age thereafter, the <c>TAm</c> observation's <c>Status</c> is set to <c>Alert</c>; otherwise
/// it is set to <c>Ok</c>. If the supplied observation is a gestational-age value (not TAm),
/// a fresh TAm observation is re-inserted with the recalculated status. The threshold is read
/// from the latest <c>Age_Gestational</c> observation when its value is below 2 weeks,
/// otherwise from the latest <c>Age_Gestational_Fixed</c>.
/// </summary>
/// <param name="obs">The base patient observation that triggered the alert evaluation (<c>TAm</c>, <c>Age_Gestational</c>, or <c>Age_Gestational_Fixed</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateTAmAlert(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -1473,7 +1739,16 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/*Entra temp axilar o incubadora. Restamos la axilar a la incubadora. Solo se calcula si están los dos valores. */
/// <summary>
/// Computes the temperature gradient between the incubator and the patient (incubator patient)
/// when both temperatures have been observed within 10 minutes of each other. Persists the
/// <c>Temp_Gradient</c> observation with the resulting value, after shifting its timestamp via
/// <c>CheckObsWithSameTimeExistsAndIncrementTime</c> to avoid colliding with a previous
/// gradient observation. Returns early when the companion temperature is missing or the two
/// readings fall outside the 10-minute window.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (<c>Temp_Patient</c> or <c>Temp_Incubator</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateTempGradient(BasePatientObservation obs)
{
List<PatientObservation> tempRetrievedesFromBd;
@@ -1525,6 +1800,10 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Enumeration of the intravenous-line types recognised by the UCIN complexity model,
/// ordered from most invasive (Artery) to least invasive (Peripheral).
/// </summary>
private enum IntraVenousLineTypes
{
Artery,
@@ -1534,6 +1813,10 @@ public class CalculatedObservations : ICalculatedObservations
Peripheral
}
/// <summary>
/// Enumeration of the respiratory-assistance types recognised by the UCIN complexity model,
/// ordered from highest score (Ino) to lowest (None).
/// </summary>
// ReSharper disable once UnusedMember.Local
private enum RespiratoryTypes
{
@@ -11,6 +11,9 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.HGM;
//Custom for Hospital Gregorio Marañón
/// <summary>
/// Represents a collection of calculated observations, providing a concrete implementation of the <see cref="ICalculatedObservations"/> contract.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private readonly List<string> _highFrequencyVentilation = [];
@@ -35,6 +38,13 @@ public class CalculatedObservations : ICalculatedObservations
nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim()));
}
/// <summary>
/// Maps a patient observation by applying domain-specific transformations based on the observation name.
/// Handles the "Resp_Mode" case by calculating the ventilation mode, while observations with a null or empty name are returned unchanged.
/// </summary>
/// <param name="obs">The patient observation to be mapped.</param>
/// <param name="onlyByName">Flag indicating whether the mapping should be restricted to name-based criteria.</param>
/// <returns>The mapped patient observation, or the original observation if no applicable mapping is found.</returns>
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
if (string.IsNullOrEmpty(obs.Name)) return obs;
@@ -50,57 +60,120 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Asynchronously maps the specified <paramref name="treatment"/> to a <see cref="PatientTreatment"/> result.
/// </summary>
/// <param name="treatment">The <see cref="PatientTreatment"/> instance to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the resulting <see cref="PatientTreatment"/>.</returns>
/// <exception cref="System.NotImplementedException">Thrown to indicate that the method has not yet been implemented.</exception>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps the provided patient diagnosis to the target representation asynchronously.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the mapping logic has not been implemented yet.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps the provided <see cref="PumpObservation"/> by returning it unchanged, wrapped in a completed task.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A completed task containing the provided <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient for whom the active bolus is calculated.</param>
/// <exception cref="NotImplementedException">The method is not yet implemented.</exception>
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Calculates the medicine observation for a patient based on the provided active medicines.
/// </summary>
/// <param name="activeMedicines">The list of active medicines currently associated with the patient.</param>
/// <param name="patientId">The unique identifier of the patient for whom the observation is being calculated.</param>
/// <exception cref="NotImplementedException">The method is not yet implemented.</exception>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Resolves time inconsistencies between the specified <paramref name="newObservation"/> and the last recorded patient observation, returning a corrected observation when applicable.
/// </summary>
/// <param name="newObservation">The new patient observation to reconcile against the last recorded observation.</param>
/// <returns>A task that returns the corrected <see cref="PatientObservation"/>, or <c>null</c> when no last observation is available to compare against.</returns>
/// <exception cref="System.NotImplementedException">The method is not yet implemented.</exception>
public Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
throw new NotImplementedException();
}
/// <summary>
/// Retrieves the active treatments currently associated with the specified patient.
/// </summary>
/// <param name="id">The identifier of the patient whose active treatments are being requested.</param>
/// <returns>A task that yields a collection of active <see cref="PatientTreatment"/> entries for the patient.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
throw new NotImplementedException();
}
/// <summary>
/// Pre-maps the provided list of patient observations before further processing, returning the list as-is in a completed task.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped.</param>
/// <returns>A completed task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm to a patient observation, returning the provided observation as the result.
/// </summary>
/// <param name="obs">The patient observation to return as the mapped result.</param>
/// <param name="alarmToInsert">The patient observation alarm to be considered during mapping.</param>
/// <returns>A <see cref="Task{PatientObservation}"/> containing the provided <paramref name="obs"/>.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm for the specified patient observation.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code identifying the alarm type.</param>
/// <returns>A task that represents the asynchronous alarm sending operation.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as it is not yet implemented.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Calculates and persists the ventilation mode (<c>Resp_Type</c>) for a patient observation by mapping the observed value against known high-frequency, invasive, and non-invasive ventilation vocabularies, defaulting to <c>None</c> when the value is empty or unrecognized.
/// </summary>
/// <param name="obs">The base patient observation whose value is used to derive the respiration type; it is cast to <see cref="PatientObservation"/> to access the value.</param>
private async Task CalculateVentilationMode(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -11,6 +11,12 @@ using Serilog;
namespace adas_core.Application.Customizations.HPAZ;
/// <summary>
/// Represents a service that provides calculated observations, receiving its dependencies through the supplied <see cref="IServiceProvider"/>.
/// </summary>
/// <remarks>
/// This class implements the <see cref="ICalculatedObservations"/> contract, exposing the behavior defined by that interface while relying on constructor-injected services for its operations.
/// </remarks>
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly Lazy<IAlarmService> _alarmService = serviceProvider.GetRequiredService<Lazy<IAlarmService>>();
@@ -32,16 +38,31 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
private readonly List<string> _pressBloodArteryMean = ["TAm"];
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active bolus is to be calculated.</param>
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Calculates medicine observations for the specified patient based on their active medicines.
/// </summary>
/// <param name="activeMedicines">The list of medicines currently active for the patient.</param>
/// <param name="patientId">The unique identifier of the patient whose medicine observations are being calculated.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Retrieves the active treatments associated with the specified patient identifier.
/// Returns an empty collection when no active treatments are found.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of active <see cref="PatientTreatment"/> records for the patient, or an empty collection if none exist.</returns>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
return Task.FromResult<IEnumerable<PatientTreatment?>>(new List<PatientTreatment?>());
@@ -87,7 +108,11 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return obs as T;
}
/// <summary>
/// Maps the specified pump observation to a new <see cref="PumpObservation"/> instance asynchronously.
/// </summary>
/// <param name="pumpObservation">The source <see cref="PumpObservation"/> to map.</param>
/// <returns>A task that represents the asynchronous mapping operation. The task result contains the mapped <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
try
@@ -181,17 +206,34 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> instance, performing the required transformation logic.
/// </summary>
/// <param name="treatment">The <see cref="PatientTreatment"/> to be mapped.</param>
/// <returns>A task representing the asynchronous operation, containing the mapped <see cref="PatientTreatment"/>.</returns>
/// <exception cref="NotImplementedException">Thrown in all cases because the method has not yet been implemented.</exception>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a <see cref="PatientDiagnosis"/> instance. This method is a placeholder and has not been implemented yet.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the mapping logic has not been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Ensures chronological order of patient observations by adjusting a new observation's time to one second after the most recent observation with the same name, when its time is earlier than or equal to the previous one (compared at second precision). Returns the observation unchanged and logs an error if the observation name is null or empty.
/// </summary>
/// <param name="newObservation">The new patient observation to validate and potentially adjust for time consistency.</param>
/// <returns>The patient observation with its time corrected if a time inconsistency was detected, otherwise the original observation.</returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -219,6 +261,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return newObservation;
}
/// <summary>
/// Pre-maps a list of patient observations by creating deep copies, applying configuration-based mapping,
/// and triggering blue code calculation. Returns the original list of patient observations unchanged.
/// </summary>
/// <param name="listToMap">The list of patient observations to process for mapping and blue code calculation.</param>
/// <returns>The original list of patient observations passed to the method.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToMap)
{
var mappedObsList = listToMap
@@ -236,6 +284,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return Task.FromResult(listToMap);
}
/// <summary>
/// Maps fields from a <see cref="PatientObservationAlarm"/> onto an existing <see cref="PatientObservation"/>, copying the alarm's EventId to Code and Event to Name when those values are provided, and always copying the alarm's Value.
/// </summary>
/// <param name="obs">The target <see cref="PatientObservation"/> instance that will be updated with values from the alarm.</param>
/// <param name="alarmToInsert">The source <see cref="PatientObservationAlarm"/> whose values are applied to <paramref name="obs"/>.</param>
/// <returns>A completed <see cref="Task{PatientObservation}"/> containing the updated <paramref name="obs"/>.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
if (!string.IsNullOrEmpty(alarmToInsert.EventId))
@@ -249,12 +303,26 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm associated with the specified patient observation, using the provided alarm name and optional code.
/// </summary>
/// <param name="obs">The patient observation that triggers or relates to the alarm.</param>
/// <param name="name">The name of the alarm to be sent.</param>
/// <param name="code">The optional alarm code identifying the type of alarm.</param>
/// <exception cref="System.NotImplementedException">Thrown because the method is not yet implemented.</exception>
Task ICalculatedObservations.SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Asynchronously sends an alarm for the specified patient observation through the alarm service, always using a None severity.
/// </summary>
/// <param name="obs">The patient observation associated with the alarm.</param>
/// <param name="name">The name of the alarm.</param>
/// <param name="code">The optional alarm code identifier.</param>
/// <param name="type">The type of alarm to send.</param>
private async Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code, AlarmEnum.Type type)
{
await _alarmService.Value.SendAlarm(obs, name, code, AlarmEnum.Severity.None, type);
@@ -281,6 +349,11 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Retrieves the alarm configuration for the given patient observation by looking up a matching configuration entry based on the observation name and patient ID, and applies it to the observation. If no matching configuration is found, the alarm is set to <c>null</c>.
/// </summary>
/// <param name="pobs">The patient observation whose alarm configuration should be resolved; its <c>Name</c> and <c>PatientId</c> are used to locate the configuration.</param>
/// <returns>The same <see cref="PatientObservation"/> instance with its <c>Alarm</c> property populated from the matching configuration, or <c>null</c> when no configuration is found.</returns>
private async Task<PatientObservation> CheckAlarmConfig(PatientObservation pobs)
{
var configObs = await _configObservationService.Get(new PatientObservation
@@ -294,6 +367,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return pobs;
}
/// <summary>
/// Creates a new <see cref="PatientObservation"/> representing an alarm, using the ADAS_ALARM coding system and prefixing the name with "Alarm_". The resulting observation inherits the value, patient identifier, timestamp, and alarm status from the provided source observation.
/// </summary>
/// <param name="name">The alarm code used to identify the type of alarm; it is set as the <c>Code</c> and used to compose the <c>Name</c>.</param>
/// <param name="pobs">The source patient observation whose value, patient identifier, time, and alarm flag are copied into the new alarm observation.</param>
/// <returns>A new <see cref="PatientObservation"/> configured as an ADAS alarm observation based on the provided source.</returns>
private static PatientObservation CreateAlarmObservation(string name, PatientObservation pobs)
{
return new PatientObservation
@@ -378,6 +457,10 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Sends a Blue Code alarm for the given patient observation. If the observation is null, the method returns without taking any action; otherwise it creates the corresponding alarm observation, validates it against the alarm configuration, persists it, and dispatches the alarm as an automatically triggered Blue alarm.
/// </summary>
/// <param name="pobs">The patient observation that triggers the Blue Code alarm; when null, the method short-circuits without processing.</param>
private async Task SendBlueCode(PatientObservation? pobs)
{
if (pobs == null)
@@ -397,6 +480,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
//PRVC : donde aparezca cambiar y mostrar en su lugar: VCRP
//FLUJO ALTO: poner OAF en su lugar
/// <summary>
/// Calculates and normalizes the ventilation mode value for a patient observation.
/// Replaces occurrences of "PRVC" with "VCRP" and maps "FLUJ.ALTO" to "OAF" in the observation's value.
/// </summary>
/// <param name="obs">The base patient observation value whose ventilation mode will be calculated and normalized.</param>
/// <returns>The patient observation with the normalized ventilation mode value.</returns>
private BasePatientObservationValue CalculateVentilationMode(BasePatientObservationValue obs)
{
var value = obs is PatientObservationAlarm oAlarm ? oAlarm.Value
@@ -414,8 +503,13 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
//TODO duplicado con el calculated del 12o, sacar a un utils
/// <summary>
/// Checks whether a patient observation with the same time already exists and, if so, increments the observation time by one second to avoid a duplicate timestamp.
/// </summary>
/// <param name="obs">The patient observation to evaluate; non-<see cref="PatientObservation"/> instances are returned unchanged.</param>
/// <returns>The original observation, with its <c>Time</c> adjusted by one second when a matching observation is found.</returns>
private async Task<BasePatientObservationValue> CheckObsWithSameTimeExistsAndIncrementTime(
BasePatientObservationValue obs)
BasePatientObservationValue obs)
{
if (obs is not PatientObservation pobs) return obs;
@@ -428,6 +522,13 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
//S/F Calc ( craneal saturation / FiO2
/// <summary>
/// Calculates the SpO2/FiO2 (S/F) ratio for a patient by pairing a newly received oxygenation observation
/// (FiO2 or Sattc) with the most recent complementary value and inserting a derived "Sattc_FiO2" observation.
/// The derived value is only inserted when both observations exist, are not expired, parse as numbers, the saturation is at most 97, and FiO2 is non-zero; otherwise the calculation is logged and skipped, and any exception is caught and logged.
/// </summary>
/// <param name="obs">The newly received patient observation that triggered the calculation; its Name must be either "FiO2" or "Sattc".</param>
/// <param name="name">The name of the observation, used to determine whether <paramref name="obs"/> is the FiO2 or the Sattc value.</param>
private async Task CalculateSf(BasePatientObservationValue obs, string name)
{
try
@@ -558,6 +659,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
//P/F Calc ( PaO2_Tidal / FiO2
/// <summary>
/// Asynchronously calculates the PaO2/FiO2 (P/F) ratio for a patient and persists the result as a new <c>PaO2_FiO2</c> observation.
/// Supports being triggered by either an FiO2 or a PaO2_Tidal observation by pairing the provided observation with its counterpart retrieved from the most recent observations, and only persists the result when both values are present, non-expired, parseable as numeric values, and the FiO2 value is non-zero.
/// </summary>
/// <param name="obs">The current patient observation that initiated the calculation; provides the patient identifier and timestamp used for the resulting observation.</param>
/// <param name="name">The name of the observation in <paramref name="obs"/>, expected to be either <c>FiO2</c> or <c>PaO2_Tidal</c>, which determines how the counterpart value is resolved.</param>
private async Task CalculatePf(BasePatientObservationValue obs, string name)
{
try
@@ -614,15 +721,23 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Creates a <see cref="PatientObservation"/> representing a pump alarm, tagging it with the
/// ADAS_ALARM coding system and a code/name derived from the alarm <paramref name="name"/>.
/// The position label used as the observation value distinguishes main rack pumps from auxiliary rack pumps based on <see cref="PumpObservation.IsAux"/>, and falls back to the position when no drug name is available. The patient identifier is assigned when present, and the observation is then validated against the alarm configuration.
/// </summary>
/// <param name="name">The alarm identifier used to build the observation <c>Code</c> and <c>Name</c>.</param>
/// <param name="pumpObservation">The pump data source providing rack/auxiliary info, drug name, time, and optional patient id.</param>
/// <returns>A <see cref="Task{PatientObservation}"/> that yields the resulting observation, or <c>null</c> when the alarm configuration check rejects it.</returns>
private async Task<PatientObservation?> CreatePumpAlarmObservation(string name,
PumpObservation pumpObservation)
PumpObservation pumpObservation)
{
var positionPump = pumpObservation.IsAux != null && !pumpObservation.IsAux.Value
? $"Rack {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}"
: $"Rack Aux {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}";
PatientObservation patientObservation = new()
{
CodingSystem = "ADAS_ALARM",
@@ -634,7 +749,7 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
if (pumpObservation.PatientId != null)
patientObservation.PatientId = pumpObservation.PatientId.Value;
return await CheckAlarmConfig(patientObservation);
}
@@ -11,6 +11,13 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.HRYC;
/// <summary>
/// Implements the HRYC-specific clinical calculations that derive secondary
/// <see cref="PatientObservation"/> values (NEWS alarms, IROX, Resp_Rate_Calculated, Hydric_Balance_Calculated,
/// Weight_Diff, Diuresis_Weight, Delta_Pressure, Daily_Balance_Calculated, Allergies, DVE, Drainage_Height,
/// Resp_Type, and Hour_Balance) from incoming raw observations and active configurations loaded from
/// <see cref="ApiSettings"/>.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private readonly IOptions<ApiSettings> _apiSettings;
@@ -27,6 +34,14 @@ public class CalculatedObservations : ICalculatedObservations
private readonly Lazy<IPatientService> _patientService;
/// <summary>
/// Initializes a new instance of the <see cref="CalculatedObservations"/> class,
/// resolving its dependencies (observation, patient, light-beacon, and config-observation services,
/// plus the logger) from the supplied <see cref="IServiceProvider"/> and loading the configured
/// code catalogues (high-frequency ventilation, non-invasive ventilation, and invasive ventilation)
/// from <see cref="ApiSettings"/>.
/// </summary>
/// <param name="serviceProvider">The application's service provider used to resolve the required dependencies and configuration.</param>
public CalculatedObservations(IServiceProvider serviceProvider)
{
_observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>(); //observationService;
@@ -52,6 +67,25 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Dispatches the supplied raw observation to the appropriate HRYC calculation based on its
/// <see cref="BasePatientObservation.Name"/>. The set of supported calculations includes
/// <c>Resp_Mode</c> (ventilation type), <c>Diuresis</c> / <c>Weight_Current</c>
/// (weight difference and diuresis-per-kilogram), <c>AllergiesObs</c>, <c>DrainagesObs</c>,
/// <c>PEEP</c> / <c>Pleateu_Pressure</c> (driving pressure), <c>Daily_Balance</c>,
/// <c>Hydric_Balance</c> / <c>Hour_Balance</c> (time shifting), <c>FR</c> / <c>Vent_Rate</c>
/// (respiratory rate), <c>SpO2</c> / <c>FiO2</c> (IROX), and <c>NEWS</c> (alarms).
/// </summary>
/// <typeparam name="T">The concrete observation type, deriving from <see cref="BasePatientObservation"/>.</typeparam>
/// <param name="obs">The observation to map or transform in-place.</param>
/// <param name="onlyByName">
/// Reserved for future use. When <see langword="true"/>, restricts the mapping strategy to
/// name-based lookups only.
/// </param>
/// <returns>
/// A <see cref="Task{T}"/> that resolves to the (possibly transformed) observation,
/// or <see langword="null"/> when the input observation has no name.
/// </returns>
public async Task<T?> Map<T>(T obs, bool onlyByName) where T : BasePatientObservation
{
_logger.LogTrace("Mapping {name} observation {obs}", obs.Name, obs);
@@ -107,40 +141,89 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Placeholder mapping for a <see cref="PatientTreatment"/>. The HRYC customization does not
/// currently derive observations from treatments.
/// </summary>
/// <param name="treatment">The treatment to map.</param>
/// <returns>Never returns a result.</returns>
/// <exception cref="NotImplementedException">Always thrown because treatment mapping is not implemented in the HRYC customization.</exception>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
/// <summary>
/// Identity mapping for a <see cref="PumpObservation"/>: returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract; pump observations
/// do not require HRYC-specific calculation.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A task containing the same <see cref="PumpObservation"/> instance that was passed in.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// No-op implementation of the medicine-observation calculation hook. The HRYC customization does
/// not derive Medication or ERMedication observations from the active medicine list.
/// </summary>
/// <param name="activeMedicines">The list of active medicines (ignored).</param>
/// <param name="patientId">The unique identifier of the patient (ignored).</param>
/// <returns>A completed task.</returns>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// No-op implementation of the active-bolus calculation hook. The HRYC customization does not
/// derive an OpiateBoluses observation.
/// </summary>
/// <param name="patientId">The unique identifier of the patient (ignored).</param>
/// <returns>A completed task.</returns>
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Returns an empty enumerable of active treatments. The HRYC customization does not currently
/// maintain a per-patient active-treatment cache.
/// </summary>
/// <param name="id">The unique identifier of the patient (ignored).</param>
/// <returns>A completed task containing an empty <see cref="IEnumerable{PatientTreatment}"/>.</returns>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
return Task.FromResult(new List<PatientTreatment?>().AsEnumerable());
}
/// <summary>
/// Identity mapping for a <see cref="PatientDiagnosis"/>: returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to map.</param>
/// <returns>A task containing the same <see cref="PatientDiagnosis"/> instance that was passed in.</returns>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
/// <summary>
/// Resolves time-inconsistencies for a new patient observation when it arrives with the same
/// (down-to-the-second) timestamp as the latest stored observation of the same name. The new
/// observation's <c>Time</c> is shifted forward by one second to avoid duplicate-key collisions.
/// </summary>
/// <param name="newObservation">The new patient observation to evaluate for time inconsistencies.</param>
/// <returns>
/// A task that resolves to the (possibly time-shifted) <see cref="PatientObservation"/>.
/// Returns the input unchanged when its <c>Name</c> is null or empty, logging the error.
/// </returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -168,6 +251,17 @@ public class CalculatedObservations : ICalculatedObservations
return newObservation;
}
/// <summary>
/// Pre-maps a batch of patient observations, optimising the order in which they are persisted
/// when both <c>Vent_Rate</c> and <c>FR</c> are present. If both observations are present with
/// non-zero values, <c>Vent_Rate</c> is inserted first (and removed from the returned list) so
/// that <see cref="CalculateRespRate"/> can derive <c>Resp_Rate_Calculated</c> correctly.
/// </summary>
/// <param name="listToInsert">The list of patient observations to pre-map. Modified in-place.</param>
/// <returns>
/// A task that resolves to the resulting list of observations (with <c>Vent_Rate</c> removed
/// when it was inserted synchronously, or the original list otherwise).
/// </returns>
public async Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
var ventRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_VENT_RESP_RATE");
@@ -210,20 +304,41 @@ public class CalculatedObservations : ICalculatedObservations
return listToInsert;
}
/// <summary>
/// Identity mapping for a source alarm observation paired with a <see cref="PatientObservationAlarm"/>:
/// returns the supplied observation unchanged, wrapped in a completed task. Provided to satisfy the
/// customization contract.
/// </summary>
/// <param name="obs">The patient observation that triggered the alarm.</param>
/// <param name="alarmToInsert">The alarm metadata to be inserted alongside the observation.</param>
/// <returns>A task containing the same <see cref="PatientObservation"/> instance that was passed in.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Placeholder alarm dispatch hook used by the customization contract. The HRYC customization
/// dispatches NEWS alarms through its private <c>SendAlarm(BasePatientObservation, AlarmEnum.Name)</c>
/// overload; calling this public overload always throws.
/// </summary>
/// <param name="obs">The patient observation that triggered the alarm.</param>
/// <param name="name">The alarm display name.</param>
/// <param name="code">The optional alarm code from <see cref="AlarmEnum.Name"/>.</param>
/// <returns>Never returns a result.</returns>
/// <exception cref="NotImplementedException">Always thrown because this overload is not used in the HRYC customization.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// IF news greater or equal than 5 and less than 7 is warning beacon and if greater or equal than 7 alert
/// Translates a NEWS (National Early Warning Score) value into an alarm level: a value below 5
/// (or an unparsable value) is reported as <c>NewsOff</c>, between 5 and 7 as <c>NewsWarning</c>,
/// and 7 or above as <c>NewsAlert</c>. Returns early when the patient cannot be located.
/// </summary>
/// <param name="obs"></param>
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> whose <c>Name</c> is <c>NEWS</c>.</param>
/// <returns>A task that represents the asynchronous alarm evaluation.</returns>
private async Task CalculateNews(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -258,6 +373,15 @@ public class CalculatedObservations : ICalculatedObservations
//if (obsAlarm == null) return;
}
/// <summary>
/// Builds a synthetic <c>Alarm_&lt;Name&gt;</c> observation, looks up its configuration, and when
/// the configured alarm and its beacon are enabled, dispatches a light-beacon colour change for
/// the patient's point-of-care. The alarm observation is always persisted. Errors are logged and
/// swallowed.
/// </summary>
/// <param name="obs">The base patient observation that triggered the alarm (used to derive <c>PatientId</c> and <c>Time</c>).</param>
/// <param name="name">The <see cref="AlarmEnum.Name"/> describing the alarm kind (for example, <c>NewsOff</c>, <c>NewsWarning</c>, <c>NewsAlert</c>).</param>
/// <returns>A task that represents the asynchronous alarm dispatch.</returns>
private async Task SendAlarm(BasePatientObservation obs, AlarmEnum.Name name)
{
var pobs = (PatientObservation)obs;
@@ -273,10 +397,10 @@ public class CalculatedObservations : ICalculatedObservations
{
//ConfigObservations
var configObs = await _configObservationService.Get(new PatientObservation
{
Name = nObs.Name,
PatientId = obs.PatientId
}
{
Name = nObs.Name,
PatientId = obs.PatientId
}
);
if (configObs == null)
{
@@ -313,6 +437,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Sends a colour command to the light beacon associated with the patient's point-of-care.
/// Maps <see cref="AlarmEnum.BeaconColor"/> values to <see cref="LightBeaconColor"/> commands
/// (blue, yellow, red, or off) and logs an error when the patient has no point-of-care identifier.
/// </summary>
/// <param name="color">The beacon colour to apply.</param>
/// <param name="patient">The patient whose associated beacon should be updated.</param>
private void SendBeaconCode(AlarmEnum.BeaconColor color, Patient patient)
{
if (!patient.PointOfCareId.HasValue)
@@ -339,7 +470,14 @@ public class CalculatedObservations : ICalculatedObservations
}
}
//(SpO2/FiO2)/FR IROX formula. Only calculate when all observations are in last 10 minutes.
/// <summary>
/// Computes the IROX index (<c>SpO2 / FiO2 / FR</c>) when <c>SpO2</c>, <c>FiO2</c>, and <c>FR</c>
/// are all present in the latest observations and were recorded within the last 10 minutes.
/// Persists the resulting <c>IROX</c> observation. Skips silently when any input is missing,
/// zero, or unparsable. Errors are logged and swallowed.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (<c>SpO2</c>, <c>FiO2</c>, or <c>FR</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
private async Task CalculateIrox(BasePatientObservation obs)
{
try
@@ -412,13 +550,16 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/*
*
RespRate solo se tiene que poner cuando no hay ningún ventRate en los últimos min y veinte segundos y el último no es un 0.
*/
//static readonly SemaphoreSlim semaphoreCalculateRespRate = new(1, 1);
/// <summary>
/// Persists a <c>Resp_Rate_Calculated</c> observation when the supplied observation's value is
/// non-zero. For <c>FR</c>, the insert is skipped if a recent (within
/// <c>CalculateRespRateVentExpires</c> minutes) non-zero <c>Vent_Rate</c> observation exists;
/// the inserted observation's <c>Time</c> is set to the original <c>Time</c> for <c>FR</c>, or
/// shifted one second forward for any other name.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (<c>FR</c> or <c>Vent_Rate</c>).</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateRespRate(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -479,6 +620,13 @@ public class CalculatedObservations : ICalculatedObservations
obs.Name, obs.Time, pobs.Value);
}
/// <summary>
/// Shifts the timestamp of a <c>Hydric_Balance</c> observation forward by one second when another
/// <c>Hydric_Balance</c> observation was already recorded within the same hour, so the most
/// recent entry can be identified unambiguously.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Hydric_Balance</c>.</param>
/// <returns>A task that resolves to the (possibly time-shifted) base patient observation.</returns>
private async Task<BasePatientObservation> CalculateHydricBalance(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -496,6 +644,12 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Shifts the timestamp of a <c>Hour_Balance</c> observation forward by one second when another
/// <c>Hour_Balance</c> observation was already recorded within the same hour.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Hour_Balance</c>.</param>
/// <returns>A task that resolves to the (possibly time-shifted) base patient observation.</returns>
private async Task<BasePatientObservation> CalculateHourBalance(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -513,6 +667,16 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Persists a <c>Hydric_Balance_Calculated</c> observation mirroring the supplied
/// <c>Hydric_Balance</c> value, after enforcing three guards: the observation's hour must not be
/// in the future, the latest <c>Hydric_Balance_Calculated</c> for the current hour prevents
/// replaying older hours, and a newer stored <c>Hydric_Balance_Calculated</c> prevents
/// overwriting it. The stored time is shifted one second forward when an existing calculated
/// observation in the same hour is detected.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Hydric_Balance</c>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateHydricBalanceCalculated(BasePatientObservation obs)
{
//Hydric_Balance_Calculated
@@ -580,6 +744,15 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(calculatedHydricBalance);
}
/// <summary>
/// Maps a <c>Resp_Mode</c> observation to a <c>Resp_Type</c> value: codes listed in
/// <c>InvasiveVentilation</c> yield <c>Invasive</c>, otherwise the observation's value is matched
/// against the configured high-frequency / non-invasive catalogues (yielding
/// <c>HighFrequencyVentilation</c>, <c>NonInvasive</c>, or <c>Invasive</c> as fallback). Values
/// equal to <c>"EnESPERA"</c> are skipped.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Resp_Mode</c>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateVentilationMode(BasePatientObservation obs)
{
_logger.LogDebug("CalculateVentilationMode {obs}", obs);
@@ -620,6 +793,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Computes the <c>Weight_Diff</c> observation (new weight previous weight, rounded to two
/// decimals) by comparing the supplied <c>Weight_Current</c> observation against the most
/// recently stored one for the same patient. Skipped when either value cannot be parsed.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Weight_Current</c>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateWeight_DiffObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -651,6 +831,14 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Computes the <c>Diuresis_Weight</c> observation (diuresis / current weight in ml/kg) when
/// either a <c>Diuresis</c> or a <c>Weight_Current</c> observation is received. When triggered
/// by <c>Weight_Current</c>, the operation is skipped if the latest <c>Diuresis</c> observation
/// has expired. Skipped silently when the weight is zero or any value is unparsable.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (<c>Diuresis</c> or <c>Weight_Current</c>).</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateDiureis_WeightObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -695,6 +883,14 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Aggregates a <c>AllergiesObs</c> observation into a single <c>Allergies</c> string per type,
/// special-casing drug allergies ("FÁRMACOS") into a single grouped entry and setting the
/// <c>Status</c> to <c>Alert</c> when drug allergies are present. <c>InvalidCastException</c>s
/// are logged and swallowed.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>AllergiesObs</c> and a collection-valued <c>Value</c> of <see cref="PatientAllergiesValue"/>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateAllergiesObservation(BasePatientObservation obs)
{
try
@@ -764,6 +960,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// For a <c>DrainagesObs</c> observation of type <c>"Drenaje ventricular"</c>, persists the
/// derived <c>DVE</c> (volume) and <c>Drainage_Height</c> observations using the underlying
/// <see cref="PatientDrainagesValue"/> properties.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>DrainagesObs</c> and a <see cref="PatientDrainagesValue"/>-typed <c>Value</c>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateDrainagesObservation(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -803,6 +1006,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Computes the <c>Delta_Pressure</c> observation (driving pressure = plateau PEEP) when both
/// <c>PEEP</c> and <c>Pleateu_Pressure</c> observations are available. The stored <c>Time</c> is
/// the most recent of the two source observations, so the result is anchored to the latest input.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (<c>PEEP</c> or <c>Pleateu_Pressure</c>).</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateDelta_PressureObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -854,6 +1064,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Persists a <c>Daily_Balance_Calculated</c> observation mirroring the supplied
/// <c>Daily_Balance</c> value, but only when the local hour of the observation is 8 (the daily
/// balance cut-off used by the HRYC customization).
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Daily_Balance</c>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateDaily_BalanceObservation(BasePatientObservation obs)
{
if (obs.Time.ToLocalTime().Hour == 8)
@@ -872,6 +1089,15 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Determines whether a patient observation has expired by comparing the current time against
/// <c>Time + Expires</c> seconds, when <c>Expires</c> is set.
/// </summary>
/// <param name="obs">The patient observation to evaluate.</param>
/// <returns>
/// <see langword="true"/> when the observation has an <c>Expires</c> value and the current time
/// is past the expiration instant; otherwise, <see langword="false"/>.
/// </returns>
private static bool CheckExpired(PatientObservation obs)
{
if (obs.Expires == null) return false;
@@ -11,6 +11,10 @@ using MongoDB.Driver;
namespace adas_core.Application.Customizations.HUVH.UCIA;
/// <summary>
/// Represents a concrete implementation of the <see cref="ICalculatedObservations"/> interface,
/// providing functionality to manage a collection of calculated observations.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private const string CodingSystem = "ADAS";
@@ -53,12 +57,23 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The identifier of the patient whose active bolus should be calculated.</param>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the calculation logic has not yet been implemented.</exception>
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Processes a patient observation by dispatching to the appropriate domain-specific calculation based on the observation name, handling over-sedation, ventilation mode, ECMO location, over-analgesia, driving pressure, ROX index, diuresis, and rehabilitation alarm assessments.
/// </summary>
/// <param name="obs">The patient observation to map; may be replaced with the result of the matched asynchronous calculation when applicable.</param>
/// <param name="onlyByName">Indicates whether the mapping should be performed using only the observation's name.</param>
/// <returns>The processed patient observation, potentially updated by the applicable calculation.</returns>
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
if (obs.Name is "PSI" or "RASS" or "TS") _ = CalculateOverSedation(obs);
@@ -81,6 +96,11 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> by determining the appropriate order control status, handling cases for missing placer orders, previous expired treatments, and treatments with a defined end time.
/// </summary>
/// <param name="treatment">The patient treatment to map and update.</param>
/// <returns>The patient treatment with its order control set according to the evaluated business rules.</returns>
public async Task<PatientTreatment> Map(PatientTreatment treatment)
{
var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden
@@ -100,12 +120,22 @@ public class CalculatedObservations : ICalculatedObservations
return treatment;
}
/// <summary>
/// Maps a <see cref="PumpObservation"/> instance to its output representation as a pass-through operation.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A completed task containing the provided <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Calculates medicine observations for a patient by validating the categories of their active medications. Typically invoked by the scheduler service to review current medication statuses.
/// </summary>
/// <param name="activeMedicines">The list of currently active medicines assigned to the patient.</param>
/// <param name="patientId">The unique identifier of the patient whose medication categories will be checked.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
//Esto viene del schedulerService para chequear los medicamentos activos
@@ -114,6 +144,11 @@ public class CalculatedObservations : ICalculatedObservations
return Task.CompletedTask;
}
/// <summary>
/// Retrieves all currently active treatments associated with the specified patient.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments should be retrieved.</param>
/// <returns>A collection of <see cref="PatientTreatment"/> objects representing the patient's active treatments.</returns>
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
@@ -121,13 +156,24 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Asynchronously maps a <see cref="PatientDiagnosis"/> instance to its corresponding data representation or transfer model.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the resulting <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the mapping logic has not yet been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Ensures the timestamp of the new observation does not collide with the most recent observation for the same patient and observation name. If the new observation's time is equal to or earlier than the last observation's time (compared at second precision), the new observation's time is incremented by one second. If the observation name is null or empty, the new observation is returned unchanged after logging an error.
/// </summary>
/// <param name="newObservation">The new patient observation whose timestamp should be adjusted to avoid time inconsistencies with prior observations.</param>
/// <returns>The patient observation, with its <c>Time</c> adjusted when a time collision is detected, or the original observation when its name is null or empty.</returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -155,21 +201,46 @@ public class CalculatedObservations : ICalculatedObservations
return newObservation;
}
/// <summary>
/// Pre-maps the provided list of patient observations, returning it as a completed task for asynchronous processing pipelines.
/// </summary>
/// <param name="listToInsert">The list of patient observations to pre-map before insertion.</param>
/// <returns>A completed task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm to a patient observation. Returns the provided observation without applying changes from the alarm.
/// </summary>
/// <param name="obs">The patient observation to be returned as the mapping result.</param>
/// <param name="alarmToInsert">The patient observation alarm intended to be associated with the observation.</param>
/// <returns>A task containing the patient observation.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Asynchronously sends an alarm notification associated with a patient observation, identified by a name and an optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code categorizing the alarm, or null if no code is specified.</param>
/// <returns>A task that represents the asynchronous alarm sending operation.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the method has not yet been implemented.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Evaluates a rehabilitation observation and marks it as Alert when the degree value is between 0 and 2 (inclusive) for more than seven consecutive days.
/// The observation is returned unchanged when it is not a patient observation, its value cannot be parsed as an integer, or its value is greater than 2.
/// </summary>
/// <param name="obs">The base patient observation to evaluate for the rehabilitation alarm condition.</param>
/// <returns>The observation with its status set to Alert when the six previous rehabilitation observations also fall within the 0-2 range; otherwise the observation is returned as-is.</returns>
private async Task<BasePatientObservation> CalculateRehabilitationAlarm(BasePatientObservation obs)
{
//En rojo si el grado es de 0 a 2 incluidos, por más de 7 días
@@ -186,6 +257,14 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Calculates the diuresis index in ml/kg/h for a patient by summing the diuresis values from the last six hours
/// and dividing by the patient's weight and by six. Handles trigger observations of type "Diuresis" (which also
/// pulls the latest recorded weight) and "Weight" (which supplies the weight directly), and aborts with a warning
/// when fewer than three diuresis values are available or the weight is not greater than zero.
/// </summary>
/// <param name="obs">The incoming base patient observation; only <see cref="PatientObservation"/> instances whose
/// <c>Name</c> is "Diuresis" or "Weight" are processed, otherwise the method returns without changes.</param>
private async Task CalculateDiuresis(BasePatientObservation obs)
{
// Calculado como el sumatorio de la Diuresis de las últimas 6h
@@ -262,6 +341,10 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Calculates the ROX Index by dividing the SpO2/FiO2 ratio by the respiratory rate (FR) and persists the result as a new <c>Rox_Index</c> observation. The method supports both directions of the calculation: when the incoming observation is the SpO2/FiO2 ratio, it retrieves the latest FR, and vice versa. It silently returns if the observation is not a <c>PatientObservation</c>, if the observation name is not recognized, if the required paired observation cannot be found, if its value cannot be parsed, or if the FR value is zero (to avoid division by zero).
/// </summary>
/// <param name="obs">The base patient observation that triggers the ROX index calculation. Only observations named <c>SpO2_FiO2_Ratio</c> or <c>FR</c> are processed; any other type or name is ignored.</param>
private async Task CalculateRoxIndex(BasePatientObservation obs)
{
// Cálculo dividiendo el ratio SpO2/FiO2 entre la FR
@@ -310,6 +393,10 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(roxIndexObs, mapObs: false);
}
/// <summary>
/// Calculates the driving pressure for a patient as the difference between Pmeset and PEEP when a new Pmeset or PEEP observation is provided. Looks up the complementary observation to obtain the missing value, then creates and persists a "Driving_Pressure" observation. Returns early if the input is not a PatientObservation, the observation name is neither Pmeset nor PEEP, the complementary observation cannot be found, or its value cannot be parsed as an integer.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation; expected to represent a Pmeset or PEEP measurement.</param>
private async Task CalculateDrivingPressure(BasePatientObservation obs)
{
//Diferencia entre la Pmeset y la PEEP
@@ -361,6 +448,11 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Evaluates whether a patient meets the over-analgesia criteria based on pain-related observations (EVN between 0 and 3, ESCID equal to 3, or ANI greater than 70) combined with sustained infusions of morphine, remifentanil, or fentanyl exceeding the analgesia threshold, and records an "Over_Analgesia" observation when the conditions are satisfied.
/// </summary>
/// <param name="obs">The observation that triggered the evaluation; its value must be numeric and it is ignored if <paramref name="patientId"/> is provided (e.g., when invoked from a treatment context).</param>
/// <param name="patientId">The patient identifier when the evaluation is triggered by a treatment event; when null, it is derived from <paramref name="obs"/>.</param>
private async Task CalculateOverAnalgesia(BasePatientObservation? obs, ObjectId? patientId = null)
{
// Mandar observación 'SOBREANALGESIA'
@@ -435,6 +527,11 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Normalizes the ECMO location value of a patient observation. If the observation value matches one of the ECMO variants (ECCO2r, VVDL, VA+V, VVDL+V, VV+V, or VVA), it is mapped to "ECMO"; VV and VA values are left unchanged. If the observation is not a PatientObservation, it is returned unmodified.
/// </summary>
/// <param name="obs">The base patient observation to evaluate and potentially transform.</param>
/// <returns>A task that yields the observation, with its value normalized to "ECMO" when the original value is one of the recognized ECMO variants.</returns>
private Task<BasePatientObservation> CalculateEcmoLocation(BasePatientObservation obs)
{
//Las opciones pueden ser VV, VA o ECMO.
@@ -453,6 +550,15 @@ public class CalculatedObservations : ICalculatedObservations
return Task.FromResult(obs);
}
/// <summary>
/// Determines the ventilation mode for a patient based on the type of the incoming observation:
/// assigns <c>VMNI</c> when an Inspiratory Pressure value is received without a Compliancia value,
/// assigns <c>VMI</c> when a Compliancia value is received, and reads the mode from the CCC coding
/// system when an Air Flow value is received. If a mode is resolved, it is persisted as a new
/// <c>Ventilation_Mode</c> patient observation; otherwise the method returns without inserting
/// anything.
/// </summary>
/// <param name="obs">The incoming patient observation whose name drives the ventilation mode calculation.</param>
private async Task CalculateVentilationMode(BasePatientObservation obs)
{
//- Si llega valor de PI pero NO COMPL, el tipo de ventilación es VMNI
@@ -501,6 +607,13 @@ public class CalculatedObservations : ICalculatedObservations
_ = _observationService.Value.InsertObservation(ventilationModeObservation, mapObs: false);
}
/// <summary>
/// Evaluates whether an "Over_Sedation" clinical condition should be recorded for a patient.
/// The condition is met when RASS is -4 or -5, PSI is below 25, TS is above 5, and an active treatment exceeds its sedation threshold (Propofol &gt; 3, Midazolam &gt; 0.05, or Isoflurane &gt; 10).
/// If invoked with a single observation, the method extracts the patient identifier, validates the observation value against its own threshold, and then loads the remaining required observations (RASS, PSI, TS) to confirm the full set of conditions before persisting the Over_Sedation observation.
/// </summary>
/// <param name="obs">The triggering patient observation used to evaluate the sedation state. When provided, its name and value drive the initial validation; when null, the method relies on the <paramref name="patientId"/> parameter (typically from a treatment update).</param>
/// <param name="patientId">Optional identifier of the patient to evaluate. If null, it is derived from <paramref name="obs"/>; otherwise, the method performs a full evaluation using the patient's recent observations.</param>
private async Task CalculateOverSedation(BasePatientObservation? obs, ObjectId? patientId = null)
{
//Mandar observación 'SOBRESEDACIÓN'
@@ -576,6 +689,12 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Determines whether the given patient treatment has exceeded the 24-hour sustained analgesia threshold (e.g., morphine, remifentanil, or fentanyl perfusions).
/// Returns false when the treatment is null or its start time is not set.
/// </summary>
/// <param name="treatment">The patient treatment to evaluate; may be null.</param>
/// <returns><c>true</c> if the treatment has a start time and was started more than 24 hours ago; otherwise, <c>false</c>.</returns>
private static bool ExceedsAnalgesiaThreshold(PatientTreatment? treatment)
{
// Perfusiones de morfina, remifentanilo o fentanilo sostenidas > 24 h
@@ -584,6 +703,11 @@ public class CalculatedObservations : ICalculatedObservations
treatment.StartTime.Value.AddSeconds(86400) < DateTime.UtcNow;
}
/// <summary>
/// Determines whether a patient treatment exceeds the sedation threshold for Propofol, Midazolam, or Isoflurane based on requested minimum dosage amounts.
/// </summary>
/// <param name="treatment">The patient treatment to evaluate, or <c>null</c>.</param>
/// <returns><c>true</c> if the treatment is not <c>null</c> and any requested medication exceeds its defined sedation threshold; otherwise, <c>false</c>.</returns>
private static bool ExceedsSedationThreshold(PatientTreatment? treatment)
{
return (treatment != null &&
@@ -595,6 +719,11 @@ public class CalculatedObservations : ICalculatedObservations
c.Text == "Isoflorano" && treatment.RequestedGiveAmountMinimum > 10));
}
/// <summary>
/// Checks the active treatments of a patient and discontinues those whose end time is in the future by setting their <see cref="PatientTreatment.OrderControl"/> to <see cref="OrderControlType.Dc"/>, returning the remaining active treatments.
/// </summary>
/// <param name="treatment">The patient treatment whose patient is used to look up the list of active treatments to evaluate.</param>
/// <returns>A task that returns the list of active patient treatments that were not discontinued.</returns>
private async Task<List<PatientTreatment>> CheckExpiredPatientTreatments(PatientTreatment treatment)
{
//si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas
@@ -616,6 +745,13 @@ public class CalculatedObservations : ICalculatedObservations
return result;
}
/// <summary>
/// Validates the medicines associated with a patient treatment by combining newly requested medicines
/// with the patient's currently active medicines, deduplicating them by name, and verifying their
/// medication categories. If no new medicines are found, the method returns without performing any
/// further validation.
/// </summary>
/// <param name="treatment">The patient treatment whose requested medicines will be checked against the patient's active medicines.</param>
private async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
var newMedicines = (await Task.WhenAll(treatment.RequestedGiveCodes
@@ -639,21 +775,26 @@ public class CalculatedObservations : ICalculatedObservations
CheckMedicationCategories(treatment.PatientId, uniqueMedicines as List<Medicine>);
}
/// <summary>
/// Classifies a patient's unique medicines into predefined clinical medication groups (e.g., sedation, inotropic, antibiotic, anxiolytic, antipsychotic, antidepressants, neuro medication, crystalloid/colloid serum, antihypertensives) and creates a multivalue observation for every category that contains at least one matching medicine. Categories whose list reference is null, medicines with a null or empty name, and categories with no matching medicines are skipped.
/// </summary>
/// <param name="patientId">The identifier of the patient whose medication categories are being evaluated and linked to the created observations.</param>
/// <param name="uniqueMedicines">The distinct list of medicines to be checked against the predefined category reference lists.</param>
private void CheckMedicationCategories(ObjectId patientId, List<Medicine> uniqueMedicines)
{
var medicationCategories = new Dictionary<string, List<string>?>
{
{ "Sedation_Medication_Multivalue", _sedationList },
{ "Inotropic_Medication_Multivalue", _inotropicMedicines },
{ "Antibiotic_Medication_Multivalue", _antibioticList },
{ "Anxiolytic_Medication_Multivalue", _anxiolyticsList },
{ "Antipsychotic_Medication_Multivalue", _antipsicoticList },
{ "Antidepressants_Medication_Multivalue", _antidepressantsList },
{ "Neuro_Medication_Multivalue", _neuroMedicationList },
{ "Crystalloid_Serum_Medication_Multivalue", _serumCrystalloidList },
{ "Colloid_Serum_Medication_Multivalue", _serumColloidList },
{ "Antihypertensives_Medication_Multivalue", _antihypertensivesList }
};
{
{ "Sedation_Medication_Multivalue", _sedationList },
{ "Inotropic_Medication_Multivalue", _inotropicMedicines },
{ "Antibiotic_Medication_Multivalue", _antibioticList },
{ "Anxiolytic_Medication_Multivalue", _anxiolyticsList },
{ "Antipsychotic_Medication_Multivalue", _antipsicoticList },
{ "Antidepressants_Medication_Multivalue", _antidepressantsList },
{ "Neuro_Medication_Multivalue", _neuroMedicationList },
{ "Crystalloid_Serum_Medication_Multivalue", _serumCrystalloidList },
{ "Colloid_Serum_Medication_Multivalue", _serumColloidList },
{ "Antihypertensives_Medication_Multivalue", _antihypertensivesList }
};
foreach (var category in medicationCategories)
{
@@ -668,8 +809,15 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Creates a new multi-value patient observation containing the names of the provided medications, expiring any
/// previous non-expired observation with the same name for the patient before inserting the new one.
/// </summary>
/// <param name="medications">The collection of medications whose names will be stored as the observation values.</param>
/// <param name="patientId">The identifier of the patient the observation belongs to.</param>
/// <param name="obsName">The name used to group and look up the previous observation for the same concept.</param>
private async Task CreateMedicationMultivalueObservation(IEnumerable<Medicine?> medications, ObjectId patientId,
string obsName)
string obsName)
{
var newObs = new PatientObservation
{
@@ -12,6 +12,10 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.HUVH.UCIN;
/// <summary>
/// Represents a concrete implementation of the <see cref="ICalculatedObservations"/> interface,
/// providing a collection of calculated observation data.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private const string Spo2PreObservationName = "SpO2";
@@ -46,12 +50,23 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient for whom the active bolus is calculated.</param>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a patient observation by applying the appropriate calculation logic based on its name, handling multi-value interventions, surgeries, complexity scoring, pre/post saturation differences, pain scales, and last defecation value.
/// </summary>
/// <param name="obs">The patient observation to be mapped, which may be transformed depending on its name.</param>
/// <param name="onlyByName">Indicates whether the mapping should be performed using only the observation name.</param>
/// <returns>The mapped patient observation, potentially enriched with computed values, wrapped in a task.</returns>
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
if (obs.Name is "Intervention_In" or "Intervention_Out") await CalculateInterventionMultiValueObservation(obs);
@@ -78,6 +93,12 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> by determining its <see cref="OrderControlType"/> based on prior treatments and expiration status.
/// If the placer order number is missing, the treatment is returned unchanged. The order control is set to <see cref="OrderControlType.Xo"/> when previous treatments exist, <see cref="OrderControlType.Dc"/> when the treatment has expired (end time set), or <see cref="OrderControlType.Nw"/> otherwise.
/// </summary>
/// <param name="treatment">The patient treatment to map, including its placer order, end time, and medicines.</param>
/// <returns>The mapped <see cref="PatientTreatment"/> with its order control type and medicines updated.</returns>
public async Task<PatientTreatment> Map(PatientTreatment treatment)
{
var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden
@@ -97,12 +118,22 @@ public class CalculatedObservations : ICalculatedObservations
return treatment;
}
/// <summary>
/// Maps the provided <see cref="PumpObservation"/> instance by returning it unchanged, wrapped in a completed task.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A completed <see cref="Task{TResult}"/> containing the provided <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Persists a multivalue medication observation for the specified patient based on the currently active medicines, expiring any previous medication observation recorded for the same patient.
/// </summary>
/// <param name="activeMedicines">The list of active medicines from which distinct medication names are extracted to build the observation value.</param>
/// <param name="patientId">The identifier of the patient for whom the medication observation is calculated and stored.</param>
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
var medicineObs = new PatientObservation
@@ -129,6 +160,11 @@ public class CalculatedObservations : ICalculatedObservations
medicineObs); //mapObs:volvemos a mapear la observación para que pase por el cálculo de la complejidad
}
/// <summary>
/// Asynchronously retrieves the collection of active treatments associated with the specified patient identifier.
/// </summary>
/// <param name="id">The unique <see cref="ObjectId"/> of the patient whose active treatments should be fetched.</param>
/// <returns>A task that represents the asynchronous operation, containing an <see cref="IEnumerable{T}"/> of nullable <see cref="PatientTreatment"/> instances for the patient.</returns>
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
@@ -136,12 +172,23 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Asynchronously maps a <see cref="PatientDiagnosis"/> to a target <see cref="PatientDiagnosis"/> representation.
/// </summary>
/// <param name="diagnosis">The source <see cref="PatientDiagnosis"/> instance to be mapped.</param>
/// <returns>A <see cref="Task{PatientDiagnosis}"/> that represents the asynchronous mapping operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="T:System.NotImplementedException">Thrown when the method is invoked, as the mapping logic has not yet been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Adjusts the time of a new patient observation to ensure it is at least one second after the most recent observation with the same name, preventing time conflicts. If the observation name is null or empty, the method logs an error and returns the observation unchanged.
/// </summary>
/// <param name="newObservation">The new patient observation whose time may be adjusted to follow the most recent observation for the same patient and measurement.</param>
/// <returns>The patient observation with a corrected time if a time conflict was detected, or the original observation if no adjustment was needed or the name was invalid.</returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -169,21 +216,45 @@ public class CalculatedObservations : ICalculatedObservations
return newObservation;
}
/// <summary>
/// Performs a pre-mapping operation on a list of patient observations, returning the list for further processing.
/// </summary>
/// <param name="listToInsert">The list of patient observations to pre-map.</param>
/// <returns>A task that represents the asynchronous operation, containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm onto a patient observation, returning the observation unchanged without applying the alarm data.
/// </summary>
/// <param name="obs">The patient observation that the alarm is to be associated with.</param>
/// <param name="alarmToInsert">The alarm intended to be inserted into the observation.</param>
/// <returns>A task containing the patient observation, returned as-is.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm notification for the specified patient observation, identified by name and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code categorizing the type of alarm to be sent.</param>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Calculates and stores a derived "Pain_Scale" observation in the ADAS coding system based on the provided source observation.
/// Handles the ALPS scale by using its value directly, and the NPASS scale by combining the complementary "NPASS_Sedation" and "NPASS_Analgesia" observations into a single combined value (defaulting to "0" when the complementary observation is not found).
/// Returns without creating a record when the observation is not a <see cref="PatientObservation"/> or when its name does not match any of the supported scales.
/// </summary>
/// <param name="obs">The source patient observation used to derive the pain scale; only instances of <see cref="PatientObservation"/> with a supported name (ALPS, NPASS_Sedation, NPASS_Analgesia) are processed.</param>
private async Task CalculatePainScale(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs) return;
@@ -197,24 +268,24 @@ public class CalculatedObservations : ICalculatedObservations
valueObs = pobs.Value;
break;
case "NPASS_Sedation":
{
//Buscamos la complementaria "NPASS Analgesia"
var analgesiaObs =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Analgesia"]);
var analgesiaValue = analgesiaObs.FirstOrDefault()?.Value.ToString() ?? "0";
{
//Buscamos la complementaria "NPASS Analgesia"
var analgesiaObs =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Analgesia"]);
var analgesiaValue = analgesiaObs.FirstOrDefault()?.Value.ToString() ?? "0";
valueObs = $"{pobs.Value}/{analgesiaValue}";
}
valueObs = $"{pobs.Value}/{analgesiaValue}";
}
break;
case "NPASS_Analgesia":
{
//Buscamos la complementaria "NPASS Sedation"
var sedationObs =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Sedation"]);
var sedationValue = sedationObs.FirstOrDefault()?.Value.ToString() ?? "0";
{
//Buscamos la complementaria "NPASS Sedation"
var sedationObs =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Sedation"]);
var sedationValue = sedationObs.FirstOrDefault()?.Value.ToString() ?? "0";
valueObs = $"{sedationValue}/{pobs.Value}";
}
valueObs = $"{sedationValue}/{pobs.Value}";
}
break;
default:
return;
@@ -232,6 +303,11 @@ public class CalculatedObservations : ICalculatedObservations
_ = _observationService.Value.InsertObservation(painScaleObs, mapObs: false);
}
/// <summary>
/// Calculates the last defecation value for a patient observation by assigning the observation's time, converted to local time and formatted as a date/time string, to its value. If the observation is not a <see cref="PatientObservation"/>, it is returned unchanged.
/// </summary>
/// <param name="obs">The base patient observation to process.</param>
/// <returns>The original <see cref="BasePatientObservation"/> if it is not a <see cref="PatientObservation"/>; otherwise, the <see cref="PatientObservation"/> with its value set to the locally formatted observation time.</returns>
private static BasePatientObservation CalculateLastDefecationValue(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs) return obs;
@@ -246,6 +322,10 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Calculates and persists a multi-value observation by appending the current value to the existing list of values for the corresponding "Multivalue" observation. Expirates the previous multi-value observation before inserting the new one.
/// </summary>
/// <param name="obs">The base patient observation used to derive the multi-value observation; it must be a <see cref="PatientObservation"/> with a non-empty name and a string value.</param>
private async Task CalculateMultiValueObservation(BasePatientObservation obs)
{
//El valor de la observación es un array de strings
@@ -298,6 +378,10 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(newObservation);
}
/// <summary>
/// Calculates and persists a multi-value observation that aggregates the active intervention groups (e.g., devices, routes) for a patient based on incoming "<c>_In</c>" and "<c>_Out</c>" observations. Handles the insertion of new groups (with special duplicate prevention for respiratory devices) and the removal of groups when a corresponding "Out" event is received, expiring the previous multi-value observation and inserting a new one when the resulting set is not empty.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation; expected to be a <see cref="PatientObservation"/> with a non-empty name and a string value representing the intervention group.</param>
private async Task CalculateInterventionMultiValueObservation(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name) ||
@@ -380,6 +464,14 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(newObservation);
}
/// <summary>
/// Extracts a numeric code from the observation value, normalizes it, and resolves the corresponding intervention
/// by looking it up in the mapping repository. Returns false when the code cannot be parsed as a number or when no
/// matching intervention is found.
/// </summary>
/// <param name="valueObs">Observation text from which the leading token is taken as the candidate intervention code.</param>
/// <param name="r">When the method returns true, contains the resolved intervention's type, name, and group; otherwise null.</param>
/// <returns><c>true</c> if a matching intervention was found; <c>false</c> if the code is not numeric or the lookup yields no result.</returns>
private bool GetInterventionValue(string valueObs, out (string type, string Name, string Group)? r)
{
var code = valueObs.Split(" ")[0]; //
@@ -404,6 +496,10 @@ public class CalculatedObservations : ICalculatedObservations
* cuando entra una nueva observación que afecte a complejidad, se recalcula de 0 para no tener que estar pendiente de qué puntos corresponden a qué observación
* Valor máximo 46 Guardamos la observación que nos llega y recalculamos complejidad.
*/
/// <summary>
/// Calcula la complejidad del paciente en función de las observaciones registradas.
/// </summary>
/// <param name="obs">Observación del paciente que se va a evaluar para recalcular su complejidad.</param>
private async Task CalculateComplexity(BasePatientObservation obs)
{
var pobs = obs as PatientObservation;
@@ -466,8 +562,15 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(complexity, mapObs: false);
}
/// <summary>
/// Calculates the total complexity value for a multivalue observation belonging to a patient. Uses the supplied observation when provided; otherwise retrieves the most recent one from the database and sums the complexity values resolved via the mapping utility, returning 0 when no data is available.
/// </summary>
/// <param name="patientId">The identifier of the patient whose observation is being evaluated.</param>
/// <param name="observationName">The name of the multivalue observation to look up and calculate complexity for.</param>
/// <param name="observation">An optional pre-loaded observation that overrides the database lookup when supplied.</param>
/// <returns>The aggregated complexity value for the observation, or 0 if no observation or values are found.</returns>
private async Task<int> CalculateMultivalueObservationComplexityValue(ObjectId patientId, string observationName,
PatientObservation? observation = null)
PatientObservation? observation = null)
{
var result =
await _observationService.Value.FindLastObservations(patientId, 1, [observationName]);
@@ -486,6 +589,13 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Calculates the complexity value associated with a newborn's weight observation for the specified patient.
/// When <paramref name="pobs"/> is null, the most recent stored "Newborn_Weight" observation is used; otherwise the provided observation is used, converting from kilograms when applicable. Returns 0 when no value can be retrieved or parsed, or when the observation name is missing, and otherwise returns the complexity value resolved via the mapping utilities.
/// </summary>
/// <param name="patientId">The identifier of the patient whose newborn weight complexity is being calculated.</param>
/// <param name="pobs">An optional patient observation to use instead of the latest stored one; if provided with "kg" units, its value is converted.</param>
/// <returns>The calculated newborn weight complexity value, or 0 when no valid value is available.</returns>
private async Task<int> CalculateWeightNewBornValue(ObjectId patientId, PatientObservation? pobs = null)
{
var result =
@@ -522,6 +632,12 @@ public class CalculatedObservations : ICalculatedObservations
return weightNewBornValue;
}
/// <summary>
/// Parses a weight observation by converting its value to grams (multiplying by 1000) and assigning the "gr" unit.
/// If the value cannot be parsed as a double, the error is logged and the original observation is returned unchanged.
/// </summary>
/// <param name="obs">The patient observation containing the weight value to parse and convert.</param>
/// <returns>The patient observation with the value converted to grams, or the original observation if the value could not be parsed.</returns>
private PatientObservation ParseWeight(PatientObservation obs)
{
if (!double.TryParse(obs.Value.ToString(), out var dValue))
@@ -535,7 +651,13 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Calculates the SpO2 difference between pre and post observations for a patient and persists the resulting SpO2_Diff and combined pre/post observations.
/// Handles both pre and post observation entry points, adjusts the timestamp to the earlier of the two observations, and defaults to 0 when a value cannot be parsed as a double.
/// </summary>
/// <param name="obs">The incoming patient observation (either a pre or post SpO2 measurement) used to drive the calculation and identify the counterpart observation.</param>
/// <returns>A task that represents the asynchronous insert of the derived observations.</returns>
private async Task CalculateSaturation_DiffObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -593,6 +715,11 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Checks for expired patient treatments and updates the matching treatment's <see cref="OrderControlType"/> to <c>DC</c> when its end time is in the future, returning the remaining active treatments for the same patient.
/// </summary>
/// <param name="treatment">The patient treatment being evaluated, used to locate the existing record by its placer order entity identifier and to persist the updated state.</param>
/// <returns>A task that yields the list of active patient treatments for the patient that are not the same placer order as the supplied treatment.</returns>
private async Task<List<PatientTreatment>> CheckExpiredPatientTreatments(PatientTreatment treatment)
{
//si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas
@@ -622,6 +749,14 @@ public class CalculatedObservations : ICalculatedObservations
return result;
}
/// <summary>
/// Validates the medicines requested in a patient treatment, handling nutrition items by creating
/// dedicated observations, detecting vasoactive drugs to record a flag, and combining remaining
/// medicines with the patient's active treatment medicines to calculate a unified medicine observation.
/// Medicines that are not found by code are skipped, and the method returns early when no new
/// medicines remain after filtering.
/// </summary>
/// <param name="treatment">The patient treatment whose requested give codes are inspected for medicines, nutrition items, and vasoactive drugs.</param>
private async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
var newMedicines = new List<Medicine>();
@@ -679,6 +814,11 @@ public class CalculatedObservations : ICalculatedObservations
await CalculateMedicineObservation(uniqueMedicines, treatment.PatientId);
}
/// <summary>
/// Creates a new feeding/nutrition observation for the specified patient. When the value indicates parenteral feeding, the observation name is normalized to "Parenteral" and the value to "SI". Any previous non-expired observation with the same name is marked as expired before the new one is inserted.
/// </summary>
/// <param name="value">The feeding type value to record; if it contains "parenteral" (case-insensitive), the observation is stored as a parenteral entry.</param>
/// <param name="patientId">The identifier of the patient to whom the observation belongs.</param>
private async Task CreateNutritionObservation(string value, ObjectId patientId)
{
var nutritionObs = new PatientObservation
@@ -7,63 +7,127 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.NursePlan;
/// <summary>
/// Provides calculated observations by leveraging services obtained from the injected service provider.
/// </summary>
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly Lazy<IObservationService> _observationService =
new(serviceProvider.GetRequiredService<IObservationService>);
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active bolus is to be calculated.</param>
/// <exception cref="System.NotImplementedException">Thrown when the method is invoked, as the calculation logic has not yet been implemented.</exception>
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Asynchronously maps the provided PumpObservation, returning the input instance as the mapping result.
/// </summary>
/// <param name="pumpObservation">The PumpObservation to be mapped.</param>
/// <returns>A task containing the mapped PumpObservation.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Asynchronously calculates a medicine observation for the specified patient based on their active medicines.
/// </summary>
/// <param name="activeMedicines">The collection of medicines currently active for the patient.</param>
/// <param name="patientId">The identifier of the patient whose medicine observation is being calculated.</param>
/// <exception cref="NotImplementedException">The method is not yet implemented.</exception>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Asynchronously resolves time inconsistencies between the supplied observation and the most recent previous observation, returning a corrected observation or <c>null</c> when no correction is required.
/// </summary>
/// <param name="newObservation">The new patient observation whose timestamps may need to be reconciled with the last recorded observation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the fixed <see cref="PatientObservation"/>, or <c>null</c> when no time inconsistency is detected.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
throw new NotImplementedException();
}
/// <summary>
/// Asynchronously retrieves the active treatment records associated with the specified patient.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments are being queried.</param>
/// <returns>A task representing the asynchronous operation, containing a collection of active PatientTreatment entries, which may include null values, for the specified patient.</returns>
/// <exception cref="NotImplementedException">The method has not yet been implemented.</exception>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
throw new NotImplementedException();
}
/// <summary>
/// Returns the provided patient observation wrapped in a completed task without modification.
/// </summary>
/// <param name="obs">The patient observation to return.</param>
/// <param name="onlyByName">Reserved flag that is not used in the current implementation.</param>
/// <returns>A completed task containing the provided observation.</returns>
public Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
return Task.FromResult(obs)!;
}
/// <summary>
/// Maps a patient treatment to its target representation asynchronously, returning the provided treatment instance unchanged.
/// </summary>
/// <param name="treatment">The patient treatment to map.</param>
/// <returns>A task containing the mapped patient treatment.</returns>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
return Task.FromResult(treatment);
}
/// <summary>
/// Maps a <see cref="PatientDiagnosis"/> instance to a completed task containing the same instance.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to be returned as a completed task result.</param>
/// <returns>A completed <see cref="Task{PatientDiagnosis}"/> containing the provided diagnosis.</returns>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
/// <summary>
/// Returns the provided <paramref name="obs"/> as-is, ignoring the <paramref name="alarmToInsert"/> parameter.
/// </summary>
/// <param name="obs">The patient observation to return.</param>
/// <param name="alarmToInsert">The patient observation alarm; not used in the current implementation.</param>
/// <returns>A completed task containing the original <paramref name="obs"/>.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm notification based on the specified patient observation, name, and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code identifying the alarm type.</param>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
return Task.CompletedTask;
}
/// <summary>
/// Returns the provided patient observation list unchanged as a pre-mapping step, typically used as a hook before further mapping or insertion operations.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped.</param>
/// <returns>A completed <see cref="Task{TResult}"/> containing the original list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
@@ -1,3 +1,7 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Represents errors that occur during API request processing, wrapping a descriptive message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public class ApiRequestException(string message) : Exception($"Api request exception: {message}");
@@ -2,12 +2,24 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Represents an exception that is thrown when a bad or invalid request is received.
/// It serves as a specialized error type derived from the base <see cref="Exception"/> class for signaling client-side request failures.
/// </summary>
public class BadRequestException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="BadRequestException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message defined in <see cref="HttpEnum.ErrorMessage"/> that describes the reason for the exception.</param>
public BadRequestException(HttpEnum.ErrorMessage message) : base(((int)message).ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BadRequestException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message that describes the reason for the exception.</param>
public BadRequestException(string message) : base(message)
{
}
@@ -2,12 +2,23 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Represents an exception that is thrown when a conflict is encountered, such as a resource state mismatch or a duplicate entry.
/// </summary>
public class ConflictException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ConflictException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message that describes the conflict.</param>
public ConflictException(HttpEnum.ErrorMessage message) : base(((int)message).ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ConflictException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public ConflictException(string message) : base(message)
{
}
@@ -2,12 +2,26 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Represents a custom exception that is thrown when one or more arguments provided to a method are invalid.
/// </summary>
/// <remarks>
/// This exception extends the base <see cref="Exception"/> class to provide a domain-specific error type for argument validation failures.
/// </remarks>
public class CustomArgumentException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomArgumentException"/> class with the specified error message.
/// </summary>
/// <param name="message">The error message from the <see cref="HttpEnum.ErrorMessage"/> enumeration.</param>
public CustomArgumentException(HttpEnum.ErrorMessage message) : base(((int)message).ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomArgumentException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message that describes the reason for the exception.</param>
public CustomArgumentException(string message) : base(message)
{
}
@@ -2,12 +2,26 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Represents an exception that is thrown when an operation is forbidden or access to a resource is denied.
/// </summary>
/// <remarks>
/// This exception is intended to signal that a requested action violates access rules or restrictions, allowing callers to handle forbidden scenarios distinctly from other error conditions.
/// </remarks>
public class ForbbidenException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ForbbidenException"/> class with a specified error message from the <see cref="HttpEnum.ErrorMessage"/> enumeration.
/// </summary>
/// <param name="message">The <see cref="HttpEnum.ErrorMessage"/> enumeration value whose integer representation is used as the exception message.</param>
public ForbbidenException(HttpEnum.ErrorMessage message) : base(((int)message).ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ForbbidenException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public ForbbidenException(string message) : base(message)
{
}
@@ -2,12 +2,23 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Represents the exception that is thrown when data is encountered in a format that is not valid or expected.
/// </summary>
public class InvalidFormatException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="InvalidFormatException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message that describes the reason for the exception.</param>
public InvalidFormatException(HttpEnum.ErrorMessage message) : base(((int)message).ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InvalidFormatException"/> class with a specified error message.
/// </summary>
/// <param name="message">The error message that describes the reason for the exception.</param>
public InvalidFormatException(string message) : base(message)
{
}
@@ -2,17 +2,31 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Represents an exception that is thrown when a requested resource or item cannot be found.
/// </summary>
public class NotFoundException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="NotFoundException"/> class.
/// </summary>
public NotFoundException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="NotFoundException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public NotFoundException(HttpEnum.ErrorMessage message) : base(((int)message).ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="NotFoundException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public NotFoundException(string message) : base(message)
{
}
}
}
@@ -1,3 +1,7 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Represents an exception that is thrown when a token-related error occurs.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public class TokenException(string message) : Exception(message);
@@ -2,12 +2,23 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Exception thrown when a user lacks authorization to perform an operation.
/// </summary>
public class UnauthorizedException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="UnauthorizedException"/> class.
/// </summary>
/// <param name="message">The error message associated with the exception.</param>
public UnauthorizedException(HttpEnum.ErrorMessage message) : base(((int)message).ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UnauthorizedException"/> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public UnauthorizedException(string message) : base(message)
{
}
@@ -2,13 +2,27 @@
namespace adas_core.Application.Exceptions;
/// <summary>
/// Represents an exception that is thrown when an entity cannot be processed due to issues with its content or structure.
/// </summary>
/// <remarks>
/// This exception is typically used to signal that a request or entity was understood but could not be processed, similar to the semantics of an HTTP 422 (Unprocessable Entity) response.
/// </remarks>
public class UnprocessableEntityException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="UnprocessableEntityException"/> class with a specified error message enum.
/// </summary>
/// <param name="message">The error message enum containing the error code.</param>
public UnprocessableEntityException(HttpEnum.ErrorMessage message) : base(((int)message).ToString())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UnprocessableEntityException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public UnprocessableEntityException(string message) : base(message)
{
}
}
}
@@ -15,6 +15,13 @@ public class AdasProvider(IOptions<ProvidersSettings> providerSettings, IHttpCli
* Predict of UCI stay duration
* Predict of medications for patients
*/
/// <summary>
/// Retrieves patient observations from the ADAS system, including stay duration predictions (most and less confident scenarios) and medication predictions.
/// Logs an error and skips stay prediction observations if the prediction result is null or contains an error.
/// Skips the medication prediction observation if no medication predictions are returned.
/// </summary>
/// <param name="patient">The patient for whom observations are being calculated.</param>
/// <returns>A task that represents the asynchronous operation, returning a list of patient observations.</returns>
public override async Task<List<PatientObservation>> GetObservations(Patient patient)
{
var calculatedObservations = new List<PatientObservation>();
@@ -69,12 +76,23 @@ public class AdasProvider(IOptions<ProvidersSettings> providerSettings, IHttpCli
return calculatedObservations;
}
/// <summary>
/// Concatenates up to the first five ADAS medication prediction results into a single caret-delimited string for use as a medication observation value.
/// </summary>
/// <param name="medicationPredict">The list of ADAS medication prediction results to be serialized. Only the first five entries are included.</param>
/// <returns>A string containing the selected medication prediction entries joined by the "^" delimiter.</returns>
private static string ParseAdasMedicationsToMedicationObservation(
List<ResultModelPredictMedicationAdas> medicationPredict)
List<ResultModelPredictMedicationAdas> medicationPredict)
{
return string.Join("^", medicationPredict.Take(5));
}
/// <summary>
/// Retrieves the ADAS prediction of length of stay for the specified patient from an external service.
/// Returns null if the patient number is null, if the service response is unsuccessful, or if an exception occurs during the request.
/// </summary>
/// <param name="patientNumber">The unique identifier of the patient whose ADAS stay prediction is being requested.</param>
/// <returns>A task containing the deserialized <see cref="ResultModelPredictOfStayAdas"/> result on success, or null if the patient number is null, the response is unsuccessful, or the call throws.</returns>
private async Task<ResultModelPredictOfStayAdas?> GetPredictOfStay(string? patientNumber)
{
if (patientNumber == null)
@@ -109,6 +127,12 @@ public class AdasProvider(IOptions<ProvidersSettings> providerSettings, IHttpCli
}
}
/// <summary>
/// Retrieves the predicted medication needs for a given patient by calling the pharmacy prediction service.
/// Returns null if the patient number is null, the service response is unsuccessful, or an exception occurs during the request.
/// </summary>
/// <param name="patientNumber">The unique identifier of the patient whose ADAS medication predictions are being retrieved.</param>
/// <returns>A task containing a list of <see cref="ResultModelPredictMedicationAdas"/> with the predicted medication data, or null if the request fails or the patient number is null.</returns>
private async Task<List<ResultModelPredictMedicationAdas>?> GetPredictMedications(string? patientNumber)
{
if (patientNumber == null)
@@ -141,6 +165,10 @@ public class AdasProvider(IOptions<ProvidersSettings> providerSettings, IHttpCli
}
}
/// <summary>
/// Creates a new <see cref="HttpClient"/> instance and configures it with the configured base address URL.
/// </summary>
/// <returns>An <see cref="HttpClient"/> with its <see cref="HttpClient.BaseAddress"/> set to the configured <c>Url</c>.</returns>
private HttpClient GetClient()
{
var client = HttpClientFactory.CreateClient();
@@ -12,5 +12,10 @@ public abstract class BaseProvider(
protected IHttpClientFactory HttpClientFactory = httpClientFactory;
protected string Url { get; set; } = providerSettings.Value.Url;
/// <summary>
/// Asynchronously retrieves the list of clinical observations associated with the specified patient.
/// </summary>
/// <param name="patient">The patient whose observations are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientObservation"/> entries for the patient.</returns>
public abstract Task<List<PatientObservation>> GetObservations(Patient patient);
}
+338
View File
@@ -0,0 +1,338 @@
# adas-core.Application — Use Case Layer
> The **Application Layer** of the ADAS Core platform.
> Contains application services, use case orchestration, repository contracts, caching abstractions, and domain-specific exceptions. This layer defines **what** the system does, delegating **how** to the Infrastructure layer.
---
## Table of Contents
1. [Overview](#overview)
2. [Responsibilities](#responsibilities)
3. [Project Structure](#project-structure)
4. [Dependencies](#dependencies)
5. [Repository Contracts](#repository-contracts)
6. [Application Services](#application-services)
7. [Caching Abstractions](#caching-abstractions)
8. [Exceptions](#exceptions)
9. [Environment Customizations](#environment-customizations)
10. [Design Rules](#design-rules)
---
## Overview
`adas-core.Application` sits between the **Domain** and **Infrastructure** layers. It orchestrates domain entities into complete use cases, enforces application-level rules, and exposes repository contracts that Infrastructure implements.
Key characteristics:
- **Pure orchestration** — Services coordinate domain objects but contain no persistence logic.
- **Repository contracts** — Interfaces in `Repositories/Interfaces/` define data access contracts; concrete implementations live in `adas-core.Infrastructure`.
- **DTO-less where possible** — Services consume and return domain entities directly when serialization concerns are handled upstream.
- **Pluggable caching** — Abstracted behind `ICacheService` with Redis or in-memory fallbacks.
- **Environment-specific logic** — Calculated observations vary per deployment via the `Customizations` folder.
---
## Responsibilities
| Concern | What this project does |
|---------|----------------------|
| **Use Case Orchestration** | Application services execute high-level workflows (admit patient, record observation, trigger alert, etc.). |
| **Repository Contracts** | Defines `I*` repository interfaces that Infrastructure must satisfy. |
| **Caching Strategy** | Provides `ICacheService`, `ILockProvider`, and `LockManagerService` for distributed or in-memory caching. |
| **External Provider Facade** | `AdasProvider` / `BaseProvider` abstract external integrations so domain logic remains clean. |
| **Real-Time Subscriptions** | `SubscribersService` and grouped subscriber models manage WebSocket client subscriptions. |
| **Calculated Observations** | `CalculatedObservationsService` evaluates patient observations using rules customized per environment. |
| **Scheduled Jobs** | `SchedulerService` coordinates Quartz-based background tasks. |
| **Exception Taxonomy** | Domain-relevant exceptions (`NotFoundException`, `ConflictException`, etc.) for predictable error handling. |
---
## Project Structure
```
adas-core.Application/
├── Repositories/
│ └── Interfaces/ # Repository contracts (~40 interfaces)
│ ├── IMongoRepository.cs
│ ├── IUserRepository.cs
│ ├── IPatientRepository.cs
│ ├── IAdmissionRepository.cs
│ ├── IObservationRepository.cs
│ ├── ITreatmentRepository.cs
│ ├── IAlarmRepository.cs
│ ├── IDeviceRepository.cs
│ ├── ILightBeaconRepository.cs
│ ├── IRelayRepository.cs
│ ├── IPump*Repository.cs
│ ├── IRecordingAlertRepository.cs
│ ├── IConfig*Repository.cs
│ ├── IUnitRepository.cs
│ ├── IDisplay*Repository.cs
│ ├── IAppointmentRepository.cs
│ ├── IPatientCarePlanRepository.cs
│ ├── IMasterListRepository.cs
│ └── ...
├── Services/
│ ├── Interfaces/ # Service contracts (~40 interfaces)
│ │ ├── IAuthService.cs
│ │ ├── IPatientService.cs
│ │ ├── IObservationService.cs
│ │ ├── ICalculatedObservationsService.cs
│ │ ├── ITreatmentService.cs
│ │ ├── IDeviceService.cs
│ │ ├── IAlarmService.cs
│ │ ├── IDisplayService.cs
│ │ ├── IConfigObservationService.cs
│ │ ├── IUnitService.cs
│ │ ├── IAppointmentService.cs
│ │ ├── IPublisherService.cs
│ │ ├── ICacheService.cs
│ │ ├── ILockProvider.cs
│ │ └── ...
│ ├── AuthService.cs
│ ├── PatientService.cs
│ ├── AdmissionService.cs
│ ├── ObservationService.cs
│ ├── CalculatedObservationsService.cs
│ ├── DefaultCalculatedObservations.cs
│ ├── TreatmentService.cs
│ ├── AlarmService.cs
│ ├── DeviceService.cs
│ ├── CameraService.cs
│ ├── Config*Service.cs
│ ├── DisplayService.cs
│ ├── PointOfCareService.cs
│ ├── AppointmentService.cs
│ ├── PatientCarePlanService.cs
│ ├── Archive*Service.cs
│ ├── Recording*Service.cs
│ ├── MasterListService.cs
│ ├── MasterListServiceFactory.cs
│ ├── PermissionService.cs
│ ├── AdminPanelService.cs
│ ├── FileService.cs
│ ├── LocalAuditService.cs
│ ├── SubscribersService.cs
│ ├── SchedulerService.cs
│ └── Caching/
│ ├── CacheService.cs
│ ├── NoCacheService.cs
│ ├── RedisService.cs
│ ├── CacheDispatcher.cs
│ ├── LockManagerService.cs
│ ├── InMemoryLockProvider.cs
│ └── RedisLockProvider.cs
├── Exceptions/
│ ├── APIRequestException.cs
│ ├── BadRequestException.cs
│ ├── ConflictException.cs
│ ├── NotFoundException.cs
│ ├── UnauthorizedException.cs
│ ├── TokenException.cs
│ ├── UnprocessableEntityException.cs
│ └── ...
├── Customizations/ # Environment-specific calculated-observation rules
│ ├── BD/
│ ├── CHUO/
│ ├── H12O/UCIN/
│ ├── HGM/
│ ├── HPAZ/
│ ├── HRYC/
│ ├── HUVH/UCIN/
│ ├── HUVH/UCIA/
│ └── NursePlan/
│ └── CalculatedObservations.cs
├── Providers/
│ ├── BaseProvider.cs
│ └── AdasProvider.cs
└── Subscriptions/
├── SubscribersService.cs
├── WsSuscriber.cs
└── WsSubscriberGrouped.cs
```
---
## Dependencies
### Downstream References
| Project | Role |
|---------|------|
| `adas-core.Domain` | Domain entities, value objects, and business rules consumed by application services. |
### Upstream References (projects that depend on this)
| Project | Reason |
|---------|--------|
| `adas-core` (Host) | Registers application services and invokes them from controllers. |
| `adas-core.Infrastructure` | Implements all repository contracts defined in `Repositories/Interfaces/`. |
| `adas-core.Authentication` | Uses `IAuthService` and user-related service contracts. |
| `adas-core.module.LightBeacons` | Consumes shared application service interfaces. |
| `adas-core.module.ProxyDevices` | Consumes shared application service interfaces. |
| `adas-core.module.Relays` | Consumes shared application service interfaces. |
| `adas-core.Test` | Mocks application service interfaces in unit tests. |
### NuGet Packages
| Package | Version | Purpose |
|---------|---------|---------|
| `AutoMapper` | 16.1.1 | Entity ↔ projection mapping. |
| `MongoDB.Driver` | 3.9.0 | Repository interface type signatures. |
| `Quartz` | 3.18.1 | Scheduling primitives for background jobs. |
| `StackExchange.Redis` | 2.13.17 | Redis caching abstractions. |
| `Microsoft.Extensions.Caching.Memory` | 10.0.8 | In-memory caching fallback. |
| `Microsoft.Extensions.Http` | 10.0.8 | Typed HTTP clients for provider integrations. |
| `Microsoft.IdentityModel.JsonWebTokens` | 8.18.0 | JWT validation helpers for auth services. |
| `Microsoft.CodeAnalysis.CSharp.Scripting` | 5.3.0 | Dynamic expression evaluation for calculated observations. |
| `AuditLogs` | 1.0.59 | Audit trail tagging in use cases. |
---
## Repository Contracts
All repository interfaces reside in `Repositories/Interfaces/`. They are **contracts only** — no implementation. This enforces the Dependency Inversion Principle: Application defines the interface, Infrastructure provides the concrete MongoDB-backed classes.
### Generic Base Contract
```csharp
public interface IMongoRepository<T> where T : class
{
Task<T?> GetByIdAsync(ObjectId id);
Task<IEnumerable<T>> GetAllAsync();
Task<T> InsertAsync(T entity);
Task UpdateAsync(ObjectId id, T entity);
Task DeleteAsync(ObjectId id);
}
```
### Specialized Contracts
Derived interfaces extend `IMongoRepository<T>` or stand alone for aggregate-specific queries:
- `IPatientRepository` — CRUD + admission/discharge history lookups.
- `IObservationRepository` — Inserts with archive triggers, range queries.
- `IPumpStateRepository` — Scoped lifetime; tracks real-time pump telemetry.
- `IAlarmRepository` — Acknowledge, escalate, and history retrieval.
- `IDisplayConfigRepository` — Display layout and card/chart configuration.
> **Rule:** Every repository method name must describe intent, not mechanism (e.g., `GetActiveByUnitAsync` rather than `FindByQuery`).
---
## Application Services
Services in `Services/` encapsulate complete use cases. They are registered as **Singletons** in the DI container unless they hold per-request state.
### Service Categories
| Category | Examples |
|----------|----------|
| **Patient Management** | `PatientService`, `AdmissionService`, `DischargeService`, `PatientCarePlanService` |
| **Observations** | `ObservationService`, `ObservationDemoService`, `CalculatedObservationsService`, `GroupedObservationService` |
| **Clinical** | `TreatmentService`, `MedicineService`, `DiagnosisService` |
| **Devices** | `DeviceService`, `CameraService`, `PumpService` |
| **Alerts** | `AlarmService`, `AlertValuesService` |
| **Configuration** | `ConfigObservationService`, `ConfigPumpsService`, `ConfigUnitsService`, `ServiceConfigService` |
| **Displays** | `DisplayService`, `DisplayConfigService` |
| **System** | `AuthService`, `PermissionService`, `UnitService`, `MasterListService`, `AdminPanelService` |
| **Archival** | `ArchivePatientObservationsService`, `ArchivedPatientService`, `HistoricalConfigChangesService` |
| **Communication** | `SubscribersService`, `PublisherService`, `ClientMessageService` |
### Calculated Observations
`CalculatedObservationsService` evaluates derived metrics (e.g., early warning scores, trend indicators) using `ICalculatedObservations` strategy pattern. Per-environment overrides are loaded from the `Customizations/` folder based on `ASPNETCORE_ENVIRONMENT`.
Example environment mappings:
| Environment | Customization Path |
|-------------|--------------------|
| `H12O` | `Customizations/H12O/UCIN/CalculatedObservations.cs` |
| `HRYCM` | `Customizations/HRYC/CalculatedObservations.cs` |
| `HPAZ` | `Customizations/HPAZ/CalculatedObservations.cs` |
| `HUVH-UCIN` | `Customizations/HUVH/UCIN/CalculatedObservations.cs` |
| `HUVH-UCIA` | `Customizations/HUVH/UCIA/CalculatedObservations.cs` |
| `CHUO` | `Customizations/CHUO/CalculatedObservations.cs` |
| `NursePlan` | `Customizations/NursePlan/CalculatedObservations.cs` |
---
## Caching Abstractions
The caching stack abstracts Redis and in-memory behind common interfaces so services remain cache-agnostic.
| Component | Responsibility |
|-----------|-------------|
| `ICacheService` | Contract for get, set, remove, and sliding/absolute expiration. |
| `CacheService` | Composite dispatcher that routes to Redis or memory. |
| `RedisService` | Concrete Redis implementation using `StackExchange.Redis`. |
| `NoCacheService` | Null-object pattern for disabling cache in test environments. |
| `ILockProvider` | Distributed or in-memory lock acquisition. |
| `LockManagerService` | Orchestrates lock lifecycle (acquire, extend, release). |
| `RedisLockProvider` | Redis-backed distributed locking. |
| `InMemoryLockProvider` | Lightweight semaphore-based locking for single-instance deployments. |
---
## Exceptions
The `Exceptions/` folder defines a predictable error taxonomy used by controllers to map to HTTP status codes.
| Exception | Mapped HTTP Status | Usage |
|-----------|-------------------|-------|
| `BadRequestException` | `400 Bad Request` | Malformed input, validation failure. |
| `UnauthorizedException` | `401 Unauthorized` | Missing or invalid credentials. |
| `ForbbidenException` | `403 Forbidden` | Authenticated but insufficient permissions. |
| `NotFoundException` | `404 Not Found` | Resource does not exist. |
| `ConflictException` | `409 Conflict` | Concurrent modification or duplicate key. |
| `UnprocessableEntityException` | `422 Unprocessable Entity` | Semantic validation failure. |
| `InvalidFormatException` | `400 Bad Request` | Payload format mismatch. |
| `CustomArgumentException` | `400 Bad Request` | Invalid argument supplied. |
| `APIRequestException` | `502 Bad Gateway` | External provider call failure. |
| `TokenException` | `401 Unauthorized` | JWT parsing or validation error. |
---
## Environment Customizations
`Customizations/` contains per-client overrides for `CalculatedObservations`. These are compiled into the assembly conditionally or selected at runtime based on environment.
This avoids branching domain logic and keeps environment-specific rules isolated:
```
Customizations/
├── BD/
├── CHUO/
├── H12O/UCIN/
├── HGM/
├── HPAZ/
├── HRYC/
├── HUVH/UCIN/
├── HUVH/UCIA/
└── NursePlan/
```
> **Rule:** Customizations may only override `ICalculatedObservations`; they must not introduce new repository calls or bypass service contracts.
---
## Design Rules
1. **No Persistence Logic** — Application services never call MongoDB, Redis, or HTTP clients directly. They use injected repository interfaces and provider abstractions.
2. **Dependency Direction** — This project references **only** `adas-core.Domain`. No references to Infrastructure, Authentication, or Modules.
3. **Contracts First** — All repositories and external providers must expose an interface in this project before Infrastructure implements them.
4. **Single Responsibility Per Service** — One service per bounded-context use case; avoid god services.
5. **Environment Agnosticism** — Core services must compile and run without environment-specific customizations. Overrides are opt-in.
6. **Exception-Driven Flow Control** — Use typed exceptions for expected error paths; never throw raw `Exception` or `ApplicationException`.
7. **Thread Safety** — Singleton services must be stateless or use immutable state. Per-request state lives in scoped repositories.
8. **Cache Invalidation Ownership** — The service that writes data is responsible for invalidating related cache keys.
9. **Lazy Evaluation** — Heavy graph traversals use deferred execution until the repository materializes results.
10. **Audit Trail Awareness** — Sensitive mutations (patient admission, alarm acknowledge, config changes) must tag audit metadata before returning.
---
<p align="center">
Back to <a href="../README.md">adas-core Root README</a>
</p>
@@ -8,35 +8,127 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IAdmissionRepository : IMongoRepository<Admission>
{
/// <summary>
/// Deletes the entity identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the entity to delete.</param>
Task Delete(ObjectId id);
/// <summary>
/// Updates the specified admission record asynchronously.
/// </summary>
/// <param name="admission">The admission entity containing the updated information.</param>
Task Update(Admission admission);
/// <summary>
/// Updates the location of an entity identified by the specified identifier to a new location.
/// </summary>
/// <param name="id">The unique identifier of the entity whose location is to be updated.</param>
/// <param name="newLocation">The identifier of the new location to assign to the entity.</param>
Task UpdateLocation(ObjectId id, ObjectId newLocation);
/// <summary>
/// Updates an existing patient record identified by the specified identifier with the provided patient information.
/// </summary>
/// <param name="id">The unique identifier of the patient to update.</param>
/// <param name="patient">The patient object containing the updated information to apply.</param>
Task UpdatePatient(ObjectId id, Person patient);
/// <summary>
/// Asynchronously retrieves all admission records.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="IEnumerable{Admission}"/> with all available admissions.</returns>
Task<IEnumerable<Admission>> FindAll();
/// <summary>
/// Asynchronously retrieves an <see cref="Admission"/> entity by its unique identifier, returning <c>null</c> when no matching record is found.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the <see cref="Admission"/> to look up.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="Admission"/>, or <c>null</c> if no admission exists with the specified identifier.</returns>
Task<Admission?> FindById(ObjectId id);
/// <summary>
/// Retrieves an admission record that matches the specified NHC (Número de Historia Clínica) identifier, or <c>null</c> when no matching admission exists.
/// </summary>
/// <param name="nhc">The NHC (clinical history number) used to look up the admission.</param>
/// <returns>A task that resolves to the matching <see cref="Admission"/>, or <c>null</c> if no admission is found for the given NHC.</returns>
Task<Admission?> FindByNhc(string nhc);
//Task<IEnumerable<Admission>?> FindByUnit(string unit);
/// <summary>
/// Asynchronously retrieves a list of admissions associated with the specified patient location.
/// </summary>
/// <param name="location">The patient location used to filter and find the relevant admissions.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of admissions matching the given location.</returns>
Task<List<Admission>> FindByLocation(PatientLocation location);
/// <summary>
/// Asynchronously retrieves a collection of admissions filtered by the specified origin.
/// </summary>
/// <param name="origin">The origin value used to filter the admissions.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of matching admissions, or <c>null</c> if no admissions are found for the given origin.</returns>
Task<IEnumerable<Admission>?> FindByOrigin(string origin);
/// <summary>
/// Asynchronously inserts a new <see cref="Admission"/> record into the data store and returns the resulting entity.
/// </summary>
/// <param name="origin">The <see cref="Admission"/> entity to be inserted.</param>
/// <returns>A task that represents the asynchronous insert operation, containing the inserted <see cref="Admission"/>, or <c>null</c> if no result is returned.</returns>
Task<Admission?> InsertOneAsyncAndReturn(Admission origin);
/// <summary>
/// Searches for an admission matching the specified patient number and unit, returning a distinct result.
/// </summary>
/// <param name="patientNumber">The patient's identifier used to locate the admission.</param>
/// <param name="unitId">The unique identifier of the unit to filter the search.</param>
/// <returns>A <see cref="Admission"/> if a matching record is found; otherwise, <c>null</c>.</returns>
Task<Admission?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId);
/// <summary>
/// Asynchronously retrieves a list of admissions associated with the specified unit, excluding Point of Care (PoC) entries.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose admissions are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Admission"/> records linked to the given unit, with PoC entries excluded.</returns>
Task<List<Admission>> GetAdmissionByUnitIdWithOutPoC(ObjectId unitId);
/// <summary>
/// Retrieves a list of admissions associated with the specified point of care identifier.
/// </summary>
/// <param name="pocId">The unique identifier of the point of care used to filter the admissions.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of admissions matching the specified point of care identifier.</returns>
Task<List<Admission>> FindByPointOfCareId(ObjectId pocId);
/// <summary>
/// Asynchronously retrieves a list of admissions associated with the specified unit identifiers.
/// </summary>
/// <param name="unitIds">The list of unit identifiers used to look up the corresponding admissions.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of admissions matching the provided unit identifiers.</returns>
Task<List<Admission>> FindByUnitIds(List<ObjectId> unitIds);
/// <summary>
/// Asynchronously counts the number of records associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The identifier of the unit used to filter the records.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the count of matching records.</returns>
Task<long> CountByUnitId(ObjectId unitId);
/// <summary>
/// Updates the master list option for the specified units based on the provided options and type, returning the affected admissions.
/// </summary>
/// <param name="unitIds">The list of unit identifiers whose master list option should be updated.</param>
/// <param name="opt">The data transfer object containing the new master list option values to apply.</param>
/// <param name="typeName">The name of the option type to be updated.</param>
/// <returns>A task that represents the asynchronous operation, containing the collection of admissions affected by the update.</returns>
Task<IEnumerable<Admission>> UpdateMasterListOption(List<ObjectId> unitIds, UpdateOptionMasterListDto opt,
string typeName);
string typeName);
/// <summary>
/// Deletes the specified option from the master list for the given units and type, returning the affected admissions.
/// </summary>
/// <param name="unitIds">The list of unit identifiers whose master list option should be deleted.</param>
/// <param name="opt">The option to be removed from the master list.</param>
/// <param name="typeName">The name of the type associated with the master list option.</param>
/// <returns>A task that represents the asynchronous operation, containing the collection of <see cref="Admission"/> records affected by the deletion.</returns>
Task<IEnumerable<Admission>> DeleteMasterListOption(List<ObjectId> unitIds, OptionList opt, string typeName);
/// <summary>
/// Asynchronously deletes admissions associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The identifier of the unit whose admissions will be deleted.</param>
/// <returns>A task that represents the asynchronous operation, containing a value indicating the result of the deletion.</returns>
Task<bool> DeleteAdmissionsByUnitId(ObjectId unitId);
}
@@ -7,9 +7,22 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IAlarmRepository : IMongoRepository<PatientObservationAlarm>
{
/// <summary>
/// Retrieves the most recent aggregated patient observations as alarm records, optionally filtered to a specific set of fields.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose last observations should be aggregated.</param>
/// <param name="filterObservations">An optional list of fields used to restrict which observation types are included in the aggregation; when null, all available observations are considered.</param>
/// <returns>A task that resolves to the list of aggregated <see cref="PatientObservationAlarm"/> entries representing the patient's latest observations.</returns>
Task<List<PatientObservationAlarm>> AggregatedPatientLastObservationsByField(ObjectId patientId,
List<Field>? filterObservations = null);
List<Field>? filterObservations = null);
/// <summary>
/// Retrieves aggregated, non-expired patient observation alarms for a specific patient, optionally filtered by a set of fields and evaluated against the supplied configuration alarms.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observation alarms are being retrieved.</param>
/// <param name="filterObservations">An optional list of fields used to restrict which observations are considered. If null, no field-based filtering is applied.</param>
/// <param name="configAlarm">The list of configuration observation definitions used to build and aggregate the resulting alarms.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of aggregated <see cref="PatientObservationAlarm"/> entries for the patient.</returns>
Task<List<PatientObservationAlarm>> AggregatedPatientNotExpiredObservationsByField(ObjectId patientId,
List<Field>? filterObservations, List<ConfigObservation> configAlarm);
List<Field>? filterObservations, List<ConfigObservation> configAlarm);
}
@@ -4,7 +4,20 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IAppointmentArchiveRepository
{
/// <summary>
/// Asynchronously inserts a single patient appointment into the underlying data store.
/// </summary>
/// <param name="patientAppointment">The patient appointment entity to be persisted.</param>
Task InsertOneAsync(PatientAppointment patientAppointment);
/// <summary>
/// Asynchronously deletes records that have a date earlier than the specified cutoff date.
/// </summary>
/// <param name="date">The cutoff date; records dated before this value will be removed.</param>
Task DeleteBeforeDate(DateTime date);
/// <summary>
/// Inserts a batch of patient appointments asynchronously, typically as part of a bulk import or synchronization process.
/// </summary>
/// <param name="appointment">The collection of patient appointments to be inserted.</param>
/// <returns>A task that represents the asynchronous insert operation, containing a long value that represents the result of the batch insertion.</returns>
Task<long> InsertBatch(IEnumerable<PatientAppointment> appointment);
}
@@ -7,16 +7,71 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IAppointmentRepository : IMongoRepository<PatientAppointment>
{
/// <summary>
/// Asynchronously retrieves the list of patient appointments associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose appointments are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientAppointment"/> objects for the specified patient.</returns>
Task<List<PatientAppointment>> GetByPatient(ObjectId patientId);
/// <summary>
/// Asynchronously inserts a single patient appointment record into the data store.
/// </summary>
/// <param name="appointment">The patient appointment entity to be inserted.</param>
new Task InsertOneAsync(PatientAppointment appointment);
/// <summary>
/// Updates an existing patient appointment asynchronously.
/// </summary>
/// <param name="appointment">The patient appointment containing the updated information.</param>
Task Update(PatientAppointment appointment);
/// <summary>
/// Updates the <see cref="ObjectId"/> field identified by <paramref name="nameId"/> across many records, replacing the existing value <paramref name="oldId"/> with the new value <paramref name="id"/>.
/// </summary>
/// <param name="nameId">The name of the field that contains the <see cref="ObjectId"/> to update.</param>
/// <param name="id">The new <see cref="ObjectId"/> value to assign to matching records.</param>
/// <param name="oldId">The current <see cref="ObjectId"/> value to be replaced.</param>
Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId);
/// <summary>
/// Asynchronously deletes the entity identified by the specified identifier.
/// </summary>
/// <param name="id">The ObjectId of the entity to delete.</param>
new Task DeleteAsync(ObjectId id);
/// <summary>
/// Asynchronously retrieves a cursor of <see cref="PatientAppointment"/> records associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique <see cref="ObjectId"/> of the patient whose appointments are being queried.</param>
/// <returns>A <see cref="Task{IAsyncCursor{PatientAppointment}}"/> that yields the matching patient appointments; the cursor may be empty if no appointments are found for the given patient.</returns>
Task<IAsyncCursor<PatientAppointment>> FindByPatientIdAsync(ObjectId patientId);
/// <summary>
/// Asynchronously deletes records associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique ObjectId of the patient whose related records should be removed.</param>
Task DeleteByPatientId(ObjectId patientId);
/// <summary>
/// Asynchronously retrieves a <see cref="PatientAppointment"/> matching the specified patient identifier and visit number.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose appointment should be located.</param>
/// <param name="visitNumber">The visit number used to identify the specific appointment for the patient.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="PatientAppointment"/> if found; otherwise, <c>null</c>.</returns>
Task<PatientAppointment?> FindByPatientAndVisitNumber(ObjectId patientId, string visitNumber);
/// <summary>
/// Asynchronously retrieves a patient appointment matching the specified patient identifier and appointment reason.
/// Returns <c>null</c> when no matching appointment is found.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose appointment should be located.</param>
/// <param name="appointmentReason">The appointment reason used to filter the lookup, or <c>null</c> to match any reason.</param>
/// <returns>A <see cref="Task{PatientAppointment}"/> that resolves to the matching <see cref="PatientAppointment"/>, or <c>null</c> if none exists.</returns>
Task<PatientAppointment?> FindByPatientAndReason(ObjectId patientId, string? appointmentReason);
/// <summary>
/// Retrieves a list of patient appointments associated with the specified location.
/// </summary>
/// <param name="location">The patient location used to filter and retrieve matching appointments.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientAppointment"/> entries for the given location.</returns>
Task<List<PatientAppointment>> FindByLocation(PatientLocation location);
/// <summary>
/// Asynchronously retrieves the list of patient appointments associated with the specified point of care.
/// </summary>
/// <param name="poc">The point of care used to filter the patient appointments.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of patient appointments for the given point of care.</returns>
Task<List<PatientAppointment>> FindByPoC(PointOfCare poc);
}
@@ -5,13 +5,43 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IArchivePatientCarePlanRepository : IMongoRepository<PatientCarePlan>
{
/// <summary>
/// Retrieves the list of care plans associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose care plans should be retrieved.</param>
/// <returns>A task that returns a list of <see cref="PatientCarePlan"/> objects for the given patient, or <c>null</c> when no care plans are found.</returns>
Task<List<PatientCarePlan>?> FindByPatientId(ObjectId patientId);
/// <summary>
/// Retrieves the list of patient care plans associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose care plans are being queried.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="PatientCarePlan"/> for the given patient, or <c>null</c> if no care plans are found.</returns>
Task<List<PatientCarePlan>?> FindByPatientId(string patientId);
/// <summary>
/// Asynchronously retrieves the list of care plans associated with the specified patient number, returning <c>null</c> when no matching care plans are found.
/// </summary>
/// <param name="patientId">The patient number used to look up the associated care plans.</param>
/// <returns>A task representing the asynchronous operation, containing a list of <see cref="PatientCarePlan"/> entries for the patient, or <c>null</c> if none exist.</returns>
Task<List<PatientCarePlan>?> FindByPatientNumber(string patientId);
/// <summary>
/// Asynchronously retrieves all patient care plans from the data store.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of all <see cref="PatientCarePlan"/> entries.</returns>
Task<List<PatientCarePlan>> FindAll();
/// <summary>
/// Asynchronously retrieves a list of patient care plans matching the provided legacy patient identifiers.
/// </summary>
/// <param name="oldPatientId">The legacy <see cref="ObjectId"/> identifier used to locate the patient.</param>
/// <param name="oldPatientPatientId">An optional legacy patient ID string used as an additional lookup criterion.</param>
/// <param name="oldPatientPatientNumber">An optional legacy patient number string used as an additional lookup criterion.</param>
/// <returns>A <see cref="Task{TResult}"/> containing a nullable list of <see cref="PatientCarePlan"/> records, or <c>null</c> if no matching care plans are found.</returns>
Task<List<PatientCarePlan>?> FindByIds(ObjectId oldPatientId, string? oldPatientPatientId,
string? oldPatientPatientNumber);
string? oldPatientPatientNumber);
/// <summary>
/// Asynchronously inserts a collection of patient care plans into the underlying data store.
/// </summary>
/// <param name="patientCarePla">The list of <see cref="PatientCarePlan"/> records to be inserted.</param>
Task InsertManyAsync(List<PatientCarePlan> patientCarePla);
}
@@ -5,12 +5,51 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IAuthorityRepository : IMongoRepository<Authorization>
{
/// <summary>
/// Retrieves an <see cref="Authorization"/> entity by its unique identifier asynchronously.
/// </summary>
/// <param name="authId">The unique identifier of the authorization to retrieve.</param>
/// <returns>A task that represents the asynchronous operation, containing the <see cref="Authorization"/> that matches the specified identifier.</returns>
public Task<Authorization> GetById(ObjectId authId);
/// <summary>
/// Creates a new authority assignment for the specified user with the given role name.
/// </summary>
/// <param name="roleName">The name of the role to assign as the new authority.</param>
/// <param name="userId">The identifier of the user to whom the authority will be assigned.</param>
void CreateNewAuthority(string roleName, ObjectId userId);
/// <summary>
/// Retrieves the list of authorizations (authorities/permissions) associated with the specified user.
/// </summary>
/// <param name="userId">The unique identifier of the user whose authorities are being requested.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the list of <see cref="Authorization"/> entries assigned to the user.</returns>
public Task<List<Authorization>> GetUserAuthorities(ObjectId userId);
/// <summary>
/// Asynchronously retrieves all available authorizations.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of all <see cref="Authorization"/> objects.</returns>
public Task<List<Authorization>> GetAllAuthorities();
/// <summary>
/// Asynchronously deletes all authorities associated with the specified user.
/// </summary>
/// <param name="userId">The unique identifier of the user whose authorities will be removed.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if authorities were deleted; otherwise, <c>false</c>.</returns>
public Task<bool> DeleteAllAuthoritiesByUser(ObjectId userId);
/// <summary>
/// Deletes all authorities associated with the specified unit.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose authorities should be removed.</param>
/// <returns>A task that resolves to <c>true</c> if the authorities were successfully deleted; otherwise, <c>false</c>.</returns>
Task<bool> DeleteAllAuthoritiesByUnit(ObjectId unitId);
/// <summary>
/// Deletes all authorities associated with the specified display identifier.
/// </summary>
/// <param name="displayId">The identifier of the display whose related authorities should be removed.</param>
/// <returns>A task that resolves to a boolean indicating the outcome of the deletion operation.</returns>
Task<bool> DeleteAllAuthoritiesByDisplay(ObjectId displayId);
/// <summary>
/// Retrieves a list of authorizations associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose authorizations are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Authorization"/> objects matching the provided unit identifier.</returns>
Task<List<Authorization>> GetByUnitId(ObjectId unitId);
}
@@ -7,11 +7,47 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface ICameraRepository : IMongoRepository<Camera>
{
/// <summary>
/// Asynchronously retrieves a <see cref="Camera"/> by its unique identifier.
/// </summary>
/// <param name="cameraId">The unique identifier of the camera to look up.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="Camera"/>, or <c>null</c> if no camera is found with the specified identifier.</returns>
Task<Camera?> GetById(ObjectId cameraId);
/// <summary>
/// Asynchronously retrieves a camera by its name, returning <c>null</c> if no matching camera is found.
/// </summary>
/// <param name="name">The name of the camera to look up.</param>
/// <returns>A <see cref="Task{TResult}"/> that resolves to the matching <see cref="Camera"/>, or <c>null</c> if no camera with the specified name exists.</returns>
Task<Camera?> GetByName(string name);
/// <summary>
/// Retrieves the list of <see cref="Camera"/> objects associated with the specified configuration relays.
/// </summary>
/// <param name="configurationRelayList">The list of <see cref="ObjectId"/> values identifying the configuration relays whose cameras should be returned.</param>
/// <returns>A <see cref="List{Camera}"/> containing the cameras linked to the provided configuration relays; returns an empty list when no matching cameras are found.</returns>
List<Camera> GetCameraInList(List<ObjectId> configurationRelayList);
/// <summary>
/// Retrieves a paginated, fluent queryable collection of cameras based on the supplied pagination filter.
/// </summary>
/// <param name="request">The pagination filter containing the page number and page size used to page the camera results.</param>
/// <returns>An <see cref="IFindFluent{Camera, Camera}"/> representing the paged query of cameras.</returns>
IFindFluent<Camera, Camera> GetPaginatedCameras(PaginationFilter request);
/// <summary>
/// Inserts a new camera record into the data store and returns the persisted entity, or <c>null</c> if the camera could not be inserted.
/// </summary>
/// <param name="camera">The <see cref="Camera"/> instance containing the data to be inserted.</param>
/// <returns>A <see cref="Task{T}"/> that resolves to the inserted <see cref="Camera"/>, or <c>null</c> when the insertion is not performed.</returns>
Task<Camera?> InsertOneCamera(Camera camera);
/// <summary>
/// Asynchronously updates an existing camera identified by the specified object identifier.
/// </summary>
/// <param name="objectId">The unique identifier of the camera to update.</param>
/// <param name="camera">The camera object containing the updated information.</param>
/// <returns>A task that represents the asynchronous update operation. The task result contains the updated <see cref="Camera"/>, or <c>null</c> if no camera with the specified identifier was found.</returns>
Task<Camera?> UpdateCameraAsync(ObjectId objectId, Camera camera);
/// <summary>
/// Asynchronously searches for cameras whose name matches the specified text.
/// </summary>
/// <param name="textToSearch">The text used to filter cameras by name.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Camera"/> objects matching the search criteria.</returns>
Task<List<Camera>> GetSearchByNameCameras(string textToSearch);
}
@@ -6,25 +6,96 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IConfigObservationRepository : IMongoRepository<ConfigObservation>
{
/// <summary>
/// Asynchronously retrieves a configuration observation by its unique identifier, returning <c>null</c> when no matching observation exists.
/// </summary>
/// <param name="id">The unique identifier of the configuration observation to locate.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="ConfigObservation"/> if found, or <c>null</c> if no observation matches the specified identifier.</returns>
Task<ConfigObservation?> FindById(ObjectId id);
/// <summary>
/// Updates an existing configuration observation in the underlying store.
/// Returns the updated observation, or <c>null</c> when no matching observation is found.
/// </summary>
/// <param name="configObservation">The configuration observation containing the updated values to persist.</param>
/// <returns>A task that resolves to the updated <see cref="ConfigObservation"/>, or <c>null</c> if the observation does not exist.</returns>
Task<ConfigObservation?> Update(ConfigObservation configObservation);
/// <summary>
/// Deletes the configuration observation identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the configuration observation to delete.</param>
/// <returns>A task that represents the asynchronous delete operation. The task result contains the deleted configuration observation, or <c>null</c> if no observation with the specified identifier was found.</returns>
Task<ConfigObservation?> Delete(ObjectId id);
/// <summary>
/// Asynchronously retrieves all available identifiers, returning them as a list of <see cref="ObjectId"/> values.
/// </summary>
/// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous operation, containing a list of all <see cref="ObjectId"/> instances found.</returns>
Task<List<ObjectId>> FindAllIds();
/// <summary>
/// Retrieves all configuration observations available in the system.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of <see cref="ConfigObservation"/> instances.</returns>
Task<ICollection<ConfigObservation>> FindAll();
/// <summary>
/// Asynchronously retrieves a list of configuration names associated with the specified identifier.
/// </summary>
/// <param name="id">The identifier used to look up the configuration names.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of configuration names.</returns>
Task<List<string>> GetConfigNames(string id);
/// <summary>
/// Asynchronously retrieves the list of available configuration names.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing the list of configuration names.</returns>
Task<List<string>> GetConfigNames();
/// <summary>
/// Asynchronously retrieves the total count of items.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing the total count as a <see cref="long"/>.</returns>
Task<long> Count();
/// <summary>
/// Retrieves a paginated collection of configuration observations based on the specified filter.
/// </summary>
/// <param name="filter">The pagination filter that defines the page size, page number, and any additional criteria used to retrieve the observations.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of <see cref="ConfigObservation"/> items that match the pagination criteria.</returns>
Task<ICollection<ConfigObservation>> GetPaginatedItems(PaginationFilter filter);
/// <summary>
/// Retrieves a configuration observation that matches the specified name.
/// </summary>
/// <param name="name">The name of the configuration observation to find.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching ConfigObservation if found; otherwise, null.</returns>
Task<ConfigObservation?> FindByName(string name);
/// <summary>
/// Retrieves a <see cref="ConfigObservation"/> matching the specified coding system and code.
/// </summary>
/// <param name="codingSystem">The coding system identifier used to look up the configuration observation.</param>
/// <param name="code">The code value within the specified coding system used to look up the configuration observation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="ConfigObservation"/> if found; otherwise, <c>null</c>.</returns>
Task<ConfigObservation?> GetByCodeSysAndCode(string? codingSystem, string? code);
/// <summary>
/// Asynchronously inserts the specified configuration observation item and returns the persisted entity.
/// </summary>
/// <param name="configObservationItem">The configuration observation to insert.</param>
/// <returns>A task that represents the asynchronous insert operation, containing the inserted <see cref="ConfigObservation"/>.</returns>
Task<ConfigObservation> InsertOneAsyncAndReturn(ConfigObservation configObservationItem);
/// <summary>
/// Asynchronously retrieves all <see cref="ConfigObservation"/> records matching the specified name.
/// </summary>
/// <param name="name">The name used to look up the configuration observations.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="ConfigObservation"/> objects matching the given name, or an empty list if none are found.</returns>
Task<List<ConfigObservation>> FindAllByName(string name);
/// <summary>
/// Retrieves a single <see cref="ConfigObservation"/> item matching the provided criteria, or <see langword="null"/> if no matching item is found.
/// </summary>
/// <param name="code">The code used to look up the configuration observation item, or <see langword="null"/> if not specified.</param>
/// <param name="codingSystem">The coding system associated with the code, or <see langword="null"/> if not specified.</param>
/// <param name="name">The name used to look up the configuration observation item, or <see langword="null"/> if not specified.</param>
/// <param name="originalName">The original name used to look up the configuration observation item, or <see langword="null"/> if not specified.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="ConfigObservation"/> item, or <see langword="null"/> if no item matches the specified criteria.</returns>
Task<ConfigObservation?> GetSingleConfigObservationItem(string? code, string? codingSystem, string? name,
string? originalName);
string? originalName);
}
@@ -4,9 +4,29 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IConfigPumpsRepository : IMongoRepository<ConfigPumps>
{
/// <summary>
/// Asynchronously retrieves a ConfigPumps configuration by its unique identifier.
/// Returns null when no matching configuration is found.
/// </summary>
/// <param name="id">The unique identifier of the configuration to look up.</param>
/// <returns>A task that yields the matching ConfigPumps instance, or null if no configuration is found for the specified id.</returns>
Task<ConfigPumps?> FindById(string id);
/// <summary>
/// Retrieves all pump configurations asynchronously.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="ConfigPumps"/> if found, or <c>null</c> if no configurations are available.</returns>
Task<List<ConfigPumps>?> GetAllConfigs();
/// <summary>
/// Updates the pumps configuration using the supplied <paramref name="config"/> instance.
/// </summary>
/// <param name="config">The pumps configuration to be persisted.</param>
/// <returns>A task that resolves to the updated <see cref="ConfigPumps"/> instance, or <see langword="null"/> if the configuration could not be found.</returns>
Task<ConfigPumps?> UpdateConfig(ConfigPumps config);
/// <summary>
/// Asynchronously deletes the specified pump configuration.
/// </summary>
/// <param name="config">The pump configuration to delete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains <c>true</c> if the configuration was successfully deleted; otherwise, <c>false</c>.</returns>
Task<bool> DeleteConfig(ConfigPumps config);
}
@@ -4,5 +4,11 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IConfigUnitsRepository : IMongoRepository<ConfigUnits>
{
/// <summary>
/// Asynchronously retrieves a <see cref="ConfigUnits"/> entity by its unique identifier.
/// Returns <c>null</c> when no matching configuration unit is found.
/// </summary>
/// <param name="id">The unique identifier of the configuration unit to look up.</param>
/// <returns>A task that represents the asynchronous operation, containing the matching <see cref="ConfigUnits"/> or <c>null</c> if no entity is found.</returns>
Task<ConfigUnits?> FindById(string id);
}
@@ -6,9 +6,35 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IDeviceRepository : IMongoRepository<Device>
{
/// <summary>
/// Asynchronously retrieves a device matching the specified MAC address, returning null if no matching device is found.
/// </summary>
/// <param name="deviceDtoMacAddr">The MAC address used to look up the device.</param>
/// <returns>A task that represents the asynchronous operation, containing the matching <see cref="Device"/> or null when no device is found.</returns>
Task<Device?> FindByMacAddr(string deviceDtoMacAddr);
/// <summary>
/// Finds and returns the device matching the specified serial number, or <c>null</c> if no matching device is found.
/// </summary>
/// <param name="deviceDtoSerialNumber">The serial number used to look up the device.</param>
/// <returns>A <see cref="Device"/> instance if a match is found; otherwise, <c>null</c>.</returns>
Task<Device?> FindBySerialNumber(string deviceDtoSerialNumber);
/// <summary>
/// Asynchronously finds a device by its unique identifier (UUID) and returns the matching device, or null if no device is found.
/// </summary>
/// <param name="deviceDtoUuid">The UUID string used to look up the device.</param>
/// <returns>A <see cref="Task{Device}"/> that resolves to the matching <see cref="Device"/>, or null when no device matches the provided UUID.</returns>
Task<Device?> FindByUuid(string deviceDtoUuid);
/// <summary>
/// Retrieves a device by its unique key identifier.
/// </summary>
/// <param name="deviceDtoKey">The key identifier of the device to look up.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="Device"/> if a matching device is found, or <c>null</c> if no device corresponds to the specified key.</returns>
Task<Device?> FindByKey(string deviceDtoKey);
/// <summary>
/// Updates the statistics of an existing device identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the device whose statistics will be updated.</param>
/// <param name="deviceExist">The device data transfer object representing the existing device to be updated.</param>
/// <returns>A task that represents the asynchronous update operation.</returns>
Task UpdateDeviceStats(ObjectId id,DeviceDto deviceExist);
}
@@ -4,7 +4,21 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IDiagnosisArchiveRepository
{
/// <summary>
/// Asynchronously inserts a new patient diagnosis record into the data store.
/// </summary>
/// <param name="patientDiagnosis">The patient diagnosis entity to be persisted.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
Task InsertOneAsync(PatientDiagnosis patientDiagnosis);
/// <summary>
/// Asynchronously deletes items that occurred before the specified cutoff date.
/// </summary>
/// <param name="date">The cutoff date; items dated prior to this value will be deleted.</param>
Task DeleteBeforeDate(DateTime date);
/// <summary>
/// Inserts a batch of patient diagnosis records into the data store.
/// </summary>
/// <param name="diagnosis">The collection of <see cref="PatientDiagnosis"/> entities to insert.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the asynchronous operation, containing the result of the batch insertion.</returns>
Task<long> InsertBatch(IEnumerable<PatientDiagnosis> diagnosis);
}
@@ -6,12 +6,48 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IDiagnosisRepository : IMongoRepository<PatientDiagnosis>
{
/// <summary>
/// Asynchronously retrieves a list of patient diagnoses associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose diagnoses are to be retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientDiagnosis"/> objects for the specified patient.</returns>
Task<List<PatientDiagnosis>> GetByPatient(ObjectId patientId);
/// <summary>
/// Asynchronously inserts a single patient diagnosis record into the data store.
/// </summary>
/// <param name="diagnosis">The patient diagnosis entity to insert.</param>
new Task InsertOneAsync(PatientDiagnosis diagnosis);
/// <summary>
/// Asynchronously deletes the entity identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the entity to delete.</param>
new Task DeleteAsync(ObjectId id);
/// <summary>
/// Updates many records by replacing the existing <see cref="ObjectId"/> identified by <paramref name="oldId"/> with the new <see cref="ObjectId"/> <paramref name="id"/>, scoped to the specified <paramref name="nameId"/>.
/// </summary>
/// <param name="nameId">The identifier of the field or collection on which the update is performed.</param>
/// <param name="id">The new <see cref="ObjectId"/> value to assign.</param>
/// <param name="oldId">The existing <see cref="ObjectId"/> value to be replaced.</param>
Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId);
/// <summary>
/// Asynchronously finds all patient diagnoses associated with the specified patient identifier, returning a cursor to iterate over the matching documents.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose diagnoses should be retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing an IAsyncCursor of PatientDiagnosis that yields the matching documents.</returns>
Task<IAsyncCursor<PatientDiagnosis>> FindByPatientIdAsync(ObjectId patientId);
/// <summary>
/// Asynchronously deletes records associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique <see cref="ObjectId"/> of the patient whose related records should be removed.</param>
Task DeleteByPatientId(ObjectId patientId);
/// <summary>
/// Asynchronously retrieves the <see cref="PatientDiagnosis"/> associated with the specified patient, diagnosis code, and coding system.
/// Returns null when no matching diagnosis is found.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose diagnosis is being looked up.</param>
/// <param name="diagnosisCode">The diagnosis code to match. May be null.</param>
/// <param name="codingSystem">The coding system of the diagnosis code (for example, ICD-10 or SNOMED). May be null.</param>
/// <returns>A task that resolves to the matching <see cref="PatientDiagnosis"/>, or null if no diagnosis matches the given criteria.</returns>
Task<PatientDiagnosis?> FindByPatientIdAndCode(ObjectId patientId, string? diagnosisCode, string? codingSystem);
}
@@ -8,37 +8,129 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IDischargeRepository : IMongoRepository<Discharge>
{
/// <summary>
/// Deletes the entity identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the entity to delete.</param>
Task Delete(ObjectId id);
/// <summary>
/// Asynchronously updates an existing discharge record.
/// </summary>
/// <param name="discharge">The discharge entity containing the updated information to be persisted.</param>
Task Update(Discharge discharge);
/// <summary>
/// Asynchronously updates the unit associated with the specified object identifier.
/// </summary>
/// <param name="id">The unique identifier of the object whose unit will be updated.</param>
/// <param name="unit">The new unit value to assign to the object.</param>
Task UpdateUnit(ObjectId id, string unit);
/// <summary>
/// Updates an existing patient record identified by the specified identifier with the provided patient data.
/// </summary>
/// <param name="id">The unique identifier of the patient to update.</param>
/// <param name="patient">The patient object containing the updated information to be applied to the existing record.</param>
Task UpdatePatient(ObjectId id, Patient patient);
/// <summary>
/// Asynchronously retrieves all discharge records.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of all <see cref="Discharge"/> entities.</returns>
Task<IEnumerable<Discharge>> FindAll();
/// <summary>
/// Retrieves a <see cref="Discharge"/> record by its unique identifier.
/// </summary>
/// <param name="id">The unique identifier of the discharge record to find.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="Discharge"/> if a matching record is found; otherwise, <c>null</c>.</returns>
Task<Discharge?> FindById(ObjectId id);
/// <summary>
/// Asynchronously retrieves a collection of <see cref="Discharge"/> records associated with the specified unit, or <see langword="null"/> if no matching records are found.
/// </summary>
/// <param name="unit">The unit identifier used to filter the discharge records.</param>
/// <returns>A task that represents the asynchronous operation. The result is an <see cref="IEnumerable{T}"/> of <see cref="Discharge"/> containing the matching records, or <see langword="null"/> when no records are found.</returns>
Task<IEnumerable<Discharge>?> FindByUnit(string unit);
/// <summary>
/// Asynchronously counts the number of records associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The ObjectId of the unit whose associated records will be counted.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the count of records for the specified unit.</returns>
Task<long> CountByUnitId(ObjectId unitId);
/// <summary>
/// Asynchronously retrieves the collection of discharge records associated with the specified destination.
/// </summary>
/// <param name="destination">The destination identifier used to look up matching discharge records.</param>
/// <returns>A task that yields an <see cref="IEnumerable{Discharge}"/> of matching discharge records, or <c>null</c> if no records are found for the given destination.</returns>
Task<IEnumerable<Discharge>?> FindByDestination(string destination);
/// <summary>
/// Asynchronously retrieves a collection of discharges associated with the specified service.
/// Returns null when no matching discharges are found for the given service.
/// </summary>
/// <param name="service">The service identifier used to look up matching discharge records.</param>
/// <returns>A task that represents the asynchronous operation. The result is an <see cref="IEnumerable{T}"/> of <see cref="Discharge"/> entries for the specified service, or <c>null</c> if none are found.</returns>
Task<IEnumerable<Discharge>?> FindByService(string service);
/// <summary>
/// Asynchronously retrieves a collection of <see cref="Discharge"/> records associated with the specified unit identifiers.
/// </summary>
/// <param name="unitIds">The collection of unit identifiers used to look up the matching discharges. May be <c>null</c>.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of <see cref="Discharge"/> objects, or <c>null</c> when no matching discharges are available.</returns>
Task<IEnumerable<Discharge>?> GetDischargesByUnitIds(IEnumerable<ObjectId>? unitIds);
/// <summary>
/// Asynchronously retrieves the discharge record associated with the specified patient location, returning null if no discharge is found for that location.
/// </summary>
/// <param name="location">The patient location used to look up the associated discharge record.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="Discharge"/> if found; otherwise, <c>null</c>.</returns>
Task<Discharge?> GetDischargeByLocation(PatientLocation location);
/// <summary>
/// Retrieves the discharge record associated with the specified patient identifier, or <see langword="null"/> if no discharge exists for that patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose discharge record is being requested.</param>
/// <returns>A <see cref="Task{Discharge}"/> that resolves to the matching <see cref="Discharge"/>, or <see langword="null"/> when no record is found.</returns>
Task<Discharge?> GetByPatientId(ObjectId patientId);
/// <summary>
/// Asynchronously retrieves the collection of <see cref="Discharge"/> records associated with the specified point-of-care identifier.
/// </summary>
/// <param name="pocId">The point-of-care identifier used to look up the associated discharges.</param>
/// <returns>A task that returns an <see cref="IEnumerable{T}"/> of <see cref="Discharge"/> records, or <see langword="null"/> if no discharges are found for the given point-of-care identifier.</returns>
Task<IEnumerable<Discharge>?> FindByPoCId(ObjectId pocId);
/// <summary>
/// Retrieves the discharge associated with the specified point of care identifier.
/// </summary>
/// <param name="poc">The identifier of the point of care used to look up the discharge.</param>
/// <returns>A task that returns the matching <see cref="Discharge"/> if one is found; otherwise, <c>null</c>.</returns>
Task<Discharge?> GetDischargeByPointOfCareId(ObjectId poc);
/// <summary>
/// Updates the master list option for the specified units based on the provided update details and type name.
/// </summary>
/// <param name="unitIds">The list of unit identifiers to be updated.</param>
/// <param name="opt">The update option master list data transfer object containing the new option details.</param>
/// <param name="typeName">The name of the type to which the master list option applies.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of updated Discharge records.</returns>
Task<IEnumerable<Discharge>> UpdateMasterListOption(List<ObjectId> unitIds, UpdateOptionMasterListDto opt,
string typeName);
string typeName);
/// <summary>
/// Deletes the specified master list option from the given units and returns the resulting discharges.
/// </summary>
/// <param name="unitIds">The identifiers of the units from which the option will be removed.</param>
/// <param name="opt">The master list option to delete.</param>
/// <param name="typeName">The type name associated with the option.</param>
/// <returns>A task that represents the asynchronous operation, containing the collection of <see cref="Discharge"/> objects resulting from the deletion.</returns>
Task<IEnumerable<Discharge>> DeleteMasterListOption(List<ObjectId> unitIds, OptionList opt, string typeName);
/// <summary>
/// Deletes the entity associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose associated record should be removed.</param>
/// <returns>A task that represents the asynchronous operation. The result is <c>true</c> if a record was deleted; otherwise, <c>false</c>.</returns>
Task<bool> DeleteByUnitId(ObjectId unitId);
}
@@ -6,9 +6,33 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IDisplayCardConfigRepository : IMongoRepository<CardConfig>
{
/// <summary>
/// Asynchronously retrieves all available card configurations.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of all <see cref="CardConfig"/> items.</returns>
Task<List<CardConfig>> GetAll();
/// <summary>
/// Retrieves a card configuration by its unique identifier, returning <c>null</c> when no matching configuration is found.
/// </summary>
/// <param name="configId">The unique identifier of the card configuration to look up.</param>
/// <returns>A <see cref="Task{TResult}"/> that yields the matching <see cref="CardConfig"/> if found; otherwise, <c>null</c>.</returns>
Task<CardConfig?> GetById(ObjectId configId);
/// <summary>
/// Asynchronously inserts a new <see cref="CardConfig"/> into the data store and returns the persisted record, which may include database-generated values.
/// </summary>
/// <param name="config">The <see cref="CardConfig"/> instance to insert.</param>
/// <returns>A <see cref="Task{TResult}"/> that yields the inserted <see cref="CardConfig"/>, or <c>null</c> if the record could not be persisted.</returns>
Task<CardConfig?> InsertOneAsyncAndReturn(CardConfig config);
/// <summary>
/// Updates a single <see cref="CardConfig"/> record based on the provided configuration.
/// </summary>
/// <param name="config">The <see cref="CardConfig"/> to update. May be <c>null</c> to indicate no update payload was provided.</param>
/// <returns>A task that resolves to an <see cref="UpdateResponse{T}"/> containing the updated <see cref="CardConfig"/>, or <c>null</c> if no matching record was found.</returns>
Task<UpdateResponse<CardConfig?>> UpdateOne(CardConfig? config);
/// <summary>
/// Deletes a single card configuration identified by the specified identifier, returning the removed configuration when found.
/// </summary>
/// <param name="configId">The unique identifier of the card configuration to delete.</param>
/// <returns>A task that represents the asynchronous delete operation. The task result contains the deleted <see cref="CardConfig"/>, or <c>null</c> if no configuration with the specified identifier exists.</returns>
Task<CardConfig?> DeleteOne(ObjectId configId);
}
@@ -6,9 +6,33 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IDisplayChartConfigRepository : IMongoRepository<ChartConfig>
{
/// <summary>
/// Asynchronously retrieves all chart configurations.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="ChartConfig"/> objects.</returns>
Task<List<ChartConfig>> GetAll();
/// <summary>
/// Asynchronously retrieves a <see cref="ChartConfig"/> by its unique identifier, returning null if no matching configuration is found.
/// </summary>
/// <param name="configId">The identifier of the chart configuration to look up.</param>
/// <returns>A task that yields the matching <see cref="ChartConfig"/>, or null when no configuration exists for the specified id.</returns>
Task<ChartConfig?> GetById(ObjectId configId);
/// <summary>
/// Asynchronously inserts a new chart configuration into the data store and returns the persisted entity, which may be null if no record is produced.
/// </summary>
/// <param name="config">The chart configuration to insert.</param>
/// <returns>A task that represents the asynchronous insert operation, containing the inserted <see cref="ChartConfig"/> or null when the operation yields no result.</returns>
Task<ChartConfig?> InsertOneAsyncAndReturn(ChartConfig config);
/// <summary>
/// Updates a single chart configuration and returns the operation result wrapped in an update response.
/// </summary>
/// <param name="config">The chart configuration to update. May be null.</param>
/// <returns>A task containing the update response with the updated chart configuration, which may be null.</returns>
Task<UpdateResponse<ChartConfig?>> UpdateOne(ChartConfig? config);
/// <summary>
/// Deletes a single chart configuration identified by its unique identifier. Returns the deleted configuration, or <see langword="null"/> if no matching configuration was found.
/// </summary>
/// <param name="configId">The unique identifier of the chart configuration to delete.</param>
/// <returns>A task that resolves to the deleted <see cref="ChartConfig"/>, or <see langword="null"/> if no configuration with the specified id exists.</returns>
Task<ChartConfig?> DeleteOne(ObjectId configId);
}
@@ -11,32 +11,171 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IDisplayConfigRepository : IMongoRepository<DisplayConfig>
{
/// <summary>
/// Asynchronously retrieves all display configurations.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of all <see cref="DisplayConfig"/> entries.</returns>
Task<List<DisplayConfig>> GetAll();
/// <summary>
/// Retrieves a paginated set of <see cref="DisplayConfig"/> entities projected as <see cref="DisplayConfigSummary"/>.
/// </summary>
/// <param name="request">The pagination filter applied to the query.</param>
/// <returns>An <see cref="IFindFluent{TDocument, TProjection}"/> that produces the paginated <see cref="DisplayConfigSummary"/> results.</returns>
IFindFluent<DisplayConfig, DisplayConfigSummary> GetAllPaginated(PaginationFilter request);
/// <summary>
/// Asynchronously retrieves the list of display configurations that match the specified display type.
/// </summary>
/// <param name="type">The display type used to filter the returned display configurations.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of <see cref="DisplayConfig"/> entries matching the specified type.</returns>
Task<List<DisplayConfig>> GetByType(DisplayConfigEnums.DisplayType type);
/// <summary>
/// Retrieves a <see cref="DisplayConfig"/> entity by its unique identifier asynchronously.
/// Returns <c>null</c> when no matching display configuration is found.
/// </summary>
/// <param name="id">The unique identifier of the display configuration to retrieve.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="DisplayConfig"/> if found, or <c>null</c> if no matching record exists.</returns>
Task<DisplayConfig?> GetById(ObjectId id);
/// <summary>
/// Asynchronously retrieves the default <see cref="DisplayConfig"/> for the specified display type.
/// Returns <c>null</c> when no default configuration is found for the requested type.
/// </summary>
/// <param name="type">The display type used to look up the default configuration.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the default <see cref="DisplayConfig"/> for the given type, or <c>null</c> if no default exists.</returns>
Task<DisplayConfig?> GetDefault(DisplayConfigEnums.DisplayType type);
/// <summary>
/// Asynchronously inserts a new <see cref="DisplayConfig"/> and returns the persisted entity, typically populated with any server-generated values.
/// </summary>
/// <param name="config">The <see cref="DisplayConfig"/> to insert.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the inserted <see cref="DisplayConfig"/>, or <see langword="null"/> when no result is returned.</returns>
Task<DisplayConfig?> InsertOneAsyncAndReturn(DisplayConfig config);
/// <summary>
/// Updates the smart display configuration identified by the specified ID with the provided new configuration.
/// </summary>
/// <param name="displayConfigId">The unique identifier of the smart display configuration to update.</param>
/// <param name="newDisplayConfig">The new smart display configuration to apply.</param>
/// <returns>A task that represents the asynchronous update operation, containing the updated smart display configuration, or null if no configuration with the given ID was found.</returns>
Task<SmartDisplay?> UpdateSmartDisplay(ObjectId displayConfigId, SmartDisplay? newDisplayConfig);
/// <summary>
/// Updates an existing display nurse configuration identified by the specified identifier.
/// </summary>
/// <param name="displayConfigId">The identifier of the display nurse configuration to update.</param>
/// <param name="newDisplayConfig">The new display nurse configuration data, or null if not provided.</param>
/// <param name="nurseObs">The list of nurse observations to associate with the configuration.</param>
/// <returns>A task that represents the asynchronous operation, containing the updated <see cref="DisplayNurse"/> or null if not found.</returns>
Task<DisplayNurse?> UpdateDisplayNurse(ObjectId displayConfigId, DisplayNurseDto? newDisplayConfig,
List<string> nurseObs);
List<string> nurseObs);
/// <summary>
/// Retrieves the default <see cref="DisplayConfig"/> associated with the specified unit and display type.
/// Returns <c>null</c> when no matching default configuration exists.
/// </summary>
/// <param name="unitId">The identifier of the unit whose default display configuration is being requested.</param>
/// <param name="displayType">The display type used to filter the configuration lookup.</param>
/// <returns>A task containing the matching <see cref="DisplayConfig"/>, or <c>null</c> if no default configuration is found for the given unit and display type.</returns>
Task<DisplayConfig?> GetDefaultByUnitIdAndType(ObjectId unitId, DisplayConfigEnums.DisplayType displayType);
/// <summary>
/// Asynchronously updates the color configuration for the specified config display entry.
/// </summary>
/// <param name="objectIdConfigDisplay">The unique identifier of the config display whose color configuration will be updated.</param>
/// <param name="colorConfig">The new color configuration to apply to the config display.</param>
/// <returns>A task that resolves to <c>true</c> if the color configuration was successfully updated; otherwise, <c>false</c>.</returns>
Task<bool> UpdateConfigColor(ObjectId objectIdConfigDisplay, ColorConfig colorConfig);
/// <summary>
/// Updates the home banner configuration identified by the specified config display ObjectId with the provided list of banner items.
/// </summary>
/// <param name="objectIdConfigDisplay">The ObjectId of the config display whose home banner set will be updated.</param>
/// <param name="bannerItems">The collection of banner items to apply to the home banner set.</param>
/// <returns>A task that resolves to <c>true</c> if the home banner set was updated successfully; otherwise, <c>false</c>.</returns>
Task<bool> UpdateSetHomeBanner(ObjectId objectIdConfigDisplay, List<BannerItem> bannerItems);
/// <summary>
/// Updates the base display configuration identified by the specified object identifier with the provided configuration data.
/// </summary>
/// <param name="objectIdConfigDisplay">The object identifier of the configuration display entry to update.</param>
/// <param name="baseConfig">The new base display configuration values to apply.</param>
/// <returns>A task that represents the asynchronous operation. The result is <c>true</c> if the update succeeded; otherwise, <c>false</c>.</returns>
Task<bool> UpdateBaseConfig(ObjectId objectIdConfigDisplay, DisplayConfig baseConfig);
/// <summary>
/// Updates an existing header configuration associated with the specified configuration display identifier.
/// </summary>
/// <param name="objectIdConfigDisplay">The unique identifier of the configuration display whose header configuration is being updated.</param>
/// <param name="headerConfig">The new header configuration values to apply.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the header configuration was successfully updated; otherwise, <c>false</c>.</returns>
Task<bool> UpdateHeaderConfig(ObjectId objectIdConfigDisplay, HeaderConfig headerConfig);
/// <summary>
/// Updates the hospital name associated with the specified display configuration.
/// </summary>
/// <param name="objectIdConfigDisplay">The identifier of the display configuration to update.</param>
/// <param name="name">The new hospital name to apply to the display configuration.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the update succeeded; otherwise, <c>false</c>.</returns>
Task<bool> UpdateDisplayConfigHospitalName(ObjectId objectIdConfigDisplay, string name);
/// <summary>
/// Updates the field list associated with the specified configuration display.
/// </summary>
/// <param name="objectIdConfigDisplay">The identifier of the configuration display whose field list is being updated.</param>
/// <param name="fields">The list of fields to apply to the configuration display.</param>
/// <returns>A task that resolves to <c>true</c> if the field list was updated successfully; otherwise, <c>false</c>.</returns>
Task<bool> UpdateFieldList(ObjectId objectIdConfigDisplay, List<Field> fields);
/// <summary>
/// Deletes a display configuration identified by the specified object ID.
/// </summary>
/// <param name="objectIdConfigDisplay">The object ID of the display configuration to delete.</param>
/// <returns>A task that represents the asynchronous delete operation. The task result contains the deleted display configuration, or null if no configuration was found with the specified ID.</returns>
Task<DisplayConfig?> DeleteDisplayConfig(ObjectId objectIdConfigDisplay);
/// <summary>
/// Retrieves all display configurations in a compact, minimal representation.
/// </summary>
/// <returns>A task containing a list of <see cref="DisplayConfigMinimalResponse"/> objects representing the display configurations.</returns>
Task<List<DisplayConfigMinimalResponse>> GetAllCompact();
/// <summary>
/// Retrieves all object identifiers associated with the specified card configuration identifier.
/// </summary>
/// <param name="cardConfigId">The identifier of the card configuration whose related objects should be returned.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="ObjectId"/> values linked to the given card configuration.</returns>
Task<List<ObjectId>> GetAllByCardConfigId(ObjectId cardConfigId);
/// <summary>
/// Asynchronously retrieves a list of ObjectIds associated with the specified card configuration that are marked as rotating.
/// </summary>
/// <param name="cardConfigId">The ObjectId of the card configuration used to filter the results.</param>
/// <returns>A task representing the asynchronous operation, containing a list of ObjectIds that match the given card configuration and rotating criteria.</returns>
Task<List<ObjectId>> GetAllByCardConfigIdAndRotating(ObjectId cardConfigId);
/// <summary>
/// Updates the card configuration identifier for the specified display configuration and result.
/// </summary>
/// <param name="displayConfigId">The identifier of the display configuration whose card configuration will be updated.</param>
/// <param name="resultId">The identifier of the result associated with the card configuration update.</param>
/// <returns>A task that resolves to <c>true</c> if the update succeeded; otherwise, <c>false</c>.</returns>
Task<bool> UpdateCardConfigId(ObjectId? displayConfigId, ObjectId? resultId);
/// <summary>
/// Adds a chart identifier to the specified display configuration asynchronously.
/// </summary>
/// <param name="displayConfigId">The identifier of the display configuration to which the chart ID will be added, or <c>null</c> when no specific configuration is targeted.</param>
/// <param name="newChartIdToAdd">The chart identifier to add.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the chart identifier was successfully added; otherwise, <c>false</c>.</returns>
Task<bool> AddChartId(ObjectId? displayConfigId, ObjectId newChartIdToAdd);
/// <summary>
/// Updates the configuration of a chart that was previously deleted, using the specified identifier.
/// </summary>
/// <param name="deletedId">The identifier of the deleted chart configuration to update.</param>
/// <returns>A task that represents the asynchronous update operation, containing the result of the update.</returns>
Task<UpdateResult> UpdateDeletedChartConfig(ObjectId deletedId);
/// <summary>
/// Asynchronously retrieves the chart configuration associated with the specified chart object identifier.
/// </summary>
/// <param name="objectIdConfigChart">The unique object identifier of the chart configuration to retrieve.</param>
/// <returns>A task that represents the asynchronous operation, containing the <see cref="ChartConfig"/> associated with the specified identifier.</returns>
Task<ChartConfig> GetChartConfig(ObjectId objectIdConfigChart);
/// <summary>
/// Asynchronously updates the detail configuration identifier, associating it with the specified result identifier.
/// </summary>
/// <param name="displayConfigId">The nullable identifier of the display configuration to update.</param>
/// <param name="resultId">The nullable identifier of the result to associate with the configuration.</param>
/// <returns>A task that represents the asynchronous operation, containing a boolean indicating whether the update was successful.</returns>
Task<bool> UpdateDetailConfigId(ObjectId? displayConfigId, ObjectId? resultId);
/// <summary>
/// Asynchronously retrieves a list of identifiers associated with the specified card detail base configuration.
/// </summary>
/// <param name="baseConfigId">The identifier of the card detail base configuration to filter by.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="ObjectId"/> values matching the provided base configuration identifier.</returns>
Task<List<ObjectId>> GetAllByCardDetailConfigId(ObjectId baseConfigId);
}
@@ -6,9 +6,34 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IDisplayDetailConfigRepository : IMongoRepository<CardDetailsConfig>
{
/// <summary>
/// Asynchronously retrieves all card details configurations from the data source.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of all <see cref="CardDetailsConfig"/> entries.</returns>
Task<List<CardDetailsConfig>> GetAll();
/// <summary>
/// Retrieves a <see cref="CardDetailsConfig"/> by its unique identifier.
/// Returns <see langword="null"/> when no matching configuration is found.
/// </summary>
/// <param name="configId">The identifier of the card details configuration to retrieve.</param>
/// <returns>A task that represents the asynchronous operation, containing the matching <see cref="CardDetailsConfig"/> or <see langword="null"/> if not found.</returns>
Task<CardDetailsConfig?> GetById(ObjectId configId);
/// <summary>
/// Asynchronously inserts a new card details configuration and returns the inserted entity.
/// </summary>
/// <param name="config">The card details configuration to insert.</param>
/// <returns>A task representing the asynchronous operation, containing the inserted <see cref="CardDetailsConfig"/>, or <c>null</c> if no entity was returned.</returns>
Task<CardDetailsConfig?> InsertOneAsyncAndReturn(CardDetailsConfig config);
/// <summary>
/// Updates a single card details configuration record asynchronously.
/// </summary>
/// <param name="config">The card details configuration to update. May be <see langword="null"/>.</param>
/// <returns>A task that represents the asynchronous operation, containing the update response with the updated <see cref="CardDetailsConfig"/> or <see langword="null"/> when no configuration is found.</returns>
Task<UpdateResponse<CardDetailsConfig?>> UpdateOne(CardDetailsConfig? config);
/// <summary>
/// Deletes a single <see cref="CardDetailsConfig"/> identified by its unique identifier.
/// </summary>
/// <param name="configId">The unique identifier of the <see cref="CardDetailsConfig"/> to delete.</param>
/// <returns>A task that represents the asynchronous operation, containing the deleted <see cref="CardDetailsConfig"/>, or <c>null</c> if no matching configuration was found.</returns>
Task<CardDetailsConfig?> DeleteOne(ObjectId configId);
}
@@ -5,31 +5,134 @@ using MongoDB.Driver;
namespace adas_core.Application.Repositories.Interfaces;
/// <summary>
/// Repository abstraction for <see cref="Display"/> entities persisted in MongoDB.
/// </summary>
public interface IDisplayRepository : IMongoRepository<Display>
{
/// <summary>
/// Retrieves every display stored in the collection.
/// </summary>
/// <returns>A <see cref="Task{List{Display}}"/> representing the asynchronous operation. The task result contains all displays, or an empty list if none exist.</returns>
Task<List<Display>> GetAll();
// ¿FilterByAuthorities / FindByUser
// Delete
// FindByUnit
// UpdateConfig -> algo en específico? ¿Toda la colección?
/// <summary>
/// Retrieves all displays that include the specified point-of-care in their point-of-care list.
/// </summary>
/// <param name="pointOfCare">The <see cref="PointOfCare"/> to filter by.</param>
/// <returns>A <see cref="Task{List{Display}}"/> representing the asynchronous operation. The task result contains the matching displays, or an empty list if none exist.</returns>
Task<List<Display>> GetByPointOfCare(PointOfCare pointOfCare);
// Task<List<Display>> GetByUser();
/// <summary>
/// Retrieves the display whose <c>Name</c> exactly matches the supplied value.
/// </summary>
/// <param name="name">The display name to look up.</param>
/// <returns>A <see cref="Task{Display}"/> representing the asynchronous operation. The task result contains the <see cref="Display"/> if found; otherwise, <see langword="null"/>.</returns>
Task<Display?> GetByName(string name);
/// <summary>
/// Retrieves a display by its unique identifier.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the display to retrieve.</param>
/// <returns>A <see cref="Task{Display}"/> representing the asynchronous operation. The task result contains the <see cref="Display"/> if found; otherwise, <see langword="null"/>.</returns>
Task<Display?> GetById(ObjectId id);
/// <summary>
/// Retrieves a display by its identifier, eagerly loading its associated <see cref="DisplayConfig"/> document.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the display to retrieve.</param>
/// <returns>A <see cref="Task{Display}"/> representing the asynchronous operation. The task result contains the <see cref="Display"/> with its config populated if found; otherwise, <see langword="null"/>.</returns>
Task<Display?> GetByIdWithConfigDisplay(ObjectId id);
/// <summary>
/// Retrieves all displays that belong to the specified unit.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the unit.</param>
/// <returns>A <see cref="Task{List{Display}}"/> representing the asynchronous operation. The task result contains the matching displays, or an empty list if none exist.</returns>
Task<List<Display>> GetByUnitId(ObjectId id);
/// <summary>
/// Retrieves all displays whose <c>DisplayConfigId</c> matches the supplied value.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the display configuration to filter by.</param>
/// <returns>A <see cref="Task{List{Display}}"/> representing the asynchronous operation. The task result contains the matching displays, or an empty list if none exist.</returns>
Task<List<Display>> GetByConfigId(ObjectId id);
/// <summary>
/// Replaces the point-of-care list of the specified display with the supplied set of point-of-care identifiers.
/// </summary>
/// <param name="objectId">The <see cref="ObjectId"/> of the display to update.</param>
/// <param name="listPocObId">The new list of <see cref="ObjectId"/> values representing the point-of-cares to associate with the display.</param>
/// <returns>A <see cref="Task{Display}"/> representing the asynchronous operation. The task result contains the updated <see cref="Display"/> if the update succeeded; otherwise, <see langword="null"/>.</returns>
Task<Display?> UpdatePointOfCareList(ObjectId objectId, List<ObjectId> listPocObId);
/// <summary>
/// Replaces the <see cref="DisplayConfig"/> of the display identified by <paramref name="oldDisplayId"/> with the supplied configuration instance.
/// </summary>
/// <param name="oldDisplayId">The <see cref="ObjectId"/> of the display whose configuration should be replaced.</param>
/// <param name="newDisplayConfigCast">The new <see cref="DisplayConfig"/> instance to assign to the display.</param>
/// <returns>A <see cref="Task{Display}"/> representing the asynchronous operation. The task result contains the updated <see cref="Display"/> if the update succeeded; otherwise, <see langword="null"/>.</returns>
Task<Display?> UpdateConfig(ObjectId oldDisplayId, DisplayConfig newDisplayConfigCast);
/// <summary>
/// Associates the display with an existing display-config preset by setting its <c>DisplayConfigId</c> to the supplied identifier.
/// </summary>
/// <param name="objectIdDisplay">The <see cref="ObjectId"/> of the display to update.</param>
/// <param name="objectIdConfigDisplay">The <see cref="ObjectId"/> of the display-config preset to associate.</param>
/// <returns>A <see cref="Task{Display}"/> representing the asynchronous operation. The task result contains the updated <see cref="Display"/> if the update succeeded; otherwise, <see langword="null"/>.</returns>
Task<Display?> UpdateConfigPreset(ObjectId objectIdDisplay, ObjectId objectIdConfigDisplay);
/// <summary>
/// Updates the <c>Name</c> of the supplied display instance and persists it.
/// </summary>
/// <param name="display">The <see cref="Display"/> instance to update. Its <see cref="ObjectId"/> is used to identify the document.</param>
/// <param name="name">The new display name.</param>
/// <returns>A <see cref="Task{Display}"/> representing the asynchronous operation. The task result contains the updated <see cref="Display"/>.</returns>
Task<Display> UpdateName(Display display, string name);
/// <summary>
/// Builds a paginated, sorted, and filtered query over displays according to the supplied <see cref="PaginationFilter"/>.
/// </summary>
/// <param name="filter">The <see cref="PaginationFilter"/> containing pagination and filter criteria.</param>
/// <returns>An <see cref="IFindFluent{Display, Display}"/> instance that can be used to further refine and execute the query.</returns>
IFindFluent<Display, Display> GetPaginatedDisplays(PaginationFilter filter);
/// <summary>
/// Counts the number of displays that currently reference the supplied display configuration.
/// </summary>
/// <param name="displayConfigId">The <see cref="ObjectId"/> of the display configuration to check for usage.</param>
/// <returns>A <see cref="Task{Int64}"/> representing the asynchronous operation. The task result contains the number of displays referencing the configuration.</returns>
Task<long> IsDisplayConfigInUse(ObjectId displayConfigId);
/// <summary>
/// Updates the <c>DisplayConfigId</c> of the specified display.
/// </summary>
/// <param name="objectId">The <see cref="ObjectId"/> of the display to update.</param>
/// <param name="displayConfigId">The new <see cref="ObjectId"/> of the display configuration to associate with the display.</param>
/// <returns>A <see cref="Task{Display}"/> representing the asynchronous operation. The task result contains the updated <see cref="Display"/> if the update succeeded; otherwise, <see langword="null"/>.</returns>
Task<Display?> UpdateConfigId(ObjectId objectId, ObjectId displayConfigId);
/// <summary>
/// Counts the number of displays that belong to the specified unit.
/// </summary>
/// <param name="unitId">The <see cref="ObjectId"/> of the unit.</param>
/// <returns>A <see cref="Task{Int64}"/> representing the asynchronous operation. The task result contains the number of displays for the unit.</returns>
Task<long> CountByUnitId(ObjectId unitId);
/// <summary>
/// Deletes every display that belongs to the specified unit.
/// </summary>
/// <param name="unitId">The <see cref="ObjectId"/> of the unit whose displays should be removed.</param>
/// <returns>A <see cref="Task{Boolean}"/> representing the asynchronous operation. The task result is <see langword="true"/> when at least one display was deleted; otherwise, <see langword="false"/>.</returns>
Task<bool> DeleteManyByUnitId(ObjectId unitId);
/// <summary>
/// Retrieves all displays that reference the supplied card-configuration identifier.
/// </summary>
/// <param name="configId">The <see cref="ObjectId"/> of the card configuration to filter by.</param>
/// <returns>A <see cref="Task{List{Display}}"/> representing the asynchronous operation. The task result contains the matching displays, or an empty list if none exist.</returns>
Task<List<Display>> GetByCardConfigId(ObjectId configId);
}
@@ -6,20 +6,63 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IHistoricalConfigChangesRepository : IMongoRepository<HistoricalConfigChanges>
{
/// <summary>
/// Asynchronously retrieves a historical configuration changes record by its unique identifier.
/// Returns null if no matching record is found.
/// </summary>
/// <param name="id">The unique identifier of the historical configuration changes record to look up.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="HistoricalConfigChanges"/> record, or null if no record with the specified identifier exists.</returns>
Task<HistoricalConfigChanges?> FindById(ObjectId id);
/// <summary>
/// Updates an existing historical config change record asynchronously.
/// </summary>
/// <param name="historicalConfigChange">The historical config change entity containing the values to persist.</param>
/// <returns>A task that represents the asynchronous update operation. The result is the updated historical config change, or <c>null</c> if the record was not found.</returns>
Task<HistoricalConfigChanges?> Update(HistoricalConfigChanges historicalConfigChange);
/// <summary>
/// Deletes the historical configuration change record identified by the specified identifier, returning the removed entry when found.
/// If no record matches the given identifier, the task resolves to <c>null</c>.
/// </summary>
/// <param name="id">The unique identifier of the historical configuration change to delete.</param>
/// <returns>A task that represents the asynchronous delete operation, containing the deleted <see cref="HistoricalConfigChanges"/> record, or <c>null</c> if no matching record exists.</returns>
Task<HistoricalConfigChanges?> Delete(ObjectId id);
/// <summary>
/// Asynchronously retrieves a list of all object identifiers.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of ObjectId values.</returns>
Task<List<ObjectId>> FindAllIds();
/// <summary>
/// Asynchronously retrieves all historical configuration changes.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of <see cref="HistoricalConfigChanges"/> records.</returns>
Task<ICollection<HistoricalConfigChanges>> FindAll();
/// <summary>
/// Asynchronously inserts a new <see cref="HistoricalConfigChanges"/> record into the data store.
/// </summary>
/// <param name="patientObservation">The historical config change entity to insert.</param>
/// <returns>A task that represents the asynchronous insert operation, containing the inserted <see cref="HistoricalConfigChanges"/> entity, or <c>null</c> if the insertion did not produce a result.</returns>
new Task<HistoricalConfigChanges?> InsertOneAsync(HistoricalConfigChanges patientObservation);
/// <summary>
/// Retrieves the most recent historical configuration changes filtered by the specified configuration type, limited to a given number of entries.
/// </summary>
/// <param name="cfgType">The configuration type used to filter the historical changes.</param>
/// <param name="num">The maximum number of recent changes to return. Defaults to 10.</param>
/// <returns>A task that returns a collection of historical configuration changes matching the specified type, ordered from most recent.</returns>
Task<ICollection<HistoricalConfigChanges>> FindLastHistoricalConfigChangesByType(
DisplayConfigEnums.ConfigTypes cfgType, int num = 10);
DisplayConfigEnums.ConfigTypes cfgType, int num = 10);
/// <summary>
/// Retrieves the most recent historical configuration changes for the specified user, optionally filtered by configuration type.
/// </summary>
/// <param name="user">The identifier of the user whose historical configuration changes are being retrieved.</param>
/// <param name="cfgType">Optional. The configuration type to filter the results by. If null, changes across all configuration types are returned.</param>
/// <param name="num">The maximum number of historical changes to return. Defaults to 10.</param>
/// <returns>A task representing the asynchronous operation, containing a collection of historical configuration changes for the user.</returns>
Task<ICollection<HistoricalConfigChanges>> FindLastHistoricalConfigChangesByUser(string user,
DisplayConfigEnums.ConfigTypes? cfgType = null, int num = 10);
DisplayConfigEnums.ConfigTypes? cfgType = null, int num = 10);
}
@@ -7,10 +7,40 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface ILightBeaconRepository : IMongoRepository<LightBeacon>
{
/// <summary>
/// Retrieves the list of <see cref="LightBeacon"/> objects associated with the specified configuration relay identifiers.
/// </summary>
/// <param name="configurationRelayList">The collection of configuration relay <see cref="ObjectId"/> values used to look up the corresponding light beacons.</param>
/// <returns>A <see cref="List{LightBeacon}"/> containing the light beacons matching the provided configuration relay identifiers; returns an empty list if no matches are found.</returns>
List<LightBeacon> GetLightBeaconInList(List<ObjectId> configurationRelayList);
/// <summary>
/// Retrieves a light beacon by its associated relay identifier.
/// </summary>
/// <param name="relayId">The unique identifier of the relay used to look up the corresponding light beacon.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="LightBeacon"/>, or <c>null</c> if no beacon is found for the specified relay.</returns>
Task<LightBeacon?> GetById(ObjectId relayId);
/// <summary>
/// Asynchronously retrieves a <see cref="LightBeacon"/> by its name.
/// </summary>
/// <param name="name">The name of the light beacon to look up.</param>
/// <returns>A task that returns the matching <see cref="LightBeacon"/>, or <c>null</c> if no beacon with the specified name is found.</returns>
Task<LightBeacon?> GetByName(string name);
/// <summary>
/// Asynchronously inserts a new LightBeacon and returns the persisted entity, or null if the insertion did not produce a result.
/// </summary>
/// <param name="beacon">The LightBeacon entity to insert.</param>
/// <returns>A task containing the inserted LightBeacon, or null when no entity was returned by the underlying operation.</returns>
Task<LightBeacon?> InsertOneAsyncAndReturn(LightBeacon beacon);
/// <summary>
/// Retrieves a paginated, queryable collection of light beacons (relays) based on the supplied pagination filter.
/// </summary>
/// <param name="filter">The pagination filter that controls paging parameters applied to the relay list.</param>
/// <returns>An <see cref="IFindFluent{LightBeacon, LightBeacon}"/> exposing the paginated relay result set for further querying or enumeration.</returns>
IFindFluent<LightBeacon, LightBeacon> GetPaginatedRelays(PaginationFilter filter);
/// <summary>
/// Asynchronously retrieves a list of <see cref="LightBeacon"/> objects whose name matches the specified search text.
/// </summary>
/// <param name="textToSearch">The text used to search for matching <see cref="LightBeacon"/> entries by name.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="LightBeacon"/> objects that match the search criteria.</returns>
Task<List<LightBeacon>> GetSearchByName(string textToSearch);
}
@@ -8,27 +8,158 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IMasterListRepository<T> : IMongoRepository<T> where T : MasterList
{
/// <summary>
/// Deletes the entity identified by the specified identifier.
/// </summary>
/// <param name="id">The identifier of the entity to delete.</param>
Task Delete(ObjectId id);
/// <summary>
/// Asynchronously updates the specified collection of items.
/// </summary>
/// <param name="list">The collection of items to update.</param>
Task Update(T list);
/// <summary>
/// Asynchronously retrieves an entity identified by the specified <paramref name="id"/>, optionally resolving localized content using the given <paramref name="locale"/>. Returns <c>null</c> when no matching entity is found.
/// </summary>
/// <param name="id">The unique identifier of the entity to look up.</param>
/// <param name="locale">The optional locale used to resolve localized fields of the retrieved entity.</param>
/// <returns>A task that yields the matching entity, or <c>null</c> if the entity is not found.</returns>
Task<T?> FindById(ObjectId id, LocaleEnum? locale);
/// <summary>
/// Asynchronously retrieves an entity of type <typeparamref name="T"/> by its unique identifier.
/// Returns <c>null</c> when no matching entity is found.
/// </summary>
/// <param name="id">The unique identifier of the entity to locate.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the entity of type <typeparamref name="T"/> if found, or <c>null</c> otherwise.</returns>
Task<T?> FindById(ObjectId id);
/// <summary>
/// Asynchronously retrieves an <see cref="OptionList"/> item identified by the specified master and option identifiers for the given locale.
/// Returns <c>null</c> when no matching option item is found for the provided identifiers and locale.
/// </summary>
/// <param name="masterId">The master identifier used to scope the lookup of the option item.</param>
/// <param name="optionId">The identifier of the specific option item to retrieve within the master scope.</param>
/// <param name="locale">The locale used to select the localized version of the option item.</param>
/// <returns>A <see cref="Task{TResult}"/> that yields the matching <see cref="OptionList"/>, or <c>null</c> if no item is found.</returns>
Task<OptionList?> FindOptionItemById(ObjectId masterId, ObjectId optionId, LocaleEnum locale);
/// <summary>
/// Asynchronously retrieves an <see cref="OptionList"/> identified by the given master and option identifiers.
/// </summary>
/// <param name="masterId">The identifier of the master record that owns the option list.</param>
/// <param name="optionId">The identifier of the specific option item to locate within the master.</param>
/// <returns>A <see cref="Task{OptionList}"/> that yields the matching <see cref="OptionList"/>, or <c>null</c> when no item is found.</returns>
Task<OptionList?> FindOptionItemById(ObjectId masterId, ObjectId optionId);
/// <summary>
/// Asynchronously retrieves all items of type T.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains an enumerable collection of all items of type T.</returns>
Task<IEnumerable<T>> GetAll();
/// <summary>
/// Retrieves all master list entries without applying optional filters or including related option data.
/// Used to fetch the complete set of master list records for scenarios such as full enumeration or bulk operations.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing an <see cref="IEnumerable{MasterListDto}"/> with all available master list entries.</returns>
Task<IEnumerable<MasterListDto>> GetAllWithoutOptions();
/// <summary>
/// Asynchronously finds and returns an entity of type <typeparamref name="T"/> matching the specified <paramref name="name"/>.
/// Returns <c>null</c> when no matching entity is found.
/// </summary>
/// <param name="name">The name used to look up the entity.</param>
/// <returns>A task that represents the asynchronous lookup, containing the matched entity of type <typeparamref name="T"/> or <c>null</c> if no match exists.</returns>
Task<T?> FindByName(string name);
/// <summary>
/// Retrieves a master list of option list entries filtered by the specified identifier and an optional text search that performs a contains match.
/// </summary>
/// <param name="id">The identifier used to scope the option list entries to be returned.</param>
/// <param name="textSearch">An optional text used to filter entries whose content contains the specified value; may be null to skip text-based filtering.</param>
/// <returns>A task representing the asynchronous operation, containing a list of <see cref="OptionList"/> entries that match the identifier and the optional text search criteria.</returns>
Task<List<OptionList>> GetMasterListByIdAndTextSearchContaining(ObjectId id, string? textSearch);
/// <summary>
/// Asynchronously adds a <see cref="FilterOptionListElement"/> to the master option list identified by the given <see cref="ObjectId"/>.
/// </summary>
/// <param name="id">The identifier of the master option list to which the element will be added.</param>
/// <param name="opt">The filter option list element to add to the master list.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the resulting <see cref="OptionList"/>, or <c>null</c> if no list is returned.</returns>
Task<OptionList?> AddOptionToMasterList(ObjectId id, FilterOptionListElement opt);
/// <summary>
/// Updates an existing master list option identified by the specified identifier using the provided option data and locale, returning the updated option list.
/// </summary>
/// <param name="id">The unique identifier of the master list option to update.</param>
/// <param name="newOpt">The new option list data to apply to the master list option.</param>
/// <param name="locale">The locale used for the update operation.</param>
/// <returns>A task that represents the asynchronous operation, containing the updated <see cref="OptionList"/> if found, or <c>null</c> if no matching master list option exists.</returns>
Task<OptionList?> UpdateMasterListOption(ObjectId id, OptionList newOpt, LocaleEnum locale);
/// <summary>
/// Updates an existing master list option identified by the given id with the provided data, returning the updated option or null if no matching option is found.
/// </summary>
/// <param name="id">The unique identifier of the master list option to update.</param>
/// <param name="newOpt">The new option data to replace the existing master list option.</param>
/// <returns>A task representing the asynchronous operation, containing the updated <see cref="OptionList"/>, or null if no option with the specified id exists.</returns>
Task<OptionList?> UpdateFullMasterListOption(ObjectId id, OptionList newOpt);
/// <summary>
/// Updates an existing master list option identified by the specified id with the provided new option data.
/// </summary>
/// <param name="id">The unique identifier of the master list option to update.</param>
/// <param name="newOpt">The new option data to apply to the existing master list option.</param>
/// <returns>A task that represents the asynchronous operation, containing the updated <see cref="OptionList"/>, or <c>null</c> if no matching option was found.</returns>
Task<OptionList?> UpdateMasterListOption(ObjectId id, OptionList newOpt);
/// <summary>
/// Deletes a master list option identified by the specified option ID.
/// </summary>
/// <param name="id">The identifier of the master list containing the option to delete.</param>
/// <param name="deleteOptId">The identifier of the option to be deleted from the master list.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the option was successfully deleted.</returns>
Task<bool> DeleteMasterListOption(ObjectId id, ObjectId deleteOptId);
/// <summary>
/// Updates the option details of an existing entry in the master list identified by the specified id.
/// </summary>
/// <param name="id">The unique identifier of the master list entry to update.</param>
/// <param name="opt">The update payload containing the new option details to apply.</param>
/// <returns>A task that represents the asynchronous operation, containing the updated master list details, or null if no matching entry is found.</returns>
Task<UpdateMasterListDetailsDto?> UpdateOptionDetailsToMasterList(ObjectId id, UpdateMasterListDetailsDto opt);
/// <summary>
/// Updates the name of an existing master list identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the master list to update.</param>
/// <param name="name">The new name to assign to the master list.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the master list was updated successfully; otherwise, <c>false</c>.</returns>
Task<bool> UpdateMasterListName(ObjectId id, string name);
/// <summary>
/// Asynchronously updates the description of a master list identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the master list to update.</param>
/// <param name="description">The new description to set for the master list.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <see langword="true"/> if the update was successful; otherwise, <see langword="false"/>.</returns>
Task<bool> UpdateMasterListDescription(ObjectId id, string description);
/// <summary>
/// Asynchronously removes the specified option from the master list associated with the given identifier.
/// </summary>
/// <param name="id">The identifier of the master list from which the option will be removed.</param>
/// <param name="oldOpt">The option to be removed from the master list.</param>
/// <returns>A task that represents the asynchronous removal operation. The task result contains true if the option was successfully removed; otherwise, false.</returns>
Task<bool> RemoveMasterListOption(ObjectId id, OptionList oldOpt);
/// <summary>
/// Retrieves a paginated master list using the specified pagination filter, returning a fluent queryable result.
/// </summary>
/// <param name="filter">The pagination filter that defines the paging criteria applied to the master list query.</param>
/// <returns>An <see cref="IFindFluent{TDocument, TProjection}"/> that represents the paginated queryable master list.</returns>
IFindFluent<T, T> GetPaginatedMasterList(PaginationFilter filter);
/// <summary>
/// Retrieves a paginated collection of options associated with the specified list identifier, applying the provided pagination filter to control the result set.
/// </summary>
/// <param name="filter">The pagination filter defining page size, page number, and related pagination constraints.</param>
/// <param name="listId">The unique identifier of the list whose options should be retrieved.</param>
/// <returns>A task representing the asynchronous operation, containing a list of <see cref="OptionList"/> items for the requested page.</returns>
Task<List<OptionList>> GetPaginatedOptions(PaginationFilter filter, ObjectId listId);
/// <summary>
/// Asynchronously counts the number of items and returns the total.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains the total count of items.</returns>
Task<int> Count();
/// <summary>
/// Retrieves a master list of <see cref="OptionList"/> entries identified by the given id, optionally applying the provided search and filtering options.
/// </summary>
/// <param name="id">The identifier of the master list to retrieve.</param>
/// <param name="filterOption">Optional filter criteria used to narrow the returned options. May be <c>null</c> to return the full list without additional filtering.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of <see cref="OptionList"/> items that match the specified id and filter, or an empty list if no matching entries are found.</returns>
Task<List<OptionList>> GetMasterListByIdAndSearchOptions(ObjectId id, FilterOptionListElement? filterOption);
}
@@ -7,16 +7,65 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IMedicineRepository : IMongoRepository<Medicine>
{
/// <summary>
/// Retrieves a medicine by its code asynchronously.
/// </summary>
/// <param name="code">The code used to look up the medicine.</param>
/// <returns>A task that returns the matching <see cref="Medicine"/> if found, or <c>null</c> if no medicine matches the specified code.</returns>
Task<Medicine?> GetMedicine(string code);
/// <summary>
/// Retrieves a list of medicines that match the specified codes or notes.
/// </summary>
/// <param name="codeNotes">The list of codes or notes used to look up the medicines.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of matching medicines.</returns>
Task<List<Medicine>> GetMedicineByCodeOrNote(List<string> codeNotes);
/// <summary>
/// Retrieves a <see cref="Medicine"/> entity by its name asynchronously.
/// </summary>
/// <param name="name">The name of the medicine to look up.</param>
/// <returns>A <see cref="Task{Medicine}"/> that resolves to the matching <see cref="Medicine"/>, or <c>null</c> if no medicine with the specified name is found.</returns>
Task<Medicine?> GetMedicineByName(string name);
/// <summary>
/// Asynchronously retrieves all medicines from the data source.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="Medicine"/> objects; an empty list is returned when no medicines are found.</returns>
Task<List<Medicine>> GetAll();
/// <summary>
/// Retrieves a medicine entity by its unique identifier from the data store.
/// Returns <see langword="null"/> if no medicine matches the provided identifier.
/// </summary>
/// <param name="medicineId">The unique <see cref="ObjectId"/> of the medicine to retrieve.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the matching <see cref="Medicine"/> if found, or <see langword="null"/> when no record exists for the given identifier.</returns>
Task<Medicine?> GetMedicineById(ObjectId medicineId);
/// <summary>
/// Asynchronously creates and posts a new medicine entry, returning the persisted <see cref="Medicine"/> on success or <c>null</c> when no result is produced.
/// </summary>
/// <param name="medicine">The medicine entity to be created and submitted.</param>
/// <returns>A task that resolves to the newly created <see cref="Medicine"/>, or <c>null</c> if the operation does not yield a result.</returns>
Task<Medicine?> PostMedicine(Medicine medicine);
/// <summary>
/// Updates an existing medicine record with the provided information.
/// </summary>
/// <param name="medicine">The medicine entity containing the updated data, typically identified by its primary key.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated <see cref="Medicine"/>, or <c>null</c> if no matching medicine was found.</returns>
Task<Medicine?> UpdateMedicine(Medicine medicine);
/// <summary>
/// Asynchronously deletes a medicine record from the data store using the specified identifier.
/// </summary>
/// <param name="medicineId">The unique identifier of the medicine to delete.</param>
Task DeleteMedicineById(ObjectId medicineId);
/// <summary>
/// Builds a MongoDB aggregation pipeline that retrieves the distinct values for the specified field.
/// </summary>
/// <param name="field">The name of the field whose distinct values should be queried.</param>
/// <returns>An <see cref="IAggregateFluent{BsonDocument}"/> representing the distinct field data query.</returns>
IAggregateFluent<BsonDocument> GetDistinctFieldDataQuery(string field);
/// <summary>
/// Retrieves a paginated collection of medicines based on the specified pagination filter.
/// </summary>
/// <param name="filter">The pagination filter containing the page number and page size used to control the result set.</param>
/// <returns>A fluent query interface for the paginated medicines.</returns>
IFindFluent<Medicine, Medicine> GetPaginatedMedicines(PaginationFilter filter);
}
@@ -7,8 +7,27 @@ public interface IMongoRepository<T>
{
IMongoCollection<T> Collection { get; }
/// <summary>
/// Gets the name of the collection.
/// </summary>
/// <returns>The name of the collection.</returns>
string GetCollectionName();
/// <summary>
/// Asynchronously inserts a single object of type <typeparamref name="T"/>.
/// </summary>
/// <param name="obj">The object to insert.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
Task InsertOneAsync(T obj);
/// <summary>
/// Asynchronously deletes the entity identified by the specified identifier and returns the deleted entity.
/// </summary>
/// <param name="id">The unique identifier of the entity to delete.</param>
/// <returns>A task that represents the asynchronous operation, containing the deleted entity of type <typeparamref name="T"/>, or <c>null</c> if no matching entity was found.</returns>
Task<T?> DeleteAsync(ObjectId id);
/// <summary>
/// Asynchronously updates an existing document identified by the specified identifier with the provided object data.
/// </summary>
/// <param name="id">The unique identifier of the document to update.</param>
/// <param name="obj">The object containing the updated values to persist.</param>
Task UpdateOneAsync(ObjectId id, T obj);
}
@@ -5,11 +5,43 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface INoticeRepository : IMongoRepository<Notice>
{
/// <summary>
/// Deletes the entity identified by the specified <see cref="ObjectId"/>.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the entity to delete.</param>
Task Delete(ObjectId id);
/// <summary>
/// Asynchronously updates an existing notice with the provided information.
/// </summary>
/// <param name="notice">The notice entity containing the updated data to be persisted.</param>
Task Update(Notice notice);
/// <summary>
/// Asynchronously retrieves all notices.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains an enumerable collection of all <see cref="Notice"/> objects.</returns>
Task<IEnumerable<Notice>> FindAll();
/// <summary>
/// Retrieves a <see cref="Notice"/> by its unique identifier. Returns <c>null</c> when no matching notice is found.
/// </summary>
/// <param name="id">The identifier of the notice to look up.</param>
/// <returns>A task that yields the matching <see cref="Notice"/>, or <c>null</c> if no notice exists for the specified <paramref name="id"/>.</returns>
Task<Notice?> FindById(ObjectId id);
/// <summary>
/// Asynchronously retrieves the collection of notices that match the specified date.
/// </summary>
/// <param name="date">The date used to filter the notices to be retrieved.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of <see cref="Notice"/> objects for the specified date, or <c>null</c> if no matching notices are found.</returns>
Task<IEnumerable<Notice>?> FindByDate(DateTime date);
/// <summary>
/// Asynchronously retrieves the <see cref="Notice"/> entries that match the specified type.
/// </summary>
/// <param name="type">The notice type used to filter the lookup.</param>
/// <returns>A task containing the matching <see cref="Notice"/> items, or <c>null</c> when no results are available.</returns>
Task<IEnumerable<Notice>?> FindByType(string type);
/// <summary>
/// Asynchronously retrieves the <see cref="Notice"/> entities associated with the specified display identifier.
/// </summary>
/// <param name="displayId">The display identifier used to locate the matching notices.</param>
/// <returns>A task that yields the collection of matching <see cref="Notice"/> items, or <c>null</c> if no notices are found.</returns>
Task<IEnumerable<Notice>?> FindByDisplayId(ObjectId displayId);
}
@@ -5,12 +5,38 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IObservationArchiveRepository : IMongoRepository<PatientObservation>
{
/// <summary>
/// Asynchronously inserts a single <see cref="PatientObservation"/> into the data store.
/// </summary>
/// <param name="patientObservation">The patient observation to insert.</param>
new Task InsertOneAsync(PatientObservation patientObservation);
/// <summary>
/// Deletes the records that have a date earlier than the specified cutoff date.
/// </summary>
/// <param name="date">The cutoff date; records dated before this value will be removed.</param>
Task DeleteBeforeDate(DateTime date);
/// <summary>
/// Asynchronously inserts a batch of patient observation records into the underlying data store.
/// </summary>
/// <param name="observations">The collection of <see cref="PatientObservation"/> entities to be inserted.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous insert operation, returning a <see cref="long"/> value (such as the number of affected rows or a generated identifier) produced by the batch insertion.</returns>
Task<long> InsertBatch(IEnumerable<PatientObservation> observations);
/// <summary>
/// Retrieves the most recent aggregated patient observations for the specified patient, limited by a count and an upper date bound, and optionally filtered by observation names.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being queried.</param>
/// <param name="num">The maximum number of recent observations to return.</param>
/// <param name="lastDate">The cutoff date; only observations on or before this date are considered.</param>
/// <param name="filterObservations">An optional list of observation names to restrict the results to; pass an empty or null list to include all observations.</param>
/// <returns>A task that resolves to a list of <see cref="PatientObservation"/> entries representing the aggregated last observations matching the criteria.</returns>
Task<List<PatientObservation>> AggregatedPatientLastObservations(ObjectId patientId, int num, DateTime lastDate,
List<string> filterObservations);
List<string> filterObservations);
/// <summary>
/// Retrieves all patient observations associated with the specified patient.
/// </summary>
/// <param name="patientId">The identifier of the patient whose observations are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientObservation"/> records for the patient.</returns>
Task<List<PatientObservation>> FindAllFromPatient(ObjectId patientId);
}
@@ -9,66 +9,257 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IObservationRepository : IMongoRepository<PatientObservation>
{
/// <summary>
/// Asynchronously aggregates a patient's observations grouped by the specified field.
/// </summary>
/// <param name="patientId">The identifier of the patient whose observations will be aggregated.</param>
/// <param name="groupedField">The field used to group the patient's observations during aggregation.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of BSON documents with the aggregated results.</returns>
Task<List<BsonDocument>> AggregatedPatientGroupedObservations(ObjectId patientId, GroupedField groupedField);
/// <summary>
/// Asynchronously retrieves the most recent aggregated observations for a specified patient,
/// returning up to the requested number of entries. Optionally filters the results to include
/// only observations whose identifiers match the provided list.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being queried.</param>
/// <param name="num">The maximum number of most recent observations to return.</param>
/// <param name="filterObservations">An optional list of observation identifiers used to restrict the returned results; if null, no filter is applied.</param>
/// <returns>A task representing the asynchronous operation, containing a list of the patient's most recent <see cref="PatientObservation"/> records.</returns>
Task<List<PatientObservation>> AggregatedPatientLastObservations(ObjectId patientId, int num,
List<string>? filterObservations = null);
List<string>? filterObservations = null);
/// <summary>
/// Retrieves the most recent aggregated patient observations for the specified patient up to the given last date, optionally filtered by a set of observation names.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being retrieved.</param>
/// <param name="num">The maximum number of observations to return.</param>
/// <param name="lastDate">The cutoff date; only observations on or before this date are considered.</param>
/// <param name="filterObservations">An optional list of observation names to restrict the results to. If null, no observation-name filter is applied.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of aggregated <see cref="PatientObservation"/> entries that match the criteria.</returns>
Task<List<PatientObservation>> AggregatedPatientLastObservationsByLastDate(ObjectId patientId, int num,
DateTime lastDate, List<string>? filterObservations = null);
DateTime lastDate, List<string>? filterObservations = null);
/// <summary>
/// Retrieves the most recent patient observations, aggregated by field, for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being retrieved.</param>
/// <param name="filterObservations">An optional list of fields to filter the observations by; if null, observations for all fields are returned.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of the latest patient observations grouped by field.</returns>
Task<List<PatientObservation>> AggregatedPatientLastObservationsByField(ObjectId patientId,
List<Field>? filterObservations = null);
List<Field>? filterObservations = null);
/// <summary>
/// Retrieves the most recent patient observations that match the specified coding system and code, returning up to the requested number of results.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being queried.</param>
/// <param name="codingSystem">The coding system used to classify the observations (for example, LOINC, SNOMED).</param>
/// <param name="code">The specific code within the coding system identifying the type of observation to retrieve.</param>
/// <param name="num">The maximum number of most recent observations to return. Defaults to 2.</param>
/// <returns>A task that yields a collection of the matching <see cref="PatientObservation"/> records, ordered from most recent to oldest, containing at most <paramref name="num"/> entries.</returns>
Task<IEnumerable<PatientObservation>> FindLastObservations(ObjectId patientId, string codingSystem, string code,
int num = 2);
int num = 2);
/// <summary>
/// Retrieves the most recent patient observations filtered by the specified coding system.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being queried.</param>
/// <param name="codingSystem">The coding system used to filter the observations.</param>
/// <param name="num">The maximum number of recent observations to return. Defaults to 10.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of the patient's most recent observations matching the coding system.</returns>
Task<List<PatientObservation>> FindLastObservationsByCodingSystem(ObjectId patientId, string codingSystem,
int num = 10);
int num = 10);
/// <summary>
/// Retrieves the most recent <see cref="PatientObservation"/> for the specified patient recorded before the given date, optionally filtered by observation name.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observation is being queried.</param>
/// <param name="name">The optional name of the observation to filter by. If <c>null</c>, observations of any name are considered.</param>
/// <param name="date">The cutoff date; only observations recorded strictly before this date are eligible.</param>
/// <returns>A task that resolves to the matching <see cref="PatientObservation"/> if found, or <c>null</c> if no observation exists before the specified date.</returns>
Task<PatientObservation?> FindLastObservationBeforeDate(ObjectId patientId, string? name, DateTime date);
/// <summary>
/// Asynchronously updates the state of patient observations that have expired, applying the appropriate expiration handling to each item in the provided list.
/// </summary>
/// <param name="expiredObservations">The list of patient observations that have expired and require their state to be updated.</param>
Task UpdateExpiredObservations(List<PatientObservation> expiredObservations);
/// <summary>
/// Finds any existing patient observations for the specified patient that share the same date, optionally filtered by observation name.
/// </summary>
/// <param name="patientId">The identifier of the patient whose observations are being searched.</param>
/// <param name="name">The optional name of the observation to match; if null, observations of any name are considered.</param>
/// <param name="date">The date used to match observations recorded on the same day.</param>
/// <returns>A task that returns a list of matching <see cref="PatientObservation"/> instances, or <c>null</c> if no matching observations are found.</returns>
Task<List<PatientObservation>?> FindAnyWithSameDate(ObjectId patientId, string? name, DateTime date);
/// <summary>
/// Asynchronously retrieves all patient observations recorded for the specified patient before the given date.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being queried.</param>
/// <param name="date">The cutoff date; only observations recorded prior to this date are returned.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of patient observations found before the specified date.</returns>
Task<List<PatientObservation>> FindAnyBeforeDate(ObjectId patientId, DateTime date);
/// <summary>
/// Retrieves the most recent unique patient observations matching the specified name for the given patient, returning a list of <see cref="PatientObservation"/> entries.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being queried.</param>
/// <param name="name">The name used to match the relevant patient observations.</param>
/// <param name="expires">An optional expiration value in seconds used to control the time window for the lookup; if <c>null</c>, no expiration is applied.</param>
/// <returns>A task that resolves to a list of the latest unique <see cref="PatientObservation"/> records matching the specified name for the patient. The list is empty if no matching observations are found.</returns>
Task<List<PatientObservation>> FindLatestUniqueValuesByName(ObjectId patientId, string name, int? expires);
/// <summary>
/// Asynchronously inserts a single <see cref="PatientObservation"/> record into the underlying data store.
/// </summary>
/// <param name="patientObservation">The patient observation entity to persist.</param>
new Task InsertOneAsync(PatientObservation patientObservation);
/// <summary>
/// Deletes records associated with the specified patient identifier.
/// </summary>
/// <param name="id">The ObjectId of the patient whose related records should be removed.</param>
Task DeleteByPatientId(ObjectId id);
/// <summary>
/// Asynchronously retrieves the collection of patient observations associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are to be retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing an <see cref="IAsyncCursor{TDocument}"/> of <see cref="PatientObservation"/> instances to iterate through the matching records.</returns>
Task<IAsyncCursor<PatientObservation>> FindByPatientIdAsync(ObjectId patientId);
/// <summary>
/// Asynchronously retrieves the <see cref="PatientObservation"/> records associated with the specified patient
/// that belong to a given coding system, optionally filtered by name.
/// </summary>
/// <param name="patientId">The identifier of the patient whose observations are being queried.</param>
/// <param name="codingSystem">The coding system used to classify the observations (e.g., LOINC, SNOMED).</param>
/// <param name="name">The name of the observation to filter within the coding system.</param>
/// <returns>A <see cref="Task{TResult}"/> that yields an <see cref="IAsyncCursor{TDocument}"/> streaming the matching <see cref="PatientObservation"/> documents.</returns>
Task<IAsyncCursor<PatientObservation>> FindByPatientIdAndCodingSystemAsync(ObjectId patientId, string codingSystem,
string name);
string name);
/// <summary>
/// Asynchronously deletes the entity identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the entity to delete.</param>
/// <returns>A task that represents the asynchronous delete operation.</returns>
new Task DeleteAsync(ObjectId id);
/// <summary>
/// Asynchronously deletes patient observations older than the specified retention period and returns the records that were removed.
/// </summary>
/// <param name="name">The identifier used to locate the relevant patient observations to be evaluated for deletion.</param>
/// <param name="retentionPolicyValue">The retention period in days; observations older than this value will be deleted.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of the deleted <see cref="PatientObservation"/> records.</returns>
Task<List<PatientObservation>> DeleteOlderDaysAsync(string name, int retentionPolicyValue);
/// <summary>
/// Asynchronously deletes patient observations based on the specified name and retention policy value.
/// </summary>
/// <param name="name">The name used to identify or filter the patient observations to be deleted.</param>
/// <param name="retentionPolicyValue">The retention policy threshold value that determines which older observations should be removed.</param>
/// <returns>A task representing the asynchronous operation, containing the list of patient observations that were deleted.</returns>
Task<List<PatientObservation>> DeleteOlderNumberAsync(string name, int retentionPolicyValue);
/// <summary>
/// Asynchronously checks whether a record exists for the specified patient identified by the given system identifier.
/// </summary>
/// <param name="patientid">The unique identifier of the patient whose record is being checked.</param>
/// <param name="systemId">The system identifier used to look up the patient's record.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if a matching record exists; otherwise, <c>false</c>.</returns>
Task<bool> ExistBySystemId(ObjectId patientid, string systemId);
/// <summary>
/// Asynchronously deletes patient observations older than the specified number of seconds and returns the affected records.
/// </summary>
/// <param name="name">The name used to identify the patient observations to filter for deletion.</param>
/// <param name="value">The age threshold in seconds; observations older than this value will be deleted.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of deleted patient observations.</returns>
Task<List<PatientObservation>> DeleteOlderSecondsAsync(string name, int value);
/// <summary>
/// Retrieves the aggregated active intravenous lines observations for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active intravenous lines observations are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientObservation"/> entries representing the patient's active intravenous lines observations, where each entry may be null.</returns>
Task<List<PatientObservation?>> AggregatedPatientActiveIntravenousLinesObservations(ObjectId patientId);
/// <summary>
/// Asynchronously retrieves the most recent observation timestamp for every patient, returning a mapping of patient identifiers to their last observation times.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a dictionary where each key is a patient <see cref="ObjectId"/> and the associated value is the date and time of that patient's last observation.</returns>
Task<Dictionary<ObjectId, DateTime>> FindAllLastPatientObservationTime();
/// <summary>
/// Asynchronously retrieves a patient observation by its unique identifier, returning <c>null</c> when no matching record is found.
/// </summary>
/// <param name="id">The unique identifier of the patient observation to locate.</param>
/// <returns>A <see cref="Task{PatientObservation}"/> that resolves to the matching <see cref="PatientObservation"/>, or <c>null</c> if no observation exists for the given identifier.</returns>
Task<PatientObservation?> FindById(ObjectId id);
/// <summary>
/// Asynchronously retrieves a list of patient observations associated with the specified patient identifier. Returns null when no observations are found for the patient.
/// </summary>
/// <param name="id">The unique identifier of the patient whose observations are being retrieved.</param>
/// <returns>A task containing a list of <see cref="PatientObservation"/> objects for the specified patient, or null if no observations are found.</returns>
Task<List<PatientObservation>?> FindByPatientId(ObjectId id);
/// <summary>
/// Updates an existing patient observation with the provided data.
/// </summary>
/// <param name="observation">The patient observation containing the updated information to be persisted.</param>
Task Update(PatientObservation observation);
/// <summary>
/// Updates many records by replacing the specified old ObjectId with the new ObjectId identified by the given name.
/// </summary>
/// <param name="nameId">The name identifier used to locate the target collection or field to update.</param>
/// <param name="id">The new ObjectId to assign to the matching records.</param>
/// <param name="oldId">The existing ObjectId that identifies the records to be updated.</param>
Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId);
/// <summary>
/// Asynchronously retrieves patient observations that have not expired, optionally filtered by the specified observation identifiers.
/// </summary>
/// <param name="filterObservations">An optional list of observation identifiers used to narrow the result set. May be null or contain null entries.</param>
/// <returns>A task that resolves to a collection of non-expired <see cref="PatientObservation"/> instances matching the filter criteria.</returns>
Task<IEnumerable<PatientObservation>> FindNotExpired(List<string?>? filterObservations);
/// <summary>
/// Asynchronously retrieves patient observations matching the specified name, optionally filtered by the provided date.
/// </summary>
/// <param name="name">The name used to look up the patient observations.</param>
/// <param name="date">The optional date used to filter the observations; when null, results are not restricted by date.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of <see cref="PatientObservation"/> entries that match the criteria.</returns>
Task<IEnumerable<PatientObservation>> FindByName(string name, DateTime? date);
/// <summary>
/// Updates multiple <see cref="PatientObservation"/> documents that match the provided update definition.
/// </summary>
/// <param name="patientObservations">The collection of patient observations to update.</param>
/// <param name="update">The update definition describing the modifications to apply to each patient observation.</param>
Task UpdateMany(IEnumerable<PatientObservation> patientObservations, UpdateDefinition<PatientObservation> update);
/// <summary>
/// Retrieves all patient observation records.
/// </summary>
/// <returns>A task representing the asynchronous operation, containing a collection of <see cref="PatientObservation"/> entries.</returns>
Task<IEnumerable<PatientObservation>> FindAll();
/// <summary>
/// Asynchronously processes and expires the specified configuration observations that have met their expiration criteria.
/// </summary>
/// <param name="configObservationsToExpire">The list of configuration observations to expire.</param>
Task ExpireExpiredObservations(List<ConfigObservation> configObservationsToExpire);
//Task<PaginationResponse<PatientObservation>> GetPaginatedObservations(PaginationFilter filter);
/// <summary>
/// Retrieves a paginated, fluent-queryable collection of patient observations based on the provided pagination filter.
/// </summary>
/// <param name="filter">The pagination filter that defines the page size, page number, and sorting criteria applied to the observations.</param>
/// <returns>An <see cref="IFindFluent{TSource, TDocument}"/> for <see cref="PatientObservation"/> that allows further refinement and execution of the paginated query.</returns>
IFindFluent<PatientObservation, PatientObservation> GetPaginatedObservations(PaginationFilter filter);
/// <summary>
/// Asynchronously retrieves the most recent non-expired patient observations matching the specified name, supporting optional pagination.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being queried.</param>
/// <param name="name">The name of the observation to filter by.</param>
/// <param name="endAfter">Optional pagination cursor indicating the number of results to skip.</param>
/// <param name="num">Optional maximum number of observations to return.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of matching <see cref="PatientObservation"/> objects.</returns>
Task<IEnumerable<PatientObservation>> FindLastNotExpiredObservatonsByPatient(ObjectId patientId, string name,
int? endAfter = null, int? num = null);
int? endAfter = null, int? num = null);
}
@@ -5,8 +5,23 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPatientArchiveRepository : IMongoRepository<Patient>
{
/// <summary>
/// Retrieves a <see cref="Patient"/> matching the specified patient number, returning <see langword="null"/> if no matching patient is found.
/// </summary>
/// <param name="patientNumber">The unique patient number used to look up the patient.</param>
/// <returns>A <see cref="Patient"/> instance if a match is found; otherwise, <see langword="null"/>.</returns>
Task<Patient?> FindByPatientNumber(string patientNumber);
/// <summary>
/// Retrieves all patients from the data source.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of all <see cref="Patient"/> entities.</returns>
Task<List<Patient>> FindAll();
/// <summary>
/// Searches for a patient by patient number within a specific distinct unit.
/// </summary>
/// <param name="patientNumber">The patient number used to locate the patient.</param>
/// <param name="unitId">The identifier of the distinct unit in which to perform the search.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="Patient"/> if found; otherwise, <c>null</c>.</returns>
Task<Patient?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId);
}
@@ -5,8 +5,28 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPatientCarePlanRepository : IMongoRepository<PatientCarePlan>
{
/// <summary>
/// Retrieves the list of care plans associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose care plans are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientCarePlan"/> records for the patient, or an empty list if no care plans are found.</returns>
Task<List<PatientCarePlan>> FindByPatientId(ObjectId patientId);
/// <summary>
/// Asynchronously retrieves the list of patient care plans associated with the specified user identifier.
/// </summary>
/// <param name="userId">The unique identifier of the user whose patient care plans are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientCarePlan"/> objects linked to the specified user.</returns>
Task<List<PatientCarePlan>> FindByUserId(ObjectId userId);
/// <summary>
/// Asynchronously retrieves all patient care plans from the data store.
/// </summary>
/// <returns>A task representing the asynchronous operation, containing a list of all <see cref="PatientCarePlan"/> records.</returns>
Task<List<PatientCarePlan>> FindAll();
/// <summary>
/// Updates all records that reference the specified old ObjectId so they are associated with the new patient ObjectId.
/// </summary>
/// <param name="patientid">The string identifier of the patient whose related records will be updated.</param>
/// <param name="patientId">The new ObjectId that will replace the previous identifier in the matching records.</param>
/// <param name="oldId">The previous ObjectId whose references should be replaced across the matching records.</param>
Task UpdateManyObjectId(string patientid, ObjectId patientId, ObjectId oldId);
}
@@ -10,36 +10,202 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPatientRepository : IMongoRepository<Patient>
{
/// <summary>
/// Deletes the item identified by the specified identifier.
/// </summary>
/// <param name="id">The identifier of the item to delete.</param>
Task Delete(ObjectId id);
/// <summary>
/// Asynchronously retrieves a <see cref="Patient"/> associated with the specified location, or <see langword="null"/> if no patient is found at that location.
/// </summary>
/// <param name="location">The location used to look up the patient.</param>
/// <returns>A task that represents the asynchronous operation, containing the <see cref="Patient"/> if found; otherwise, <see langword="null"/>.</returns>
Task<Patient?> FindByLocation(PatientLocation location);
/// <summary>
/// Retrieves a patient by their unique identifier, returning <c>null</c> when no matching patient exists.
/// </summary>
/// <param name="id">The unique <see cref="ObjectId"/> of the patient to look up.</param>
/// <returns>A <see cref="Task{T}"/> that resolves to the matching <see cref="Patient"/>, or <c>null</c> if no patient is found.</returns>
Task<Patient?> FindById(ObjectId id);
/// <summary>
/// Asynchronously updates the location of a patient identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the patient whose location is to be updated.</param>
/// <param name="location">The new patient location data to apply to the existing record.</param>
Task UpdateLocation(ObjectId id, PatientLocation location);
/// <summary>
/// Updates the location of the entity identified by the specified identifier.
/// </summary>
/// <param name="id">The identifier of the entity whose location will be updated.</param>
/// <param name="location">The identifier of the new location to associate with the entity.</param>
Task UpdateLocation(ObjectId id, ObjectId location);
/// <summary>
/// Updates the attending doctor for the record identified by the specified id.
/// </summary>
/// <param name="id">The identifier of the record whose attending doctor will be updated.</param>
/// <param name="attendingDoctor">The person to be set as the new attending doctor.</param>
Task UpdateAttendingDoctor(ObjectId id, Person attendingDoctor);
/// <summary>
/// Updates the patient data identified by the specified id, optionally updating the patient number when <paramref name="updatePatientNumber"/> is true.
/// </summary>
/// <param name="id">The unique identifier of the patient to update.</param>
/// <param name="patientNumber">The patient number associated with the patient.</param>
/// <param name="data">The new person data to apply to the patient record.</param>
/// <param name="updatePatientNumber">Indicates whether the patient number should be updated; defaults to true.</param>
Task UpdatePatientData(ObjectId id, string patientNumber, Person data, bool updatePatientNumber = true);
/// <summary>
/// Updates the information of an existing patient in the system.
/// </summary>
/// <param name="patient">The patient entity containing the updated information to be persisted.</param>
Task Update(Patient patient);
/// <summary>
/// Asynchronously retrieves a <see cref="Patient"/> by their unique patient number.
/// Returns <c>null</c> when no patient matches the provided identifier.
/// </summary>
/// <param name="patientNumber">The unique patient number used to look up the patient.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the matching <see cref="Patient"/>, or <c>null</c> if no patient is found.</returns>
Task<Patient?> FindByPatientNumber(string patientNumber);
/// <summary>
/// Asynchronously retrieves a <see cref="Patient"/> that matches the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient to look up.</param>
/// <returns>A task that represents the asynchronous operation, containing the matching <see cref="Patient"/> if found; otherwise, <c>null</c>.</returns>
Task<Patient?> FindByPatientId(string patientId);
/// <summary>
/// Asynchronously retrieves all <see cref="Patient"/> records.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of all <see cref="Patient"/> entities.</returns>
Task<List<Patient>> FindAll();
/// <summary>
/// Asynchronously retrieves a list of patients associated with the specified point of care.
/// </summary>
/// <param name="pointOfCare">The point of care identifier used to filter patients.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of patients matching the specified point of care.</returns>
Task<List<Patient>> FindByPointOfCare(string pointOfCare);
/// <summary>
/// Asynchronously retrieves the list of patients associated with the specified point of care.
/// </summary>
/// <param name="pointOfCare">The unique identifier of the point of care used to filter patients.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of patients matching the specified point of care.</returns>
Task<List<Patient>> FindByPointOfCare(ObjectId pointOfCare);
/// <summary>
/// Retrieves a patient by their point-of-care identifier, returning <c>null</c> when no matching patient is found.
/// </summary>
/// <param name="pointOfCare">The unique identifier of the point-of-care location used to look up the patient.</param>
/// <returns>A task that resolves to the matching <see cref="Patient"/>, or <c>null</c> if no patient is associated with the specified point-of-care id.</returns>
Task<Patient?> FindByPointOfCareId(ObjectId pointOfCare);
/// <summary>
/// Asynchronously retrieves a list of patients who have been discharged.
/// </summary>
/// <returns>A task representing the asynchronous operation, containing a list of discharged <see cref="Patient"/> records.</returns>
Task<List<Patient>> FindDischargedPatients();
/// <summary>
/// Updates an existing patient record and returns the updated patient, or <see langword="null"/> if the patient was not found.
/// </summary>
/// <param name="updatedPatient">The patient containing the updated information.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated <see cref="Patient"/>, or <see langword="null"/> if no matching patient exists.</returns>
Task<Patient?> UpdateOne(Patient updatedPatient);
/// <summary>
/// Asynchronously retrieves a list of patients who have an inactive Point of Care (PoC) assignment.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="Patient"/> objects with inactive PoC status.</returns>
Task<List<Patient>> FindInActivePoC();
/// <summary>
/// Asynchronously retrieves a list of patients who are currently marked as inactive points of contact (PoC).
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of inactive PoC patients.</returns>
Task<List<Patient>> FindInInactivePoC();
/// <summary>
/// Retrieves a list of patients whose records have not been updated since the specified date.
/// </summary>
/// <param name="date">The cutoff date; patients last updated before this date will be included in the result.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="Patient"/> objects that have not been updated since the specified date.</returns>
Task<List<Patient>> FindPatientsNotUpdatedSince(DateTime date);
/// <summary>
/// Asynchronously retrieves a patient from the data store by their unique identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient to look up.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="Patient"/> if one is found; otherwise, <c>null</c>.</returns>
Task<Patient?> FindByPatientId(ObjectId patientId);
/// <summary>
/// Updates the incoming data for the specified patient and returns the updated patient record.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose incoming data should be updated.</param>
/// <param name="person">The patient object containing the incoming data to apply.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated <see cref="Patient"/>, or <c>null</c> if the patient was not found.</returns>
Task<Patient?> UpdatePatientIncomingData(ObjectId patientId, Patient person);
/// <summary>
/// Updates the demographic data of an existing patient identified by the given identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose demographic data is to be updated.</param>
/// <param name="person">The patient object containing the updated demographic information.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated <see cref="Patient"/>, or <c>null</c> if the patient was not found.</returns>
Task<Patient?> UpdatePatientDemographicData(ObjectId patientId, Patient person);
/// <summary>
/// Asynchronously retrieves the patient matching the specified unit and point of care identifier.
/// </summary>
/// <param name="unit">The identifier of the unit used to locate the patient.</param>
/// <param name="pointOfCare">The point of care identifier used together with the unit to locate the patient.</param>
/// <returns>A task that resolves to the <see cref="Patient"/> associated with the given unit and point of care.</returns>
Task<Patient> FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare);
/// <summary>
/// Asynchronously retrieves the total count of records associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The identifier of the unit whose associated records should be counted.</param>
/// <returns>A task that represents the asynchronous operation, containing the total count of matching records.</returns>
Task<long> CountByUnitId(ObjectId unitId);
/// <summary>
/// Asynchronously searches for a patient by their patient number within a specific unit, ensuring the patient is associated with a distinct unit.
/// </summary>
/// <param name="patientNumber">The unique patient number used to identify the patient.</param>
/// <param name="unitId">The identifier of the unit to constrain the search to a distinct unit context.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="Patient"/> if found; otherwise, <c>null</c>.</returns>
Task<Patient?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId);
/// <summary>
/// Retrieves a paginated, fluent query of patients based on the specified pagination filter.
/// </summary>
/// <param name="filter">The pagination filter that defines page size, page number, and additional criteria applied to the patient query.</param>
/// <returns>An <see cref="IFindFluent{Patient, Patient}"/> representing the paginated patient query that can be further composed before execution.</returns>
IFindFluent<Patient, Patient> GetPaginatedPatients(PaginationFilter filter);
/// <summary>
/// Asynchronously retrieves all patients who have completed procedures, filtering by the number of minutes that have elapsed since the procedure end date for archival purposes.
/// </summary>
/// <param name="archiveProcedureEndDateAfterMinutes">The number of minutes after the procedure end date used to determine which finished procedures are eligible for archive retrieval.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of patients with finished procedures matching the archive criteria.</returns>
Task<List<Patient>> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes);
/// <summary>
/// Asynchronously retrieves all patients whose tests have finished based on the specified archive end date threshold.
/// </summary>
/// <param name="archiveTestEndDateAfterMinutes">The number of minutes after which a test is considered finished and eligible for retrieval.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of patients with finished tests.</returns>
Task<List<Patient>> FindAllPatientWithFinishedTests(int archiveTestEndDateAfterMinutes);
/// <summary>
/// Retrieves all patients whose treatment has been finished and is eligible for archival based on the specified end date threshold.
/// </summary>
/// <param name="archiveTreatmentEndDateAfterMinutes">The number of minutes after the treatment end date used as the archival threshold.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of patients with finished treatments that meet the archival criteria.</returns>
Task<List<Patient>> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes);
/// <summary>
/// Updates a master list option for the specified units and returns the patients affected by the change.
/// </summary>
/// <param name="unitIds">The list of unit identifiers whose patients should be considered for the update.</param>
/// <param name="opt">The DTO containing the master list option update details.</param>
/// <param name="typeName">The name of the option type being updated.</param>
/// <returns>A task that resolves to the list of patients impacted by the master list option update.</returns>
Task<List<Patient>> UpdateMasterListOption(List<ObjectId> unitIds, UpdateOptionMasterListDto opt, string typeName);
/// <summary>
/// Asynchronously retrieves a list of <see cref="Patient"/> objects associated with the specified unit identifiers and patient type.
/// </summary>
/// <param name="unitIds">The list of unit identifiers used to filter the patients to be returned.</param>
/// <param name="typeName">The name of the patient type used to further refine the query results.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous operation, containing a list of <see cref="Patient"/> objects matching the provided unit identifiers and type.</returns>
Task<List<Patient>> GetPatientsByUnitIds(List<ObjectId> unitIds, string typeName);
/// <summary>
/// Deletes the specified master list option and returns the patients affected by its removal.
/// </summary>
/// <param name="unitIds">The identifiers of the units whose master list option should be deleted.</param>
/// <param name="opt">The master list option to remove.</param>
/// <param name="typeName">The name of the master list type to which the option belongs.</param>
/// <returns>A task that yields the collection of patients impacted by deleting the master list option.</returns>
Task<IEnumerable<Patient>> DeleteMasterListOption(List<ObjectId> unitIds, OptionList opt, string typeName);
}
@@ -4,6 +4,16 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPoCMappingRepository
{
/// <summary>
/// Asynchronously retrieves a <see cref="PoCMapping"/> associated with the specified key.
/// Returns <c>null</c> when no mapping exists for the given key.
/// </summary>
/// <param name="key">The key used to look up the <see cref="PoCMapping"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="PoCMapping"/> if found, or <c>null</c> if no mapping exists for the specified key.</returns>
Task<PoCMapping?> FindByKey(string key);
/// <summary>
/// Retrieves the name of the collection associated with the current context or entity.
/// </summary>
/// <returns>The collection name as a string.</returns>
string GetCollectionName();
}
@@ -6,13 +6,36 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPoCSettingsRepository : IMongoRepository<PoCSettings>
{
/// <summary>
/// Asynchronously deletes the entity identified by the specified <see cref="ObjectId"/>.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the entity to delete.</param>
Task Delete(ObjectId id);
/// <summary>
/// Asynchronously retrieves the Point of Care (PoC) settings associated with the specified patient location.
/// </summary>
/// <param name="location">The patient location used to look up the corresponding PoC settings.</param>
/// <returns>A task that returns the matching <see cref="PoCSettings"/> if found; otherwise, <c>null</c>.</returns>
Task<PoCSettings?> FindByLocation(PatientLocation location);
/// <summary>
/// Retrieves the PoC (Proof of Concept) settings associated with the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the PoC settings to retrieve.</param>
/// <returns>A task that represents the asynchronous operation. The result contains the matching <see cref="PoCSettings"/> if found, or <c>null</c> when no settings exist for the given identifier.</returns>
Task<PoCSettings?> FindById(ObjectId id);
/// <summary>
/// Updates the PoC (Proof of Concept) settings with the provided values.
/// </summary>
/// <param name="pocSettings">The PoC settings to be applied.</param>
/// <returns>A task that represents the asynchronous update operation.</returns>
Task Update(PoCSettings pocSettings);
/// <summary>
/// Asynchronously retrieves all <see cref="PoCSettings"/> records.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of all <see cref="PoCSettings"/> entries.</returns>
Task<List<PoCSettings>> FindAll();
}
@@ -10,49 +10,183 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPointOfCareRepository : IMongoRepository<PointOfCare>
{
/// <summary>
/// Asynchronously deletes the entity identified by the specified <paramref name="id"/>.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the entity to delete.</param>
Task Delete(ObjectId id);
/// <summary>
/// Asynchronously updates the specified <see cref="PointOfCare"/> entity.
/// </summary>
/// <param name="pointOfCare">The <see cref="PointOfCare"/> instance containing the updated data to be persisted.</param>
Task Update(PointOfCare pointOfCare);
/// <summary>
/// Updates the unit identifier associated with the specified object.
/// </summary>
/// <param name="id">The identifier of the object whose unit will be updated.</param>
/// <param name="unit">The unit value to apply to the object.</param>
Task UpdateUnitId(ObjectId id, Unit unit);
/// <summary>
/// Updates the point of care configuration identified by the specified identifier with the provided settings.
/// </summary>
/// <param name="id">The unique identifier of the point of care configuration to update.</param>
/// <param name="configuration">The new configuration values to apply to the existing point of care record.</param>
Task UpdateConfiguration(ObjectId id, PointOfCareConfiguration configuration);
/// <summary>
/// Retrieves a <see cref="PointOfCare"/> by its identifier, returning <c>null</c> when no matching record is found.
/// </summary>
/// <param name="id">The unique identifier of the point of care to locate.</param>
/// <returns>A task that yields the matching <see cref="PointOfCare"/>, or <c>null</c> if no record exists for the given identifier.</returns>
Task<PointOfCare?> FindById(ObjectId id);
/// <summary>
/// Retrieves all <see cref="PointOfCare"/> records associated with the specified unit identifier.
/// </summary>
/// <param name="unit">The <see cref="ObjectId"/> of the unit whose points of care should be returned.</param>
/// <returns>A task that yields an <see cref="IEnumerable{T}"/> of <see cref="PointOfCare"/> for the matching unit, or <c>null</c> when no results are found.</returns>
Task<IEnumerable<PointOfCare>?> FindAllByUnitId(ObjectId unit);
/// <summary>
/// Asynchronously retrieves the points of care associated with the specified room.
/// </summary>
/// <param name="room">The room identifier used to look up the corresponding points of care.</param>
/// <returns>A task that represents the asynchronous operation. The result is a collection of <see cref="PointOfCare"/> matching the specified room, or <see langword="null"/> if no matching points of care are found.</returns>
Task<IEnumerable<PointOfCare>?> FindByRoom(string room);
/// <summary>
/// Asynchronously retrieves a list of <see cref="PointOfCare"/> entities that match the specified filter, optionally applying a projection to shape the returned fields.
/// </summary>
/// <param name="filter">The filter definition used to match <see cref="PointOfCare"/> entities in the data store.</param>
/// <param name="projection">An optional projection definition that limits or shapes the fields returned in each result. If <c>null</c>, the full <see cref="PointOfCare"/> entity is returned.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PointOfCare"/> entities that satisfy the filter. Returns an empty list when no matching entities are found.</returns>
Task<List<PointOfCare>> FindByFilter(FilterDefinition<PointOfCare> filter,
ProjectionDefinition<PointOfCare>? projection = null);
ProjectionDefinition<PointOfCare>? projection = null);
/// <summary>
/// Retrieves the points of care associated with the specified bed, returning <c>null</c> when no matching points of care are found.
/// </summary>
/// <param name="bed">The bed identifier used to look up the associated points of care.</param>
/// <returns>A task that yields an <see cref="IEnumerable{T}"/> of <see cref="PointOfCare"/> when matches exist, or <c>null</c> if none are found.</returns>
Task<IEnumerable<PointOfCare>?> FindByBed(string bed);
/// <summary>
/// Asynchronously retrieves all available points of care.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result is a list of <see cref="PointOfCare"/> instances, or <c>null</c> if no points of care are available.</returns>
Task<List<PointOfCare>?> GetAll();
/// <summary>
/// Retrieves all Point of Care configurations available in the system.
/// </summary>
/// <returns>A task containing a list of <see cref="PointOfCare"/> configurations, or <c>null</c> if no configurations are found.</returns>
Task<List<PointOfCare>?> GetAllConfigs();
/// <summary>
/// Retrieves all Point of Care location information.
/// </summary>
/// <returns>A task that resolves to a list of <see cref="PointOfCare"/> locations, or <c>null</c> if no location data is available.</returns>
Task<List<PointOfCare>?> GetAllLocationInfo();
/// <summary>
/// Retrieves the Point of Care configuration associated with the specified identifier.
/// Returns null when no configuration exists for the given identifier.
/// </summary>
/// <param name="pocId">The unique identifier of the Point of Care whose configuration is being requested.</param>
/// <returns>A task that resolves to the matching <see cref="PointOfCare"/> configuration, or null if no configuration is found.</returns>
Task<PointOfCare?> GetPoCConfiguration(ObjectId pocId);
/// <summary>
/// Asynchronously retrieves the <see cref="PointOfCare"/> associated with the specified bed within the given unit, returning <see langword="null"/> if no matching record is found.
/// </summary>
/// <param name="bed">The bed identifier used to locate the point of care; may be <see langword="null"/>.</param>
/// <param name="unitId">The identifier of the unit in which the bed is located.</param>
/// <returns>A <see cref="Task{TResult}"/> that yields the matching <see cref="PointOfCare"/>, or <see langword="null"/> if none is found.</returns>
Task<PointOfCare?> FindByBedAndUnitId(string? bed, ObjectId unitId);
/// <summary>
/// Retrieves the point of care associated with the specified patient location.
/// </summary>
/// <param name="patientLocation">The patient location used to look up the corresponding point of care.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="PointOfCare"/>, or <c>null</c> if no point of care is found for the given patient location.</returns>
Task<PointOfCare?> FindByPatientLocation(PatientLocation patientLocation);
/// <summary>
/// Retrieves a collection of <see cref="PointOfCare"/> entities that belong to the specified unit and match the given status.
/// When <paramref name="excludeVirtual"/> is <c>true</c>, virtual points of care are omitted from the results.
/// </summary>
/// <param name="unitId">The identifier of the unit whose points of care are being queried.</param>
/// <param name="status">The point-of-care status used to filter the results.</param>
/// <param name="excludeVirtual">When set to <c>true</c>, virtual points of care are excluded from the returned collection.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="IEnumerable{PointOfCare}"/> of points of care matching the specified unit and status.</returns>
Task<IEnumerable<PointOfCare>> FindByUnitAndStatus(ObjectId unitId, StatusEnum.PointOfCare status,
bool excludeVirtual = false);
bool excludeVirtual = false);
/// <summary>
/// Updates the relay configuration associated with the specified point of contact identifier.
/// </summary>
/// <param name="pocId">The unique identifier of the point of contact whose relay configuration is being updated.</param>
/// <param name="relayConfig">The collection of relay entries to apply to the configuration.</param>
Task UpdateRelayConfig(ObjectId pocId, List<Relay> relayConfig);
/// <summary>
/// Updates the relay configuration for the specified point of configuration (POC) using the provided list of relay configuration identifiers.
/// </summary>
/// <param name="pocId">The unique identifier of the point of configuration whose relay configuration will be updated.</param>
/// <param name="relayConfig">The list of relay configuration identifiers to apply to the specified POC.</param>
Task UpdateRelayConfig(ObjectId pocId, List<ObjectId> relayConfig);
/// <summary>
/// Retrieves a paginated, fluent queryable collection of Points of Care (PoCs) based on the provided filter criteria.
/// </summary>
/// <param name="filter">The pagination filter that defines paging parameters such as page number and page size.</param>
/// <returns>An <see cref="IFindFluent{PointOfCare, PointOfCare}"/> representing the paginated query against the PoCs collection.</returns>
IFindFluent<PointOfCare, PointOfCare> GetPaginatedPoCs(PaginationFilter filter);
/// <summary>
/// Asynchronously counts the number of entities associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The identifier of the unit used to filter the entities to be counted.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous operation, containing the total count of matching entities.</returns>
Task<long> CountByUnitId(ObjectId unitId);
/// <summary>
/// Asynchronously counts the number of virtuals associated with the specified unit.
/// </summary>
/// <param name="unitId">The identifier of the unit whose virtuals will be counted.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the count of virtuals for the given unit.</returns>
Task<long> CountVirtualsByUnitId(ObjectId unitId);
/// <summary>
/// Deletes multiple records associated with the specified unit identifier asynchronously.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose related records should be deleted.</param>
/// <returns>A task that represents the asynchronous operation, containing a boolean value indicating whether the deletion was successful.</returns>
Task<bool> DeleteManyByUnitId(ObjectId unitId);
/// <summary>
/// Retrieves a <see cref="PointOfCare"/> entity by its identifier, including all associated configuration data.
/// Returns <see langword="null"/> when no matching entity is found.
/// </summary>
/// <param name="id">The unique identifier of the point of care to retrieve.</param>
/// <returns>A task that resolves to the matching <see cref="PointOfCare"/> with all configuration, or <see langword="null"/> if not found.</returns>
Task<PointOfCare?> FindByIdAllConfig(ObjectId id);
/// <summary>
/// Asynchronously retrieves the set of camera identifiers that are currently in use.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a hash set of <see cref="ObjectId"/> values for all cameras in use.</returns>
Task<HashSet<ObjectId>> FindAllIdCamerasInUse();
/// <summary>
/// Asynchronously retrieves all ID relays that are currently in use, returning them as a hash set for efficient lookup.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a hash set of the <see cref="ObjectId"/> values of all ID relays currently in use.</returns>
Task<HashSet<ObjectId>> FindAllIdRelaysInUse();
/// <summary>
/// Asynchronously retrieves the set of all ID beacons that are currently in use.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="HashSet{ObjectId}"/> with the identifiers of all ID beacons currently in use.</returns>
Task<HashSet<ObjectId>> FindAllIdBeaconsInUse();
/// <summary>
/// Retrieves all points of care associated with the specified unit identifier, including their related devices.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose points of care and associated devices are to be retrieved.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an enumerable collection of points of care with their associated devices, or <c>null</c> if no points of care are found for the specified unit.</returns>
Task<IEnumerable<PointOfCare>?> FindAllByUnitIdWithDevices(ObjectId unitId);
}
@@ -5,18 +5,47 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPumpAlarmEventRepository
{
/// <summary>
/// Asynchronously inserts a pump alarm event into the underlying data store.
/// </summary>
/// <param name="alarmEvent">The pump alarm event to be persisted.</param>
Task InsertAsync(PumpAlarmEvent alarmEvent);
/// <summary>
/// Retrieves pump alarm events associated with the specified device, optionally filtered by a date range and capped by a maximum result count.
/// </summary>
/// <param name="deviceId">The unique identifier of the device whose alarm events should be returned.</param>
/// <param name="from">Optional start of the date range used to filter the events.</param>
/// <param name="to">Optional end of the date range used to filter the events.</param>
/// <param name="limit">Optional maximum number of alarm events to return.</param>
/// <returns>A task that represents the asynchronous operation, containing the collection of pump alarm events matching the criteria.</returns>
Task<IEnumerable<PumpAlarmEvent>> FindByDeviceIdAsync(
string deviceId,
DateTime? from = null,
DateTime? to = null,
int? limit = null);
string deviceId,
DateTime? from = null,
DateTime? to = null,
int? limit = null);
/// <summary>
/// Asynchronously retrieves the most recent pump alarm event associated with the specified device identifier.
/// Returns null if no alarm event is found for the given device.
/// </summary>
/// <param name="deviceId">The unique identifier of the device whose last pump alarm event is to be retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing the most recent <see cref="PumpAlarmEvent"/> for the device, or null if none exists.</returns>
Task<PumpAlarmEvent?> FindLastByDeviceIdAsync(string deviceId);
// cleanup
/// <summary>
/// Deletes records associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose records should be deleted.</param>
Task DeleteByPatientId(ObjectId patientId);
/// <summary>
/// Asynchronously updates the ObjectId value of multiple records where the specified field currently matches the old identifier, replacing it with the new identifier.
/// </summary>
/// <param name="fieldName">The name of the field whose value should be updated.</param>
/// <param name="newId">The new ObjectId value to assign to the matching records.</param>
/// <param name="oldId">The existing ObjectId value used to identify the records to be updated.</param>
/// <returns>A task that represents the asynchronous operation, containing the number of records that were updated.</returns>
Task<long> UpdateManyObjectIdByFiledNameAsync(string fieldName, ObjectId newId, ObjectId oldId);
}
@@ -6,22 +6,56 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPumpAlarmStateRepository
{
/// <summary>
/// Asynchronously retrieves the active alarm state for a specified device, optionally filtered by alarm type and alarm code.
/// Returns <c>null</c> when no matching active alarm state is found.
/// </summary>
/// <param name="deviceId">The identifier of the device whose active alarm state is being queried.</param>
/// <param name="alarmType">The optional alarm type to filter the search by; if <c>null</c>, no alarm type filter is applied.</param>
/// <param name="alarmCodeMdc">The optional MDC alarm code to further filter the search; if <c>null</c>, no alarm code filter is applied.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the active <see cref="PumpAlarmState"/>, or <c>null</c> if no matching active alarm state exists.</returns>
Task<PumpAlarmState?> FindActiveAsync(
string deviceId,
PumpEnum.AlarmType? alarmType,
string? alarmCodeMdc = null);
string deviceId,
PumpEnum.AlarmType? alarmType,
string? alarmCodeMdc = null);
/// <summary>
/// Asynchronously retrieves all active pump alarm states associated with the specified device.
/// </summary>
/// <param name="deviceId">The unique identifier of the device whose active pump alarm states are being queried.</param>
/// <returns>A task representing the asynchronous operation, containing a collection of active <see cref="PumpAlarmState"/> entries for the given device.</returns>
Task<IEnumerable<PumpAlarmState>> FindAllActiveByDeviceAsync(string deviceId);
/// <summary>
/// Inserts a new active pump alarm state or updates the existing one, maintaining the current state for the alarm.
/// </summary>
/// <param name="state">The pump alarm state to upsert as the active record.</param>
Task UpsertActiveAsync(PumpAlarmState state);
/// <summary>
/// Asynchronously removes alarm records associated with the specified device, optionally filtered by alarm type and alarm code.
/// </summary>
/// <param name="deviceId">The identifier of the device whose alarms should be removed.</param>
/// <param name="alarmType">The optional alarm type to filter the removal; when null, all alarm types are considered.</param>
/// <param name="alarmCodeMdc">The optional alarm code (MDC) to further filter the removal; when null, no code filter is applied.</param>
Task RemoveAsync(
string deviceId,
PumpEnum.AlarmType? alarmType,
string? alarmCodeMdc = null);
string deviceId,
PumpEnum.AlarmType? alarmType,
string? alarmCodeMdc = null);
//cleanup
/// <summary>
/// Deletes records associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose related records should be removed.</param>
Task DeleteByPatientId(ObjectId patientId);
/// <summary>
/// Asynchronously updates the <see cref="ObjectId"/> value in multiple documents identified by the specified field name, replacing <paramref name="oldId"/> with <paramref name="newId"/>, and returns the number of documents affected.
/// </summary>
/// <param name="fieldName">The name of the field whose value will be updated.</param>
/// <param name="newId">The new <see cref="ObjectId"/> value to assign.</param>
/// <param name="oldId">The existing <see cref="ObjectId"/> value to be replaced.</param>
/// <returns>A <see cref="Task{T}"/> that represents the asynchronous operation. The task result contains the number of documents updated.</returns>
Task<long> UpdateManyObjectIdByFieldNameAsync(string fieldName, ObjectId newId, ObjectId oldId);
}
@@ -5,24 +5,82 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPumpObservationRepository
{
/// <summary>
/// Asynchronously inserts a pump observation into the underlying data store.
/// </summary>
/// <param name="obs">The pump observation entity to persist.</param>
Task InsertAsync(PumpObservation obs);
/// <summary>
/// Asynchronously inserts a collection of pump observations into the underlying data store.
/// </summary>
/// <param name="observations">The collection of <see cref="PumpObservation"/> records to be persisted.</param>
Task InsertManyAsync(IEnumerable<PumpObservation> observations);
/// <summary>
/// Asynchronously retrieves pump observations associated with the specified device identifier, optionally filtered by a time range and limited in count.
/// </summary>
/// <param name="deviceId">The unique identifier of the device whose pump observations are being queried.</param>
/// <param name="from">Optional inclusive lower bound for the observation timestamp; when null, no lower bound is applied.</param>
/// <param name="to">Optional inclusive upper bound for the observation timestamp; when null, no upper bound is applied.</param>
/// <param name="limit">Optional maximum number of observations to return; when null, all matching observations are returned.</param>
/// <returns>A task that represents the asynchronous operation, yielding an enumerable collection of <see cref="PumpObservation"/> instances matching the criteria.</returns>
Task<IEnumerable<PumpObservation>> FindByDeviceIdAsync(
string deviceId,
DateTime? from = null,
DateTime? to = null,
int? limit = null);
string deviceId,
DateTime? from = null,
DateTime? to = null,
int? limit = null);
/// <summary>
/// Asynchronously retrieves the most recent <see cref="PumpObservation"/> associated with the specified device identifier.
/// </summary>
/// <param name="deviceId">The unique identifier of the device whose last observation is being queried.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the last <see cref="PumpObservation"/> for the device, or <c>null</c> if no observation exists for the given device.</returns>
Task<PumpObservation?> FindLastByDeviceIdAsync(string deviceId);
/// <summary>
/// Asynchronously retrieves the most recent observation time for all patients, returning a mapping of patient identifiers to their last observation timestamps.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a dictionary where each key is a patient <see cref="ObjectId"/> and the associated value is the <see cref="DateTime"/> of that patient's last observation.</returns>
Task<Dictionary<ObjectId, DateTime>> FindAllLastPatientObservationTimeAsync();
/// <summary>
/// Asynchronously retrieves the pump observations associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The identifier of the patient whose pump observations are being queried. May be <see langword="null"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the collection of <see cref="PumpObservation"/> items found for the patient, or an empty collection if no observations are found.</returns>
Task<IEnumerable<PumpObservation>> FindByPatientId(ObjectId? patientId);
/// <summary>
/// Retrieves the most recent aggregated pump observations for a specified patient, ordered from newest to oldest.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being queried.</param>
/// <param name="num">The maximum number of observations to return. Defaults to 100.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of the patient's most recent <see cref="PumpObservation"/> entries.</returns>
Task<List<PumpObservation>> AggregatedPatientLastObservations(ObjectId patientId, int num = 100);
/// <summary>
/// Deletes all records associated with the specified patient identifier. If the patient identifier is null, no deletion is performed.
/// </summary>
/// <param name="patientId">The optional patient identifier whose related records should be removed.</param>
Task DeleteByPatientId(ObjectId? patientId);
/// <summary>
/// Asynchronously deletes records older than the specified number of days, optionally filtered by name, and returns the number of items removed.
/// </summary>
/// <param name="days">The age threshold in days; only records older than this value will be deleted.</param>
/// <param name="name">An optional name used to filter which records are targeted for deletion. If null, deletion applies to all records regardless of name.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous operation, containing the total count of deleted records.</returns>
Task<long> DeleteOlderThanDaysAsync(int days, string? name = null);
/// <summary>
/// Asynchronously deletes items, retaining only the most recent <paramref name="maxCount"/> entries, and returns the number of items that were removed.
/// </summary>
/// <param name="maxCount">The maximum number of most recent items to keep after the deletion.</param>
/// <returns>A task representing the asynchronous operation, containing the count of items that were deleted.</returns>
Task<long> DeleteKeepLastNAsync(int maxCount);
/// <summary>
/// Asynchronously updates the ObjectId value of the specified field for multiple records, replacing the existing value (optionally matched by <paramref name="oldId"/>) with <paramref name="newId"/>.
/// </summary>
/// <param name="fieldName">The name of the field whose ObjectId value will be updated.</param>
/// <param name="newId">The new ObjectId value to assign to the field.</param>
/// <param name="oldId">The optional current ObjectId value used as a filter; when <see langword="null"/>, the update is applied without filtering by the previous value.</param>
/// <returns>A <see cref="Task{T}"/> that represents the asynchronous operation, containing the number of records updated.</returns>
Task<long> UpdateManyObjectIdByFieldAsync(string fieldName, ObjectId newId, ObjectId? oldId);
}
@@ -4,9 +4,23 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IPumpStateRepository
{
/// <summary>
/// Asynchronously retrieves the pump state associated with the specified device identifier.
/// Returns null when no matching pump state is found for the given device identifier.
/// </summary>
/// <param name="deviceId">The unique identifier of the device whose pump state is being looked up.</param>
/// <returns>A task that resolves to the PumpState if a match is found, or null if no pump state exists for the specified device.</returns>
Task<PumpState?> FindByDeviceIdAsync(string deviceId);
/// <summary>
/// Asynchronously creates or updates a pump state record in the data store.
/// </summary>
/// <param name="state">The pump state to be inserted if it does not exist, or updated if it already exists.</param>
Task UpsertAsync(PumpState state);
/// <summary>
/// Asynchronously retrieves all available pump states.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of all <see cref="PumpState"/> objects.</returns>
Task<IEnumerable<PumpState>> GetAllAsync();
}
@@ -4,7 +4,20 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IRecordingAlertArchiveRepository : IMongoRepository<PatientRecordingAlert>
{
/// <summary>
/// Inserts a new patient recording alert into the underlying data store asynchronously.
/// </summary>
/// <param name="recordingAlert">The patient recording alert to be inserted.</param>
new Task InsertOneAsync(PatientRecordingAlert recordingAlert);
/// <summary>
/// Asynchronously deletes records that have a date earlier than the specified threshold.
/// </summary>
/// <param name="date">The cutoff date; records with a date value before this will be removed.</param>
Task DeleteBeforeDate(DateTime date);
/// <summary>
/// Inserts a batch of <see cref="PatientRecordingAlert"/> entities into the data store in a single operation.
/// </summary>
/// <param name="recordingAlerts">The collection of patient recording alerts to insert.</param>
/// <returns>A task that represents the asynchronous batch insert operation, containing the number of affected or inserted records.</returns>
Task<long> InsertBatch(IEnumerable<PatientRecordingAlert> recordingAlerts);
}
@@ -6,13 +6,56 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IRecordingAlertRepository : IMongoRepository<PatientRecordingAlert>
{
/// <summary>
/// Retrieves an aggregated list of the most recent patient recording alerts for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose recent alerts are being retrieved.</param>
/// <param name="num">The maximum number of recent observations to return.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientRecordingAlert"/> entries for the patient's last observations.</returns>
Task<List<PatientRecordingAlert>> AggregatedPatientLastObservations(ObjectId patientId, int num);
/// <summary>
/// Retrieves the most recent observations of a specified type for a given patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are queried.</param>
/// <param name="name">The name of the observation to filter by.</param>
/// <param name="num">The maximum number of recent observations to return. Defaults to 2.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="PatientRecordingAlert"/> entries representing the matching observations.</returns>
Task<List<PatientRecordingAlert>> FindLastObservations(ObjectId patientId, string name, int num = 2);
/// <summary>
/// Asynchronously inserts a single <see cref="PatientRecordingAlert"/> into the underlying data store.
/// </summary>
/// <param name="patientRecordingAlert">The patient recording alert to be inserted.</param>
new Task InsertOneAsync(PatientRecordingAlert patientRecordingAlert);
/// <summary>
/// Asynchronously deletes records older than the specified number of days, identified by the given name.
/// </summary>
/// <param name="name">The identifier or key used to locate the records to be evaluated for deletion.</param>
/// <param name="value">The age threshold, in days, used to determine which records are considered older and eligible for deletion.</param>
Task DeleteOlderDaysAsync(string name, int value);
/// <summary>
/// Asynchronously deletes a number record matching the specified name and value.
/// </summary>
/// <param name="name">The name associated with the number to delete.</param>
/// <param name="value">The numeric value of the record to delete.</param>
/// <returns>A task that represents the asynchronous delete operation.</returns>
Task DeleteOlderNumberAsync(string name, int value);
/// <summary>
/// Asynchronously deletes records associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique <see cref="ObjectId"/> of the patient whose related records should be removed.</param>
Task DeleteByPatientId(ObjectId patientId);
/// <summary>
/// Updates many records by replacing the specified <paramref name="oldId"/> with the new <paramref name="id"/> within the collection identified by <paramref name="nameId"/>.
/// </summary>
/// <param name="nameId">The name or identifier of the collection in which the update is performed.</param>
/// <param name="id">The new ObjectId to assign to the matching records.</param>
/// <param name="oldId">The existing ObjectId used to locate the records to be updated.</param>
Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId);
/// <summary>
/// Asynchronously retrieves all patient recording alerts associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose recording alerts are being queried.</param>
/// <returns>A task that represents the asynchronous operation, containing an asynchronous cursor over the matching <see cref="PatientRecordingAlert"/> documents.</returns>
Task<IAsyncCursor<PatientRecordingAlert>> FindByPatientIdAsync(ObjectId patientId);
}
@@ -8,11 +8,48 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IRelayRepository : IMongoRepository<Relay>
{
/// <summary>
/// Asynchronously retrieves a relay entity by its unique identifier.
/// </summary>
/// <param name="relayId">The unique identifier of the relay to retrieve.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="Relay"/> if found; otherwise, <c>null</c>.</returns>
Task<Relay?> GetById(ObjectId relayId);
/// <summary>
/// Retrieves the relays of the specified type from the provided configuration relay list.
/// </summary>
/// <param name="configurationRelayList">The list of relay object identifiers to search through.</param>
/// <param name="type">The relay type used to filter the configuration relay list.</param>
/// <returns>A list of relays matching the specified type; an empty list if no relays of that type are found.</returns>
List<Relay> GetRelayByTypeInList(List<ObjectId> configurationRelayList, RelayEnum.Type type);
/// <summary>
/// Retrieves the <see cref="Relay"/> objects that correspond to the supplied configuration relay identifiers.
/// </summary>
/// <param name="configurationRelayList">The list of <see cref="ObjectId"/> values identifying the configuration relays to look up.</param>
/// <returns>A <see cref="List{Relay}"/> containing the relays that match the provided configuration relay identifiers.</returns>
List<Relay> GetRelayInList(List<ObjectId> configurationRelayList);
/// <summary>
/// Retrieves a paginated, filterable query of relays based on the specified pagination filter.
/// </summary>
/// <param name="filter">The pagination filter used to control paging and additional query criteria for the relay lookup.</param>
/// <returns>An <see cref="IFindFluent{Relay, Relay}"/> representing the paginated query that can be further refined and executed.</returns>
IFindFluent<Relay, Relay> GetPaginatedRelays(PaginationFilter filter);
/// <summary>
/// Asynchronously inserts a single <see cref="Relay"/> record and returns the inserted entity, or <see langword="null"/> if the insertion did not produce a result.
/// </summary>
/// <param name="request">The <see cref="Relay"/> entity to be inserted.</param>
/// <returns>A task that represents the asynchronous insert operation. The result is the inserted <see cref="Relay"/>, or <see langword="null"/> when no relay is returned.</returns>
Task<Relay?> InsertOneRelayAsync(Relay request);
/// <summary>
/// Updates an existing relay identified by the specified object identifier.
/// </summary>
/// <param name="objectId">The unique identifier of the relay to update.</param>
/// <param name="relay">The relay object containing the updated information.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated relay, or <see langword="null"/> if no relay with the specified identifier was found.</returns>
Task<Relay?> UpdateRelayAsync(ObjectId objectId, Relay relay);
/// <summary>
/// Retrieves a relay by its name asynchronously, returning null if no matching relay is found.
/// </summary>
/// <param name="requestRelayName">The name of the relay to look up. May be null.</param>
/// <returns>A task that represents the asynchronous lookup. The task result contains the matching relay, or null if no relay with the specified name exists.</returns>
Task<Relay?> GetByName(string? requestRelayName);
}
@@ -5,17 +5,57 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface ISectionRepository : IMongoRepository<Section>
{
/// <summary>
/// Retrieves a list of sections associated with the specified patient location.
/// </summary>
/// <param name="location">The patient location used to filter the sections.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the list of sections matching the specified patient location.</returns>
Task<List<Section>> FindByLocation(PatientLocation location);
/// <summary>
/// Asynchronously retrieves a <see cref="Section"/> by its identifier, returning <c>null</c> when no matching section is found.
/// </summary>
/// <param name="id">The identifier of the section to look up.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="Section"/>, or <c>null</c> if no section with the specified identifier exists.</returns>
Task<Section?> FindById(object id);
/// <summary>
/// Asynchronously retrieves the <see cref="Section"/> that matches the specified identifier.
/// Returns <c>null</c> when no matching section is found.
/// </summary>
/// <param name="id">The unique identifier of the section to look up.</param>
/// <returns>A <see cref="Task{Section}"/> that resolves to the matching <see cref="Section"/>, or <c>null</c> if no section is found.</returns>
Task<Section?> FindById(string id);
/// <summary>
/// Asynchronously retrieves a <see cref="Section"/> that matches the specified section identifier.
/// </summary>
/// <param name="section">The section name or identifier used to look up the matching <see cref="Section"/>.</param>
/// <returns>A <see cref="Task{TResult}"/> that yields the matching <see cref="Section"/>, or <c>null</c> if no section is found.</returns>
Task<Section?> FindBySection(string section);
/// <summary>
/// Asynchronously retrieves the <see cref="Section"/> associated with the specified point of care.
/// </summary>
/// <param name="pointOfCare">The point of care identifier used to look up the section.</param>
/// <returns>A task that returns the matching <see cref="Section"/>, or <c>null</c> if no section is found for the given point of care.</returns>
Task<Section?> FindByPointOfCare(string pointOfCare);
/// <summary>
/// Asynchronously retrieves all sections.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of all <see cref="Section"/> items.</returns>
Task<List<Section>> GetAll();
/// <summary>
/// Updates the specified section in the system and returns the updated entity.
/// </summary>
/// <param name="section">The section entity containing the updated information.</param>
/// <returns>A task that represents the asynchronous update operation, containing the updated section, or null if the section was not found.</returns>
Task<Section?> UpdateSection(Section section);
//Task<Section?> UpdateSectionItems(Section section);
//Task<Section?> UpdateSectionConfig(Section section);
//Section UpdateSectionItems(string sectionId, string group, string boxName, BoxResponse boxUpdated);
/// <summary>
/// Inserts a single <see cref="Section"/> into the underlying data store and returns the inserted entity, or <see langword="null"/> if the operation did not produce a result.
/// </summary>
/// <param name="section">The <see cref="Section"/> instance to be inserted.</param>
/// <returns>A <see cref="Task{TResult}"/> that resolves to the inserted <see cref="Section"/>, or <see langword="null"/> if no section was inserted.</returns>
Task<Section?> InsertOneSection(Section section);
}
@@ -5,6 +5,17 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IServiceConfigRepository : IMongoRepository<ServiceConfig>
{
/// <summary>
/// Asynchronously retrieves a <see cref="ServiceConfig"/> by its unique <see cref="ObjectId"/>.
/// Returns <c>null</c> when no matching service configuration is found.
/// </summary>
/// <param name="oid">The unique identifier of the service configuration to look up.</param>
/// <returns>A <see cref="ServiceConfig"/> matching the provided identifier, or <c>null</c> if no configuration exists for that id.</returns>
Task<ServiceConfig?> FindById(ObjectId oid);
/// <summary>
/// Retrieves a <see cref="ServiceConfig"/> by its identifier, returning <see langword="null"/> when no matching configuration is found.
/// </summary>
/// <param name="id">The unique identifier of the service configuration to look up.</param>
/// <returns>A task that resolves to the matching <see cref="ServiceConfig"/>, or <see langword="null"/> if no configuration exists for the given id.</returns>
Task<ServiceConfig?> FindById(string id);
}
@@ -5,9 +5,27 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface ITreatmentArchiveRepository
{
/// <summary>
/// Asynchronously inserts a new patient treatment record into the underlying data store.
/// </summary>
/// <param name="patientTreatment">The patient treatment entity to insert.</param>
Task InsertOneAsync(PatientTreatment patientTreatment);
/// <summary>
/// Asynchronously deletes items whose associated date is earlier than the specified cutoff date.
/// </summary>
/// <param name="date">The cutoff date; items dated before this value are removed.</param>
Task DeleteBeforeDate(DateTime date);
/// <summary>
/// Asynchronously inserts a batch of <see cref="PatientTreatment"/> records.
/// </summary>
/// <param name="treatments">The collection of patient treatment entities to be inserted.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="long"/> value associated with the batch insertion.</returns>
Task<long> InsertBatch(IEnumerable<PatientTreatment> treatments);
/// <summary>
/// Asynchronously retrieves all patient treatment records associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose treatments are being queried.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientTreatment"/> records for the given patient.</returns>
Task<List<PatientTreatment>> FindAllFromPatient(ObjectId patientId);
}
@@ -7,17 +7,80 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface ITreatmentRepository : IMongoRepository<PatientTreatment>
{
/// <summary>
/// Retrieves the collection of <see cref="PatientTreatment"/> records associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose treatments are being queried.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="IEnumerable{PatientTreatment}"/> of treatments for the patient; the collection is empty if no treatments are found.</returns>
Task<IEnumerable<PatientTreatment>> GetByPatientId(ObjectId patientId);
/// <summary>
/// Asynchronously retrieves a patient treatment record from the data store that matches the specified identifier.
/// Returns a cursor over the matching <see cref="PatientTreatment"/> document, or an empty cursor if no record is found.
/// </summary>
/// <param name="id">The unique <see cref="ObjectId"/> of the patient treatment record to retrieve.</param>
/// <returns>A <see cref="Task{IAsyncCursor{PatientTreatment}}"/> that yields a cursor containing the matching patient treatment, if any.</returns>
Task<IAsyncCursor<PatientTreatment>> GetById(ObjectId id);
/// <summary>
/// Asynchronously inserts a new <see cref="PatientTreatment"/> record into the data store.
/// </summary>
/// <param name="treatment">The patient treatment entity to be persisted.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous insert operation.</returns>
new Task InsertOneAsync(PatientTreatment treatment);
/// <summary>
/// Asynchronously deletes the entity identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the entity to delete.</param>
new Task DeleteAsync(ObjectId id);
/// <summary>
/// Updates an existing <see cref="PatientTreatment"/> record in the data store asynchronously.
/// </summary>
/// <param name="treatment">The <see cref="PatientTreatment"/> entity containing the updated information.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous update operation. The task result contains <c>true</c> if the update was successful; otherwise, <c>false</c>.</returns>
Task<bool> Update(PatientTreatment treatment);
/// <summary>
/// Asynchronously retrieves the collection of <see cref="PatientTreatment"/> records associated with the specified patient identifier, returning the results as a cursor for streaming access.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose treatments are being queried.</param>
/// <returns>A task that yields an <see cref="IAsyncCursor{TDocument}"/> of <see cref="PatientTreatment"/> records matching the given patient identifier.</returns>
Task<IAsyncCursor<PatientTreatment>> FindByPatientIdAsync(ObjectId patientId);
/// <summary>
/// Asynchronously retrieves the collection of patient treatments associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose treatments are being queried.</param>
/// <returns>A task that represents the asynchronous operation, containing an enumerable collection of <see cref="PatientTreatment"/> records for the given patient.</returns>
Task<IEnumerable<PatientTreatment>> FindByPatientId(ObjectId patientId);
/// <summary>
/// Asynchronously deletes a record associated with the specified patient identifier.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose record should be deleted.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the record was successfully deleted; otherwise, <c>false</c>.</returns>
Task<bool> DeleteByPatientId(ObjectId patientId);
/// <summary>
/// Asynchronously retrieves the list of bolus treatments associated with the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose bolus treatments are being queried.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientTreatment"/> entries representing the patient's bolus treatments.</returns>
Task<List<PatientTreatment>> FindBolusTreatments(ObjectId patientId);
/// <summary>
/// Updates many records that reference a specific ObjectId, replacing the existing ObjectId with a new one.
/// Typically used to remap references from <paramref name="oldId"/> to <paramref name="id"/> for the field identified by <paramref name="nameId"/>.
/// </summary>
/// <param name="nameId">The name of the field/property that contains the ObjectId reference to be updated.</param>
/// <param name="id">The new ObjectId value that will replace the existing reference.</param>
/// <param name="oldId">The current ObjectId value to match and be replaced.</param>
Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId);
/// <summary>
/// Retrieves a paginated, fluent queryable collection of patient treatments based on the provided pagination filter.
/// </summary>
/// <param name="filter">The pagination filter containing page size, page number, and other pagination criteria.</param>
/// <returns>An <see cref="IFindFluent{TSource, TDocument}"/> for <see cref="PatientTreatment"/> that supports further fluent query composition and pagination.</returns>
IFindFluent<PatientTreatment, PatientTreatment> GetPaginatedTreatments(PaginationFilter filter);
/// <summary>
/// Asynchronously retrieves a list of active patient treatments for the specified patient, ordered according to the provided ordering criterion.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active treatments are to be retrieved.</param>
/// <param name="order">The ordering criterion applied to the returned list of treatments.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of active <see cref="PatientTreatment"/> objects for the specified patient.</returns>
Task<List<PatientTreatment>> GetActiveTreatmentsByPatientIdAndOrder(ObjectId patientId, string order);
}
@@ -10,19 +10,83 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IUnitRepository : IMongoRepository<Unit>
{
//Task<Unit?> FindByLocation(PatientLocation location);
/// <summary>
/// Asynchronously retrieves a <see cref="Unit"/> entity by its identifier.
/// </summary>
/// <param name="id">The identifier of the <see cref="Unit"/> to locate.</param>
/// <returns>A task that resolves to the matching <see cref="Unit"/>, or <c>null</c> if no entity is found.</returns>
Task<Unit?> FindById(object id);
/// <summary>
/// Asynchronously finds a unit by its section name.
/// </summary>
/// <param name="section">The name of the section to search for.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the unit if found; otherwise, null.</returns>
Task<Unit?> FindByName(string section);
//Task<Unit?> FindByPointOfCare(PointOfCare pointOfCare);
/// <summary>
/// Asynchronously retrieves all available units and returns them as a list.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of all <see cref="Unit"/> entities.</returns>
Task<List<Unit>> GetAll();
/// <summary>
/// Updates an existing unit in the underlying store and returns the updated entity.
/// </summary>
/// <param name="section">The unit containing the updated data to persist.</param>
/// <returns>A task that resolves to the updated <see cref="Unit"/>, or <c>null</c> when the unit cannot be found.</returns>
Task<Unit?> UpdateUnit(Unit section);
/// <summary>
/// Inserts a single <see cref="Unit"/> record and returns the inserted entity, or <see langword="null"/> if the insertion did not produce a result.
/// </summary>
/// <param name="unit">The <see cref="Unit"/> entity to insert.</param>
/// <returns>A task that represents the asynchronous insert operation. The task result contains the inserted <see cref="Unit"/>, or <see langword="null"/> when no unit is returned.</returns>
Task<Unit?> InsertOneUnit(Unit unit);
/// <summary>
/// Retrieves the collection of units associated with the specified master list identifier and master list type.
/// </summary>
/// <param name="id">The unique identifier of the master list whose units should be retrieved.</param>
/// <param name="masterListType">The type of the master list used to filter or scope the unit lookup.</param>
/// <returns>A task that represents the asynchronous operation, containing the collection of units linked to the specified master list.</returns>
Task<IEnumerable<Unit>> FindByMasterListId(ObjectId id, MasterListType masterListType);
/// <summary>
/// Asynchronously retrieves the collection of <see cref="Unit"/> entities associated with the specified master list identifier.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the master list whose units are being requested.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="IEnumerable{Unit}"/> with the units linked to the given master list.</returns>
Task<IEnumerable<Unit>> FindByMasterListId(ObjectId id);
/// <summary>
/// Updates the unit master list based on the provided unit identifiers. Returns the updated <see cref="Unit"/> when the operation succeeds, or <c>null</c> when no matching unit is found.
/// </summary>
/// <param name="updateUnitListDto">The data transfer object containing the list of unit identifiers to update.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated <see cref="Unit"/>, or <c>null</c> if no unit was updated.</returns>
Task<Unit?> UpdateUnitMasterList(UpdateUnitIdListDto updateUnitListDto);
/// <summary>
/// Updates the name and title of an existing unit identified by the specified identifier.
/// </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>A task that represents the asynchronous update operation. The task result contains the updated <see cref="Unit"/>, or <c>null</c> if the unit was not found.</returns>
Task<Unit?> UpdateUnitInfo(ObjectId unitId, string name, string title);
/// <summary>
/// Updates the configuration of a unit identified by the specified identifier.
/// </summary>
/// <param name="unitIdParsed">The parsed object identifier of the unit whose configuration will be updated.</param>
/// <param name="unitConfiguration">The new configuration values to apply to the unit.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the configuration was successfully updated; otherwise, <c>false</c>.</returns>
Task<bool> UpdateConfiguration(ObjectId unitIdParsed, UnitConfiguration unitConfiguration);
/// <summary>
/// Retrieves a paginated collection of <see cref="Unit"/> records based on the provided pagination criteria.
/// </summary>
/// <param name="filter">The pagination filter that defines the page size, page number, and any additional filtering criteria.</param>
/// <returns>An <see cref="IFindFluent{Unit, Unit}"/> representing the fluent query for the paginated units.</returns>
IFindFluent<Unit, Unit> GetPaginatedUnits(PaginationFilter filter);
/// <summary>
/// Asynchronously counts the number of units associated with the master list identified by the specified identifier and type.
/// </summary>
/// <param name="id">The unique identifier of the master list whose units should be counted.</param>
/// <param name="masterListType">The type of the master list used to resolve the appropriate unit collection.</param>
/// <returns>A task that represents the asynchronous operation, containing the total number of units matching the specified master list.</returns>
Task<long> CountUnitsByMasterListId(ObjectId id, MasterListType masterListType);
}
@@ -7,12 +7,54 @@ namespace adas_core.Application.Repositories.Interfaces;
public interface IUserRepository : IMongoRepository<User>
{
/// <summary>
/// Asynchronously retrieves a user by validating the provided username and password credentials.
/// Returns the matching user when authentication succeeds, or null if no user matches the supplied credentials.
/// </summary>
/// <param name="username">The username used to look up the user account.</param>
/// <param name="password">The password used to authenticate the user.</param>
/// <returns>A task that resolves to the authenticated <see cref="User"/>, or null if no user matches the provided credentials.</returns>
Task<User?> GetUser(string username, string password);
/// <summary>
/// Retrieves a user by their unique identifier asynchronously.
/// </summary>
/// <param name="id">The unique identifier of the user to retrieve.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="User"/> if found; otherwise, <see langword="null"/>.</returns>
Task<User?> GetById(ObjectId id);
/// <summary>
/// Asynchronously retrieves a user matching the specified username. Returns <c>null</c> if no user is found.
/// </summary>
/// <param name="name">The username to look up.</param>
/// <returns>A task that represents the asynchronous operation, containing the matching <see cref="User"/> or <c>null</c> if not found.</returns>
Task<User?> GetByUserName(string name);
/// <summary>
/// Asynchronously retrieves a user by their name, returning null if no matching user is found.
/// </summary>
/// <param name="name">The name of the user to look up.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the user with the specified name, or null if no matching user exists.</returns>
Task<User?> GetByName(string name);
/// <summary>
/// Asynchronously retrieves the user associated with the specified authorities name.
/// </summary>
/// <param name="name">The authorities name used to look up the user.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="User"/> if found; otherwise, <c>null</c>.</returns>
Task<User?> GetByUserAndAuthoritesName(string name);
/// <summary>
/// Updates an existing user in the system, optionally updating the user's password when the <paramref name="updatePass"/> flag is set.
/// </summary>
/// <param name="user">The user entity containing the updated information to be persisted.</param>
/// <param name="updatePass">A flag indicating whether the user's password should also be updated as part of this operation.</param>
/// <returns>A task that returns the updated <see cref="User"/> entity, or <c>null</c> if the user was not found.</returns>
Task<User?> UpdateUser(User user, bool updatePass);
/// <summary>
/// Retrieves a paginated, fluent queryable collection of <see cref="User"/> entities based on the supplied pagination filter.
/// </summary>
/// <param name="filter">The pagination filter that defines paging parameters such as page number and page size.</param>
/// <returns>An <see cref="IFindFluent{User, User}"/> representing the queryable, paginated user results.</returns>
IFindFluent<User, User> GetPaginatedUsers(PaginationFilter filter);
/// <summary>
/// Retrieves the existing system user, creating a new one if it does not already exist.
/// </summary>
/// <returns>The existing or newly created system <see cref="User"/>.</returns>
Task<User> GetOrCreateSystemUser();
}
@@ -32,132 +32,178 @@ public class AdminPanelService(
#region Patient
/// <summary>
/// Archives the specified patient via the patient service and logs the operation. Returns <c>true</c> on success; any exception thrown by the underlying service is written to the console and rethrown.
/// </summary>
/// <param name="patient">The patient to archive.</param>
/// <returns><c>true</c> if the patient was archived successfully.</returns>
/// <exception cref="System.Exception">Rethrows any exception thrown by the underlying patient service after logging it to the console.</exception>
public async Task<bool> ArchivePatient(Patient patient)
{
try
{
await patientService.ArchivePatient(patient);
try
{
await patientService.ArchivePatient(patient);
logger.LogDebug("archived patientid {patientid} from ADMPanel", patient.Id);
return true;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
logger.LogDebug("archived patientid {patientid} from ADMPanel", patient.Id);
/// <summary>
/// Creates a new patient from an ADMPanel request, generating a new identifier and persisting it through the patient service.
/// </summary>
/// <param name="admRequest">The ADMPanel request containing the data used to populate the new patient.</param>
/// <returns>The newly created <see cref="Patient"/> with its generated identifier.</returns>
public async Task<Patient?> CreatePatient(AdmPanelRequest admRequest)
{
var patient = new Patient
{
Id = ObjectId.GenerateNewId()
};
await UpdateNewPatient(patient, admRequest);
await patientService.Insert(patient);
logger.LogDebug("Inserted {patientid} from ADMPanel", patient.Id);
return patient;
}
/// <summary>
/// Updates an existing <see cref="Patient"/> with data from an <see cref="AdmPanelRequest"/>, applying patient fields,
/// person data, default identifier records, and unit/point-of-care (location) assignment, including conflict and not-found validations.
/// </summary>
/// <param name="patient">The patient entity to be updated in place.</param>
/// <param name="admRequest">The admission panel request containing the new values to apply to the patient.</param>
/// <exception cref="Exception">Thrown when the target <c>PointOfCareId</c> is already occupied by another patient.</exception>
/// <exception cref="NotFoundException">Thrown when the requested <c>PointOfCareId</c> does not exist.</exception>
/// <returns>The asynchronous <see cref="Task"/> representing the update operation.</returns>
private async Task UpdateNewPatient(Patient patient, AdmPanelRequest admRequest)
{
if (!string.IsNullOrEmpty(admRequest.PatientNumber)) patient.PatientNumber = admRequest.PatientNumber;
if (admRequest.AdmTime.HasValue) patient.AdmTime = admRequest.AdmTime;
if (admRequest.Patient != null && !admRequest.Patient.IsEmptyDontCheckIds())
patient.Person = admRequest.Patient;
if (admRequest.Patient != null && (admRequest.Patient.Ids == null || admRequest.Patient.Ids.Count == 0))
{
if (patient.Person is { Ids: null }) patient.Person.Ids = new Dictionary<string, string>();
foreach (var item in _defaultIdRecord) patient.Person?.Ids?.Add(item, admRequest?.PatientNumber ?? "null");
}
patient.Person?.SetIds(patient.Person?.Ids ?? new Dictionary<string, string>());
if (admRequest is { UnitId: not null })
{
var unit = await unitService.FindById(admRequest.UnitId);
patient.UnitId = unit?.Id ?? admRequest.UnitId;
patient.UnitString = unit?.Name;
if (admRequest is { PointOfCareId: not null })
{
//El paciente existe en la localizacion no te dejo insertarlo
var patientInLocation = await patientService.FindByPointOfCareId(admRequest.PointOfCareId.Value);
if (patientInLocation != null)
{
logger.LogError(
"trying to insert patientid {patientid} in to location already in use. PointOfcareId: {poc}",
patient.Id, patient.PointOfCareId);
throw new Exception($"patient already in location: {patient.Location}");
}
var poc = await pocService.FindById(admRequest.PointOfCareId.Value);
if (poc == null)
{
logger.LogError(
"trying to insert patientid {patientid} in to location not found. PointOfcareId: {poc}",
patient.Id, admRequest.PointOfCareId);
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
await pocService.SetPointOfCareStatus(poc.Id, StatusEnum.PointOfCare.InUse);
patient.Location = new PatientLocation
(
bed: poc.Bed,
room: poc.Room,
unitName: unit?.Name
);
patient.PointOfCareId = poc.Id;
}
else
{
var pocUnknown =
await pocService.FindByBedAndUnitId(VirtualPointOfCare.Unknown.ToString(), patient.UnitId);
patient.PointOfCareId = pocUnknown?.Id;
}
patient.UpdateDate = DateTime.UtcNow;
}
}
/// <summary>
/// Retrieves a patient by their unique identifier from the patient service.
/// Throws a not-found exception when no matching patient exists for the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the patient to look up.</param>
/// <returns>The patient matching the provided identifier.</returns>
/// <exception cref="NotFoundException">Thrown when no patient is found for the specified identifier.</exception>
public async Task<Patient?> FindPatientById(ObjectId id)
{
return await patientService.FindById(id) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
/// <summary>
/// Finds a patient at the specified location by delegating to the patient service.
/// Returns null when no patient is found at the given location.
/// </summary>
/// <param name="location">The location to search for a patient at.</param>
/// <returns>A <see cref="Patient"/> if one is found at the specified location; otherwise, <c>null</c>.</returns>
public async Task<Patient?> FindPatientByLocation(PatientLocation location)
{
return await patientService.FindByLocation(location);
}
/// <summary>
/// Retrieves a patient by their unique patient number by delegating to the patient service.
/// Returns <c>null</c> when no matching patient is found.
/// </summary>
/// <param name="patientNumber">The unique patient number used to look up the patient.</param>
/// <returns>A <see cref="Patient"/> if a match is found; otherwise, <c>null</c>.</returns>
public async Task<Patient?> FindPatientByPatientNumber(string patientNumber)
{
return await patientService.FindByPatientNumber(patientNumber);
}
/// <summary>
/// Updates the patient information based on the provided administrative panel request, persisting only the patient-related fields and detecting whether the patient number has changed.
/// </summary>
/// <param name="request">The administrative panel request containing the new patient number and the updated patient data to apply.</param>
/// <param name="oldPatient">The existing patient record currently stored in the database that will be updated.</param>
/// <returns>A task that resolves to <c>true</c> when the patient data is successfully updated.</returns>
/// <exception cref="ConflictException">Thrown when the request is missing the patient number, the patient payload is null, or the patient payload is empty.</exception>
public async Task<bool> UpdatePatientData(AdmPanelRequest request, Patient oldPatient)
{
//traer el paciente que se quiere actualizar con los valores que tenga en la base de datos a una variable
//actualizar unicamente los campos que tengan que ver con los datos del paciente
if (request.PatientNumber == null || request.Patient == null || request.Patient.IsEmptyDontCheckIds())
{
logger.LogError("Patient Data not updated. Old Patient:{oldPatient}. Api Request {request}", oldPatient,
request);
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
}
var patientNumberChanged = oldPatient.PatientNumber != request.PatientNumber;
await patientService.UpdatePatientData(oldPatient.Id, request.PatientNumber, request.Patient,
patientNumberChanged);
return true;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public async Task<Patient?> CreatePatient(AdmPanelRequest admRequest)
{
var patient = new Patient
{
Id = ObjectId.GenerateNewId()
};
await UpdateNewPatient(patient, admRequest);
await patientService.Insert(patient);
logger.LogDebug("Inserted {patientid} from ADMPanel", patient.Id);
return patient;
}
private async Task UpdateNewPatient(Patient patient, AdmPanelRequest admRequest)
{
if (!string.IsNullOrEmpty(admRequest.PatientNumber)) patient.PatientNumber = admRequest.PatientNumber;
if (admRequest.AdmTime.HasValue) patient.AdmTime = admRequest.AdmTime;
if (admRequest.Patient != null && !admRequest.Patient.IsEmptyDontCheckIds())
patient.Person = admRequest.Patient;
if (admRequest.Patient != null && (admRequest.Patient.Ids == null || admRequest.Patient.Ids.Count == 0))
{
if (patient.Person is { Ids: null }) patient.Person.Ids = new Dictionary<string, string>();
foreach (var item in _defaultIdRecord) patient.Person?.Ids?.Add(item, admRequest?.PatientNumber ?? "null");
}
patient.Person?.SetIds(patient.Person?.Ids ?? new Dictionary<string, string>());
if (admRequest is { UnitId: not null })
{
var unit = await unitService.FindById(admRequest.UnitId);
patient.UnitId = unit?.Id ?? admRequest.UnitId;
patient.UnitString = unit?.Name;
if (admRequest is { PointOfCareId: not null })
{
//El paciente existe en la localizacion no te dejo insertarlo
var patientInLocation = await patientService.FindByPointOfCareId(admRequest.PointOfCareId.Value);
if (patientInLocation != null)
{
logger.LogError(
"trying to insert patientid {patientid} in to location already in use. PointOfcareId: {poc}",
patient.Id, patient.PointOfCareId);
throw new Exception($"patient already in location: {patient.Location}");
}
var poc = await pocService.FindById(admRequest.PointOfCareId.Value);
if (poc == null)
{
logger.LogError(
"trying to insert patientid {patientid} in to location not found. PointOfcareId: {poc}",
patient.Id, admRequest.PointOfCareId);
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
await pocService.SetPointOfCareStatus(poc.Id, StatusEnum.PointOfCare.InUse);
patient.Location = new PatientLocation
(
bed: poc.Bed,
room: poc.Room,
unitName: unit?.Name
);
patient.PointOfCareId = poc.Id;
}
else
{
var pocUnknown =
await pocService.FindByBedAndUnitId(VirtualPointOfCare.Unknown.ToString(), patient.UnitId);
patient.PointOfCareId = pocUnknown?.Id;
}
patient.UpdateDate = DateTime.UtcNow;
}
}
public async Task<Patient?> FindPatientById(ObjectId id)
{
return await patientService.FindById(id) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
public async Task<Patient?> FindPatientByLocation(PatientLocation location)
{
return await patientService.FindByLocation(location);
}
public async Task<Patient?> FindPatientByPatientNumber(string patientNumber)
{
return await patientService.FindByPatientNumber(patientNumber);
}
public async Task<bool> UpdatePatientData(AdmPanelRequest request, Patient oldPatient)
{
//traer el paciente que se quiere actualizar con los valores que tenga en la base de datos a una variable
//actualizar unicamente los campos que tengan que ver con los datos del paciente
if (request.PatientNumber == null || request.Patient == null || request.Patient.IsEmptyDontCheckIds())
{
logger.LogError("Patient Data not updated. Old Patient:{oldPatient}. Api Request {request}", oldPatient,
request);
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
}
var patientNumberChanged = oldPatient.PatientNumber != request.PatientNumber;
await patientService.UpdatePatientData(oldPatient.Id, request.PatientNumber, request.Patient,
patientNumberChanged);
return true;
}
public async Task<bool> UpdatePatientLocation(AdmPanelRequest request)
{
@@ -198,102 +244,154 @@ public class AdminPanelService(
return true;
}
/// <summary>
/// Asynchronously retrieves a <see cref="Patient"/> based on the provided admission panel request, using location-aware lookup when the location is not fully empty.
/// When a location is present, the search is performed including location criteria; otherwise the location parameter is ignored.
/// </summary>
/// <param name="request">The admission panel request containing the patient identification data and optional location used to locate the patient.</param>
/// <returns>The matching <see cref="Patient"/> if found.</returns>
/// <exception cref="NotFoundException">Thrown when no patient matches the provided request criteria.</exception>
public async Task<Patient?> FindPatient(AdmPanelRequest request)
{
var findByLocation = !request.Location?.IsFullEmpty();
return await patientService.FindPatient(request.PatientId, request.PatientNumber, request.Location,
findByLocation ?? false) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
{
var findByLocation = !request.Location?.IsFullEmpty();
return await patientService.FindPatient(request.PatientId, request.PatientNumber, request.Location,
findByLocation ?? false) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
/// <summary>
/// Retrieves a patient associated with the specified location, throwing a not-found exception if no patient is found.
/// </summary>
/// <param name="location">The location used to look up the patient.</param>
/// <returns>The patient found at the specified location.</returns>
/// <exception cref="NotFoundException">Thrown when no patient is found for the given location.</exception>
public async Task<Patient?> FindByLocation(PatientLocation location)
{
return await patientService.FindByLocation(location) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
{
return await patientService.FindByLocation(location) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
#endregion
#region ConfigObservations
/// <summary>
/// Creates a new configuration based on the provided observation data.
/// Throws a conflict exception when the underlying creation operation fails (returns null), otherwise returns true.
/// </summary>
/// <param name="configObservation">The configuration observation containing the data to persist.</param>
/// <returns>A task that resolves to <c>true</c> when the configuration is created successfully.</returns>
/// <exception cref="ConflictException">Thrown when the configuration creation fails, indicated by a null result from the service call.</exception>
public async Task<bool> CreateConfig(ConfigObservation configObservation)
{
_ = await configObservationService.CreateConfig(configObservation) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
return true;
}
{
_ = await configObservationService.CreateConfig(configObservation) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
return true;
}
/// <summary>
/// Updates a configuration observation by delegating to the configuration service. If the service returns a null result, indicating a failure to update, a conflict exception is thrown.
/// </summary>
/// <param name="configObservation">The configuration observation to update.</param>
/// <returns>A task that resolves to <c>true</c> when the configuration is successfully updated.</returns>
/// <exception cref="ConflictException">Thrown when the underlying update operation fails, as indicated by a null result from the service.</exception>
public async Task<bool> UpdateConfig(ConfigObservation configObservation)
{
_ = await configObservationService.UpdateConfig(configObservation) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
return true;
}
{
_ = await configObservationService.UpdateConfig(configObservation) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
return true;
}
/// <summary>
/// Deletes a config observation item by its identifier. Throws a conflict exception if the underlying removal service returns a null result, indicating the delete could not be completed.
/// </summary>
/// <param name="id">The unique identifier of the config observation item to delete.</param>
/// <returns><c>true</c> when the config observation item is successfully removed.</returns>
/// <exception cref="ConflictException">Thrown when the removal operation returns a null result, signaling a conflict with the delete request.</exception>
public async Task<bool> DeleteConfigObservationItem(ObjectId id)
{
_ = await configObservationService.RemoveConfigItem(id) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
return true;
}
{
_ = await configObservationService.RemoveConfigItem(id) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
return true;
}
#endregion
#region Unit
/// <summary>
/// Inserts a new unit and automatically creates the default Points of Care associated with it, one for each value of the <see cref="VirtualPointOfCare"/> enum.
/// Each created Point of Care is set to Available status, using the enum value name for both Room and Bed, and its identifier is added to the unit's PointOfCareIds before updating the unit. Returns null if the initial unit insertion fails.
/// </summary>
/// <param name="unit">The unit to be inserted.</param>
/// <returns>The inserted unit with its associated Point of Care identifiers, or null if the unit could not be inserted.</returns>
public async Task<Unit?> InsertUnit(Unit unit)
{
//Insertamos la unidad y creamos los PoCs por defecto para esa unidad
var result = await unitService.InsertOne(unit);
if (result != null)
{
// Por cada valor del enum VirtualPointOfCare, creamos un PointOfCare asociado a la unidad
foreach (var pocEnum in Enum.GetValues(typeof(VirtualPointOfCare)).Cast<VirtualPointOfCare>())
//Insertamos la unidad y creamos los PoCs por defecto para esa unidad
var result = await unitService.InsertOne(unit);
if (result != null)
{
var poc = new PointOfCare
// Por cada valor del enum VirtualPointOfCare, creamos un PointOfCare asociado a la unidad
foreach (var pocEnum in Enum.GetValues(typeof(VirtualPointOfCare)).Cast<VirtualPointOfCare>())
{
UnitId = result.Id,
Status = StatusEnum.PointOfCare.Available,
Room = pocEnum.ToString(),
Bed = pocEnum.ToString()
};
var insertedPoc = await pocService.InsertPointOfCare(poc);
if (insertedPoc != null)
{
result.PointOfCareIds ??= [];
result.PointOfCareIds.Add(insertedPoc.Id);
var poc = new PointOfCare
{
UnitId = result.Id,
Status = StatusEnum.PointOfCare.Available,
Room = pocEnum.ToString(),
Bed = pocEnum.ToString()
};
var insertedPoc = await pocService.InsertPointOfCare(poc);
if (insertedPoc != null)
{
result.PointOfCareIds ??= [];
result.PointOfCareIds.Add(insertedPoc.Id);
}
}
// Actualizamos la unidad con los nuevos PointOfCareIds
await unitService.UpdateUnit(result);
}
// Actualizamos la unidad con los nuevos PointOfCareIds
await unitService.UpdateUnit(result);
return result;
}
return result;
}
/// <summary>
/// Asynchronously creates a <see cref="UnitInfoDto"/> for the specified unit, populated with counts of related entities such as admissions, discharges, displays, patients, points of care, and virtual points of care. Returns <c>null</c> if any of the underlying count operations fail, logging the error.
/// </summary>
/// <param name="unit">The unit for which to build the dependency information DTO.</param>
/// <returns>A task that resolves to a <see cref="UnitInfoDto"/> containing the unit's dependency counts, or <c>null</c> if an error occurs while retrieving the counts.</returns>
public async Task<UnitInfoDto?> GetUnitDependencyDto(Unit unit)
{
try
{
return new UnitInfoDto(unit)
try
{
Admissions = await admissionService.CountAdmissionsByUnitId(unit.Id),
Discharges = await dischargeService.CountDischargesByUnitId(unit.Id),
Displays = await displayService.CountDisplaysByUnitId(unit.Id),
Patients = await patientService.CountPatientsByUnitId(unit.Id),
PointOfCares = await pocService.CountPoCsByUnitId(unit.Id),
VirtualPointOfCares = await pocService.CountVirtualPoCsByUnitId(unit.Id)
};
return new UnitInfoDto(unit)
{
Admissions = await admissionService.CountAdmissionsByUnitId(unit.Id),
Discharges = await dischargeService.CountDischargesByUnitId(unit.Id),
Displays = await displayService.CountDisplaysByUnitId(unit.Id),
Patients = await patientService.CountPatientsByUnitId(unit.Id),
PointOfCares = await pocService.CountPoCsByUnitId(unit.Id),
VirtualPointOfCares = await pocService.CountVirtualPoCsByUnitId(unit.Id)
};
}
catch (Exception e)
{
logger.LogError(e.Message);
return null;
}
}
catch (Exception e)
{
logger.LogError(e.Message);
return null;
}
}
/// <summary>
/// Deletes a unit identified by its identifier, cascading the removal to all associated resources
/// (admissions, discharges, authorizations, displays, and points of care). Throws a <see cref="NotFoundException"/>
/// if the unit does not exist, and a <see cref="ConflictException"/> if the unit still has patients assigned to it.
/// </summary>
/// <param name="unitId">The identifier of the unit to delete.</param>
/// <returns><c>true</c> if the unit and its related resources were successfully deleted; otherwise, <c>false</c> when an error is caught and logged.</returns>
/// <exception cref="NotFoundException">Thrown when no unit is found for the specified <paramref name="unitId"/>.</exception>
/// <exception cref="ConflictException">Thrown when the unit has patients assigned to it, preventing deletion.</exception>
public async Task<bool> DeleteUnitById(ObjectId unitId)
{
try
@@ -338,35 +436,61 @@ public class AdminPanelService(
#region Medicine
/// <summary>
/// Retrieves a medicine by its unique identifier, or throws an exception if the medicine cannot be found.
/// </summary>
/// <param name="medicineId">The unique identifier of the medicine to retrieve.</param>
/// <returns>The medicine matching the specified identifier.</returns>
/// <exception cref="NotFoundException">Thrown when no medicine is found with the specified identifier.</exception>
public async Task<Medicine?> GetMedicineById(ObjectId medicineId)
{
return await medicineService.GetMedicineById(medicineId) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
{
return await medicineService.GetMedicineById(medicineId) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
/// <summary>
/// Creates a new medicine by forwarding the request to the medicine service. Throws a conflict exception if the service is unable to create the medicine.
/// </summary>
/// <param name="medicine">The medicine to be created.</param>
/// <returns>The created <see cref="Medicine"/>.</returns>
/// <exception cref="ConflictException">Thrown when the medicine service fails to create the medicine.</exception>
public async Task<Medicine?> PostMedicine(Medicine medicine)
{
var newMedicine = await medicineService.PostMedicine(medicine) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
return newMedicine;
}
{
var newMedicine = await medicineService.PostMedicine(medicine) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
return newMedicine;
}
/// <summary>
/// Updates an existing <see cref="Medicine"/> by delegating to the medicine service.
/// Throws a <see cref="ConflictException"/> when the service returns a null result, indicating the update could not be applied.
/// </summary>
/// <param name="medicine">The medicine entity containing the updated information to persist.</param>
/// <returns>The updated <see cref="Medicine"/> returned by the service.</returns>
/// <exception cref="ConflictException">Thrown when the update operation fails and the service returns a null result.</exception>
public async Task<Medicine?> UpdateMedicine(Medicine medicine)
{
var updatedMedicine = await medicineService.UpdateMedicine(medicine) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
return updatedMedicine;
}
{
var updatedMedicine = await medicineService.UpdateMedicine(medicine) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
return updatedMedicine;
}
/// <summary>
/// Deletes a medicine record by its identifier, validating the identifier format and confirming successful removal.
/// </summary>
/// <param name="medicineId">The string representation of the medicine's ObjectId to delete.</param>
/// <returns>A task that resolves to <c>true</c> when the medicine has been successfully deleted.</returns>
/// <exception cref="BadRequestException">Thrown when <paramref name="medicineId"/> is not a valid ObjectId format.</exception>
/// <exception cref="ConflictException">Thrown when the medicine still exists after the delete operation, indicating the deletion failed.</exception>
public async Task<bool> DeleteMedicineById(string medicineId)
{
if (!ObjectId.TryParse(medicineId, out var objectId))
throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat);
await medicineService.DeleteMedicineById(objectId);
_ = await medicineService.GetMedicineById(ObjectId.Parse(medicineId)) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
return true;
}
{
if (!ObjectId.TryParse(medicineId, out var objectId))
throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat);
await medicineService.DeleteMedicineById(objectId);
_ = await medicineService.GetMedicineById(ObjectId.Parse(medicineId)) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
return true;
}
#endregion
}
@@ -33,11 +33,19 @@ public class AdmissionService(
// Auditory logs
/// <summary>
/// Deletes the specified admission by delegating to the delete operation using the admission's identifier.
/// </summary>
/// <param name="admission">The admission entity to delete, identified by its <see cref="Admission.Id"/>.</param>
public async Task DeleteAdmissionAsync(Admission admission)
{
await DeleteAdmissionByIdAsync(admission.Id);
}
/// <summary>
/// Deletes an admission identified by the given id. If the admission is not found, the operation is skipped and logged; otherwise the admission is removed, any associated point of care is detached (clearing its <c>AdmissionId</c> and <c>Admission</c>) and set to <c>Available</c> when not currently <c>Locked</c> or <c>InUse</c>, a delete broadcast is sent, and an audit log entry is created.
/// </summary>
/// <param name="admissionId">The identifier of the admission to delete.</param>
public async Task DeleteAdmissionByIdAsync(ObjectId admissionId)
{
var admissionAux = await admissionRepository.FindById(admissionId);
@@ -71,24 +79,38 @@ public class AdmissionService(
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, admissionAux, null);
}
/// <summary>
/// Asynchronously deletes all admissions associated with the specified unit identifier by delegating the operation to the admission repository.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose admissions should be removed.</param>
public async Task DeleteAdmissionsByUnitId(ObjectId unitId)
{
_ = await admissionRepository.DeleteAdmissionsByUnitId(unitId);
}
/// <summary>
/// Retrieves an admission by its identifier and, when a point of care is associated, enriches the result with the patient's location (unit, bed, and room) obtained from the point of care service. Returns null if the admission cannot be found.
/// </summary>
/// <param name="admissionId">The unique identifier of the admission to retrieve.</param>
/// <returns>The matching <see cref="Admission"/> with its <see cref="Admission.PatientLocation"/> populated when applicable, or null if no admission is found.</returns>
public async Task<Admission?> GetAdmissionByIdAsync(ObjectId admissionId)
{
var result = await admissionRepository.FindById(admissionId);
if (result?.PointOfCareId != null)
{
var poc = await pointOfCareService.GetInfo(result.PointOfCareId.Value, null,false);
var poc = await pointOfCareService.GetInfo(result.PointOfCareId.Value, null, false);
result.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room);
}
return result;
}
/// <summary>
/// Asynchronously retrieves all admissions and enriches each one with its associated point of care information (unit, bed, and room) when available.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of <see cref="Admission"/> objects with patient location details populated for those linked to a point of care.</returns>
/// <exception cref="NotFoundException">Thrown when the admission repository returns no results.</exception>
public async Task<IEnumerable<Admission>> GetAdmissionsAsync()
{
var resultList = await admissionRepository.FindAll() ??
@@ -97,13 +119,21 @@ public class AdmissionService(
foreach (var admission in admissionsAsync)
if (admission.PointOfCareId != null)
{
var poc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value, null,false);
var poc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value, null, false);
admission.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room);
}
return admissionsAsync;
}
/// <summary>
/// Inserts a new admission, preventing duplicates by NHC and optionally linking it to a Point of Care.
/// When a Point of Care is assigned, its information is used to populate the patient location and, if free, it is reserved and associated with the newly created admission.
/// </summary>
/// <param name="admission">The admission to insert, optionally including a PointOfCareId to associate with a care location.</param>
/// <returns>The newly inserted <see cref="Admission"/>, or <c>null</c> if no result is produced.</returns>
/// <exception cref="ConflictException">Thrown when an admission with the same NHC already exists, or when the insertion fails to return a result.</exception>
/// <exception cref="NotFoundException">Thrown when the specified Point of Care does not exist.</exception>
public async Task<Admission?> InsertAdmission(Admission admission)
{
var admissionAux = await admissionRepository.FindByNhc(admission.Nhc);
@@ -141,20 +171,25 @@ public class AdmissionService(
return insertedAdmission;
}
/// <summary>
/// Updates an existing admission record, enriching its patient location details from the linked point of care on both the prior and incoming states, and records the change via audit log and broadcast.
/// If the admission is not found, the method returns without making changes; point of care lookups are only applied when a <c>PointOfCareId</c> is present and yields a result.
/// </summary>
/// <param name="admission">The admission entity containing the updated information to persist.</param>
public async Task UpdateAdmissionAsync(Admission admission)
{
var oldAdmission = await admissionRepository.FindById(admission.Id);
if (oldAdmission == null) return;
if (oldAdmission.PointOfCareId.HasValue)
{
var pocOld = await pointOfCareService.GetInfo(oldAdmission.PointOfCareId.Value, null,false);
var pocOld = await pointOfCareService.GetInfo(oldAdmission.PointOfCareId.Value, null, false);
if (pocOld != null)
oldAdmission.PatientLocation = new PatientLocation(pocOld.UnitName, pocOld.Bed, pocOld.Room);
}
if (admission.PointOfCareId.HasValue)
{
var poc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value, null,false);
var poc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value, null, false);
if (poc != null) admission.PatientLocation = new PatientLocation(poc.UnitName, poc.Bed, poc.Room);
}
@@ -165,6 +200,11 @@ public class AdmissionService(
SendAdmissionBroadcast(admission, OperationType.UpdateAdmission);
}
/// <summary>
/// Admits a patient based on the provided <see cref="Admission"/>, creating a new <see cref="Patient"/> assigned to the specified point of care, marking the point of care as in use, and updating related master lists (insulation, allergies, diagnosis, origin, language barrier, passive sitting) when present. If the point of care id, unit, or point of care cannot be resolved, the operation is skipped after logging an error. When <paramref name="isNew"/> is <c>false</c>, the originating admission record is deleted after the patient is inserted.
/// </summary>
/// <param name="admission">The admission data used to create the patient and populate location, diagnosis, allergies, and other attributes.</param>
/// <param name="isNew">When <c>false</c>, the admission record is deleted after a successful patient insertion; when <c>true</c>, the admission is retained.</param>
public async Task AdmitPatient(Admission admission, bool isNew = false)
{
if (admission.PointOfCareId == null)
@@ -238,7 +278,7 @@ public class AdmissionService(
if (!isNew)
await DeleteAdmissionAsync(admission);
if (admission.Insulation != null)
await patientService.UpdatePatientMasterList(
patient.Id,
@@ -278,6 +318,10 @@ public class AdmissionService(
null, null);
}
/// <summary>
/// Returns a patient to the admissions workflow by creating a new admission record, removing any existing discharge, and archiving the patient. Validates that the patient and its associated unit exist before proceeding, and only builds the admission when a point of care is assigned.
/// </summary>
/// <param name="patientId">The identifier of the patient to be returned to admissions.</param>
public async Task ReturnPatientToAdmissions(ObjectId patientId)
{
var patient = await patientService.FindById(patientId);
@@ -327,6 +371,11 @@ public class AdmissionService(
}
// Used for temporal beds like PUSHED
/// <summary>
/// Returns a patient to the admissions flow by creating a new admission record from the patient's existing data, removing any prior discharge, and archiving the patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient to be returned to admissions.</param>
/// <param name="adm">The admission context used to resolve the unit and point of care for the new admission record.</param>
public async Task ReturnPatientToAdmissions(ObjectId patientId, Admission adm)
{
var patient = await patientService.FindById(patientId);
@@ -376,6 +425,11 @@ public class AdmissionService(
pointOfCareService.CheckNextAdmission(patient.PointOfCareId);
}
/// <summary>
/// Retrieves a list of admissions for the specified patient location. If an error occurs during retrieval, the error is logged and an empty list is returned.
/// </summary>
/// <param name="location">The patient location used to filter admissions.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of admissions matching the specified location, or an empty list if an error occurs.</returns>
public async Task<List<Admission>> GetAdmissionByLocation(PatientLocation location)
{
try
@@ -390,6 +444,11 @@ public class AdmissionService(
}
}
/// <summary>
/// Retrieves a list of admissions associated with the specified point of care identifier, enriching each admission with its patient location information when available.
/// </summary>
/// <param name="pocId">The identifier of the point of care whose admissions should be retrieved.</param>
/// <returns>A task representing the asynchronous operation, containing the list of admissions for the given point of care, or an empty list if an error occurs.</returns>
public async Task<List<Admission>> GetAdmissionByPointOfCareId(ObjectId pocId)
{
try
@@ -397,7 +456,7 @@ public class AdmissionService(
var result = await admissionRepository.FindByPointOfCareId(pocId);
foreach (var admission in result)
{
var poc = await pointOfCareService.GetInfo(pocId, null,false);
var poc = await pointOfCareService.GetInfo(pocId, null, false);
if (poc != null) admission.PatientLocation = new PatientLocation(poc.UnitName, poc.Bed, poc.Room);
}
@@ -410,6 +469,12 @@ public class AdmissionService(
}
}
/// <summary>
/// Retrieves all admissions associated with the specified point of care and applies translations according to the given locale in parallel.
/// </summary>
/// <param name="pocId">The identifier of the point of care whose admissions will be retrieved.</param>
/// <param name="locale">The locale used to translate the admission fields.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of admissions with their fields translated to the specified locale.</returns>
public async Task<List<Admission>> GetAdmissionByPointOfCareIdAndLocale(ObjectId pocId, LocaleEnum locale)
{
var admissions = await GetAdmissionByPointOfCareId(pocId);
@@ -422,6 +487,12 @@ public class AdmissionService(
return translatedAdmissions.ToList();
}
/// <summary>
/// Retrieves admissions associated with the specified unit, excluding those linked to a Point of Care (PoC).
/// If an error occurs during retrieval, the exception is logged and an empty list is returned as a fallback.
/// </summary>
/// <param name="unitId">The identifier of the unit whose admissions (without PoC) are being requested.</param>
/// <returns>A task that returns a list of <see cref="Admission"/> objects for the given unit, or an empty list if an error occurs.</returns>
public async Task<List<Admission>> GetAdmissionByUnitIdWithOutPoC(ObjectId unitId)
{
try
@@ -436,6 +507,12 @@ public class AdmissionService(
}
}
/// <summary>
/// Asynchronously counts the number of admissions associated with the specified unit identifier by delegating to the admission repository.
/// Returns 0 and logs the error if the repository operation fails, ensuring the method does not propagate exceptions to the caller.
/// </summary>
/// <param name="unitId">The identifier of the unit whose admissions should be counted.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the number of admissions for the given unit, or 0 if an error occurs.</returns>
public async Task<long> CountAdmissionsByUnitId(ObjectId unitId)
{
try
@@ -450,6 +527,12 @@ public class AdmissionService(
}
}
/// <summary>
/// Searches for a patient by patient number, enriching the current patient record with its point of care and unit name when available, and combines it with archived patient and admission lookups scoped to the specified unit.
/// </summary>
/// <param name="patientNumber">The unique patient number used as the primary search key.</param>
/// <param name="unitId">The identifier of the unit used to filter the archived patient and admission searches.</param>
/// <returns>A <see cref="PatientSearch"/> aggregating the current patient, archived patient, and admission data, including flags indicating whether the patient exists only in the archive and whether any of the three sources returned a result.</returns>
public async Task<PatientSearch?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId)
{
var patient = await patientService.FindByPatientNumber(patientNumber);
@@ -475,13 +558,24 @@ public class AdmissionService(
return result;
}
/// <summary>
/// Retrieves the admission record associated with the specified patient clinical record number (NHC) from the admission repository.
/// </summary>
/// <param name="patientNumber">The patient's clinical record number (NHC) used to look up the admission.</param>
/// <returns>A task that resolves to the matching <see cref="Admission"/> if found, or <c>null</c> when no admission exists for the given patient number.</returns>
public Task<Admission?> GetAdmissionByPatientNumber(string patientNumber)
{
return admissionRepository.FindByNhc(patientNumber);
}
/// <summary>
/// Updates the master list option for admissions associated with the specified units, records an audit log entry for each modified admission, and broadcasts the updates.
/// </summary>
/// <param name="opt">The master list update options to apply to the matching admissions.</param>
/// <param name="unitList">The collection of units whose admissions are affected by the update.</param>
/// <param name="typeName">The name of the master list type being modified.</param>
public async Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable<Unit> unitList,
string typeName)
string typeName)
{
var unitIds = unitList.Select(x => x.Id).ToList();
var oldAdmissionList = await admissionRepository.FindByUnitIds(unitIds);
@@ -494,6 +588,12 @@ public class AdmissionService(
}
}
/// <summary>
/// Deletes a master list option from patient admissions associated with the specified units and type, records an audit log entry for each affected admission, and broadcasts the admission update when the updated admission is found.
/// </summary>
/// <param name="opt">The master list option to remove from the admissions.</param>
/// <param name="unitList">The collection of units whose admissions will be processed for the deletion.</param>
/// <param name="typeName">The name of the option type being deleted.</param>
public async Task DeletePatientMasterListItem(OptionList opt, IEnumerable<Unit> unitList, string typeName)
{
var unitIds = unitList.Select(x => x.Id).ToList();
@@ -510,6 +610,11 @@ public class AdmissionService(
}
}
/// <summary>
/// Processes an admission API request by performing the appropriate action based on the request type: inserts a new admission, updates an existing one, or deletes it.
/// Required fields (Nhc, Origin, and Diagnosis) are validated before insert and update operations, and the method exits early when the admission or any required value is missing.
/// </summary>
/// <param name="apiRequest">The API request containing the admission payload and the operation type to execute.</param>
public async Task SaveRequest(ApiRequest apiRequest)
{
try
@@ -520,40 +625,40 @@ public class AdmissionService(
switch (apiRequest.Type)
{
case "NewAdmission":
{
if (string.IsNullOrEmpty(apiRequest.Admission.Nhc) ||
apiRequest.Admission.Origin == null ||
apiRequest.Admission.Diagnosis == null)
{
logger.LogDebug(
"Error saving admission api request. Some values are required. Admission: {Admission}",
apiRequest.Admission);
return;
}
if (string.IsNullOrEmpty(apiRequest.Admission.Nhc) ||
apiRequest.Admission.Origin == null ||
apiRequest.Admission.Diagnosis == null)
{
logger.LogDebug(
"Error saving admission api request. Some values are required. Admission: {Admission}",
apiRequest.Admission);
return;
}
await InsertAdmission(apiRequest.Admission);
break;
}
await InsertAdmission(apiRequest.Admission);
break;
}
case "UpdateAdmission":
{
if (string.IsNullOrEmpty(apiRequest.Admission.Nhc) ||
apiRequest.Admission.Origin == null ||
apiRequest.Admission.Diagnosis == null)
{
logger.LogDebug(
"Error updating admission api request. Some values are required. Admission: {Admission}",
apiRequest.Admission);
return;
}
if (string.IsNullOrEmpty(apiRequest.Admission.Nhc) ||
apiRequest.Admission.Origin == null ||
apiRequest.Admission.Diagnosis == null)
{
logger.LogDebug(
"Error updating admission api request. Some values are required. Admission: {Admission}",
apiRequest.Admission);
return;
}
await UpdateAdmissionAsync(apiRequest.Admission);
break;
}
await UpdateAdmissionAsync(apiRequest.Admission);
break;
}
case "DeleteAdmission":
{
await DeleteAdmissionAsync(apiRequest.Admission);
break;
}
{
await DeleteAdmissionAsync(apiRequest.Admission);
break;
}
}
}
catch (Exception ex)
@@ -564,11 +669,21 @@ public class AdmissionService(
}
}
/// <summary>
/// Asynchronously saves the specified API request by scheduling the underlying save operation on a background task.
/// </summary>
/// <param name="apiRequest">The API request to persist.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
public Task SaveRequestAsync(ApiRequest apiRequest)
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
}
/// <summary>
/// Handles the PointOfCare change when an admission is updated, transferring the assignment from the old PointOfCare to the new one. Updates the status of both PointOfCares (e.g., Reserved, InUse, Available) based on patient occupancy, locks, and admission association, and checks for the next pending admission whenever a PointOfCare becomes available.
/// </summary>
/// <param name="admission">The current admission containing the updated PointOfCare identifier.</param>
/// <param name="oldAdmission">The previous admission state used to identify the original PointOfCare to release.</param>
private async Task HandlePointOfCareChange(Admission admission, Admission oldAdmission)
{
// Check if PointOfCare has changed.
@@ -620,6 +735,11 @@ public class AdmissionService(
}
}
/// <summary>
/// Updates the status of the specified point of care, performing a lookup by identifier first. If no point of care is found, the method returns without applying any change.
/// </summary>
/// <param name="pointOfCareId">The identifier of the point of care whose status will be updated.</param>
/// <param name="status">The new status to assign to the point of care.</param>
private async Task SetPointOfCareStatus(ObjectId pointOfCareId, StatusEnum.PointOfCare status)
{
var pointOfCare = await pointOfCareService.FindById(pointOfCareId);
@@ -628,6 +748,12 @@ public class AdmissionService(
await pointOfCareService.SetPointOfCareStatus(pointOfCareId, status);
}
/// <summary>
/// Sends an admission broadcast message by routing to the appropriate sender based on whether a point-of-care identifier is set.
/// Falls back to unit-based delivery when no point-of-care is available; logs and swallows any errors encountered during dispatch.
/// </summary>
/// <param name="admission">The admission record to broadcast.</param>
/// <param name="operation">The operation type associated with the broadcast.</param>
private async void SendAdmissionBroadcast(Admission admission, OperationType operation)
{
try
@@ -644,6 +770,12 @@ public class AdmissionService(
}
}
/// <summary>
/// Sends an admission broadcast to subscribers associated with the admission's Point of Care, grouped and translated by locale.
/// Logs an error and returns early if the admission has no Point of Care id or the Point of Care cannot be found.
/// </summary>
/// <param name="admission">The admission whose broadcast is being sent; its Point of Care is used to select subscribers and locale-specific content.</param>
/// <param name="operation">The type of operation to send to the subscribers.</param>
private async Task SendAdmissionBroadcastByPoC(Admission admission, OperationType operation)
{
if (!admission.PointOfCareId.HasValue)
@@ -677,6 +809,11 @@ public class AdmissionService(
}
}
/// <summary>
/// Sends an admission broadcast to all WebSocket subscribers associated with displays in the admission's unit, grouped by locale so each subscriber receives a localized copy. If the admission has an empty unit id, the broadcast is skipped and an error is logged.
/// </summary>
/// <param name="admission">The admission to broadcast, which supplies the target unit identifier.</param>
/// <param name="operation">The operation type associated with the broadcast message.</param>
private async Task SendAdmissionByUnitId(Admission admission, OperationType operation)
{
if (admission.UnitId == ObjectId.Empty)
@@ -702,6 +839,15 @@ public class AdmissionService(
}
}
/// <summary>
/// Localizes the <see cref="Admission"/>'s Origin, Diagnosis, and Insulation names by resolving them
/// against locale-specific master lists associated with the admission's unit. If the unit is not found,
/// or any of the referenced master list lookups fail or contain no matching option, the original
/// admission values are preserved as a fallback.
/// </summary>
/// <param name="admission">The admission whose reference names will be updated with localized values.</param>
/// <param name="locale">The locale used to retrieve the appropriate master list translations.</param>
/// <returns>The same <see cref="Admission"/> instance with its localized reference names applied when available.</returns>
private async Task<Admission> GetAdmissionWithLocale(Admission admission, LocaleEnum locale)
{
var unit = await unitService.FindById(admission.UnitId);
File diff suppressed because it is too large Load Diff
@@ -7,11 +7,22 @@ using MongoDB.Bson;
namespace adas_core.Application.Services;
/// <summary>
/// Provides an implementation of the IAlertValuesService interface for managing alert values
/// using a configuration observation repository.
/// </summary>
public class AlertValuesService(IConfigObservationRepository alertValueRepository) : IAlertValuesService
{
/// <summary>
/// Retrieves a <see cref="ConfigObservation"/> by its unique identifier from the alert value repository.
/// Throws a <see cref="NotFoundException"/> when no matching resource is found.
/// </summary>
/// <param name="key">The unique identifier of the configuration observation to retrieve.</param>
/// <returns>The matching <see cref="ConfigObservation"/> if found.</returns>
/// <exception cref="NotFoundException">Thrown when no configuration observation exists for the specified <paramref name="key"/>.</exception>
public async Task<ConfigObservation?> FindByKey(ObjectId key)
{
return await alertValueRepository.FindById(key) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
{
return await alertValueRepository.FindById(key) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
}
@@ -33,8 +33,13 @@ public class AppointmentService(
: IAppointmentService
{
private readonly bool _createPatientWithSiu = apiSettings.Value.CreatePatientWithSiu;
private readonly CacheSettings? _cacheSettings = cacheSettings.Value ;
private readonly CacheSettings? _cacheSettings = cacheSettings.Value;
/// <summary>
/// Saves an API request by validating the patient, resolving or creating the patient record, processing the request, and handling associated observations and diagnoses.
/// </summary>
/// <param name="apiRequest">The API request containing the patient number, location, type, observations, diagnosis, and related data to be processed.</param>
/// <exception cref="ApiRequestException">Thrown when the <paramref name="apiRequest"/> has a null or empty patient number.</exception>
public async Task SaveRequest(ApiRequest apiRequest)
{
if (string.IsNullOrEmpty(apiRequest.PatientNumber))
@@ -73,6 +78,12 @@ public class AppointmentService(
}
}
/// <summary>
/// Processes an incoming API request for a patient, handling HL7 SIU message types to create, update, or cancel appointments.
/// Validates the patient and auto-ADT configuration, then routes the request to the appropriate handler based on message type: SIU_S12-S14 and SIU_S18-S22 (booking/rescheduling/modification), SIU_S15-S17 (cancellation), and other types (blocked slots / no-show), persisting changes, refreshing cache, creating audit logs, and emitting broadcasts.
/// </summary>
/// <param name="apiRequest">The API request containing the HL7 message type, timestamp, and appointment payload to process.</param>
/// <param name="patient">The patient associated with the request; if null, the method returns without processing.</param>
public async Task ProcessApiRequest(ApiRequest apiRequest, Patient? patient)
{
if (patient == null)
@@ -209,16 +220,29 @@ public class AppointmentService(
}
}
/// <summary>
/// Asynchronously saves the provided API request by running the save operation on a background task.
/// </summary>
/// <param name="apiRequest">The API request to be saved.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
public Task SaveRequestAsync(ApiRequest apiRequest)
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
}
/// <summary>
/// Archives the specified patient by delegating the operation to <see cref="ArchiveByPatientId"/> using the patient's identifier.
/// </summary>
/// <param name="patient">The patient to be archived.</param>
public async Task Archive(Patient patient)
{
await ArchiveByPatientId(patient.Id);
}
/// <summary>
/// Archives all appointments associated with the specified patient by copying them to the appointment archive repository and then deleting them from the source collection.
/// </summary>
/// <param name="id">The unique identifier of the patient whose appointments should be archived.</param>
public async Task ArchiveByPatientId(ObjectId id)
{
logger.LogDebug("Archive Appointments by patientId {id}", id);
@@ -232,14 +256,26 @@ public class AppointmentService(
await DeleteByPatientId(id);
}
/// <summary>
/// Retrieves a list of patient appointments associated with the specified patient identifier by delegating to the appointment repository.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose appointments are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientAppointment"/> records for the given patient.</returns>
public async Task<List<PatientAppointment>> GetByPatient(ObjectId patientId)
{
return await appointmentRepository.GetByPatient(patientId);
}
/// <summary>
/// Retrieves the list of appointments scheduled for today (UTC) for the specified patient, using a cache-aside pattern to avoid repeated database queries.
/// The full list of patient appointments is fetched from cache (or loaded from the repository on a cache miss) and then filtered locally to include only those whose start time falls on the current UTC date.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose appointments are being queried.</param>
/// <param name="ct">Cancellation token used to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to a list of <see cref="PatientAppointment"/> instances scheduled for today; an empty list is returned when no appointments match.</returns>
public async Task<List<PatientAppointment>> GetTodayByPatient(
ObjectId patientId,
CancellationToken ct = default)
ObjectId patientId,
CancellationToken ct = default)
{
// Obtener clave + TTL según CacheSettings
var (key, ttl) = CacheKeys.PatientAppointmentsTodayKeyWithTtl(_cacheSettings, patientId);
@@ -270,14 +306,20 @@ public class AppointmentService(
return todayAppointments;
}
/// <summary>
/// Retrieves the patient appointments scheduled for today at the specified point of care. Uses a cache to store the full appointment list for the point of care and filters it by today's date; returns an empty list if the point of care is not found.
/// </summary>
/// <param name="pocId">The identifier of the point of care whose appointments should be retrieved.</param>
/// <param name="ct">A cancellation token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of patient appointments scheduled for today at the specified point of care.</returns>
public async Task<List<PatientAppointment>> GetTodayByPoc(
ObjectId pocId,
CancellationToken ct = default)
ObjectId pocId,
CancellationToken ct = default)
{
var poc = await pointOfCareService.FindById(pocId);
if(poc == null) return [];
if (poc == null) return [];
// Obtener clave + TTL según CacheSettings
var (key, ttl) = CacheKeys.PocAppointmentsTodayKeyWithTtl(_cacheSettings, pocId);
@@ -307,34 +349,59 @@ public class AppointmentService(
return todayAppointments;
}
/// <summary>
/// Asynchronously retrieves all patient appointments associated with the specified patient identifier by delegating to the appointment repository.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose appointments are to be retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing an async cursor over the matching <see cref="PatientAppointment"/> documents.</returns>
public Task<IAsyncCursor<PatientAppointment>> FindByPatientIdAsync(ObjectId patientId)
{
return appointmentRepository.FindByPatientIdAsync(patientId);
}
/// <summary>
/// Retrieves all patient appointments associated with the specified location.
/// </summary>
/// <param name="location">The location used to filter the patient appointments.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of patient appointments for the specified location.</returns>
public async Task<List<PatientAppointment>> FindByLocation(PatientLocation location)
{
return await appointmentRepository.FindByLocation(location);
}
/// <summary>
/// Deletes all appointments associated with the specified patient identifier, invalidates the appointments cache, and records the action in the audit log.
/// </summary>
/// <param name="id">The unique identifier of the patient whose appointments will be deleted.</param>
public async Task DeleteByPatientId(ObjectId id)
{
var patientApp = await FindByPatientIdAsync(id);
logger.LogDebug("Delete Appointments by Patient Id {id}", id);
await appointmentRepository.DeleteByPatientId(id);
// Invalidar CACHE (colección completa)
await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.Appointments));
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, patientApp, null);
}
/// <summary>
/// Updates multiple appointment records by replacing the specified <paramref name="oldId"/> with the new <paramref name="id"/>, scoped by the given <paramref name="nameId"/>. Delegates the operation to the underlying appointment repository.
/// </summary>
/// <param name="nameId">The identifier used to scope which appointment records are affected by the update.</param>
/// <param name="id">The new ObjectId that will replace the existing one in the matching records.</param>
/// <param name="oldId">The current ObjectId to be replaced in the matching records.</param>
public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
{
await appointmentRepository.UpdateManyObjectId(nameId, id, oldId);
}
/// <summary>
/// Broadcasts a patient appointment operation to all subscribers associated with the appointment's locations. Iterates through each resource group and location, resolving the unit and point of care, and dispatches a fire-and-forget message to every matching subscriber. Skips locations with missing unit/bed data and silently ignores unresolved unit or point-of-care lookups.
/// </summary>
/// <param name="appointment">The patient appointment whose resource groups and locations will be broadcast to subscribers.</param>
/// <param name="operationType">The optional operation type describing the change performed on the appointment; passed along to the subscriber message.</param>
private async Task SendBroadcast(PatientAppointment appointment, OperationType? operationType)
{
//RECORRE LOS DIFERENTES LOCATIONS DE LA CITA
@@ -16,6 +16,11 @@ public class ArchivePatientCarePlanService(
{
#region Create
/// <summary>
/// Inserts a patient care plan into the archived patient repository, logs the operation, and creates an audit log entry capturing the current user context.
/// </summary>
/// <param name="patient">The patient care plan to be archived and inserted.</param>
/// <returns>The inserted patient care plan, or <see langword="null"/> if no plan was provided.</returns>
public async Task<PatientCarePlan?> InsertOneAsync(PatientCarePlan patient)
{
await archivedPatientRepository.InsertOneAsync(patient);
@@ -26,6 +31,10 @@ public class ArchivePatientCarePlanService(
return patient;
}
/// <summary>
/// Asynchronously inserts a batch of archived patient care plans into the repository and creates an audit log entry for each one using the current HTTP context user.
/// </summary>
/// <param name="patientCarePla">The list of patient care plans to insert and audit.</param>
public async Task InsertManyAsync(List<PatientCarePlan> patientCarePla)
{
await archivedPatientRepository.InsertManyAsync(patientCarePla);
@@ -38,21 +47,43 @@ public class ArchivePatientCarePlanService(
#region Read
/// <summary>
/// Retrieves the list of archived patient care plans associated with the specified patient identifier by delegating to the underlying repository.
/// Returns a nullable list, which may be null when no archived care plans exist for the patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose archived care plans are being searched.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="PatientCarePlan"/> for the patient, or null if no records are found.</returns>
public async Task<List<PatientCarePlan>?> FindByPatientId(ObjectId patientId)
{
return await archivedPatientRepository.FindByPatientId(patientId);
}
/// <summary>
/// Retrieves a list of archived patient care plans associated with the specified patient identifier.
/// Returns null when no archived care plans are found for the given patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose archived care plans are being retrieved.</param>
/// <returns>A list of <see cref="PatientCarePlan"/> entries for the patient, or <c>null</c> if no records exist.</returns>
public async Task<List<PatientCarePlan>?> FindByPatientId(string patientId)
{
return await archivedPatientRepository.FindByPatientId(patientId);
}
/// <summary>
/// Retrieves a list of archived patient care plans associated with the specified patient number.
/// Returns null if no archived care plans are found for the given patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose archived care plans are being searched.</param>
/// <returns>A list of <see cref="PatientCarePlan"/> objects for the specified patient, or null if no records are found.</returns>
public async Task<List<PatientCarePlan>?> FindByPatientNumber(string patientId)
{
return await archivedPatientRepository.FindByPatientNumber(patientId);
}
/// <summary>
/// Retrieves all archived patient care plans by delegating to the archived patient repository.
/// </summary>
/// <returns>A task that resolves to a list of <see cref="PatientCarePlan"/> objects representing all archived patient care plans.</returns>
public Task<List<PatientCarePlan>> FindAll()
{
return archivedPatientRepository.FindAll();
@@ -8,6 +8,11 @@ namespace adas_core.Application.Services;
public class ArchivePatientObservationsService(IObservationArchiveRepository archivedPatientObservationService)
: IArchivedPatientObservationService
{
/// <summary>
/// Retrieves all archived patient observations associated with the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose archived observations are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of archived <see cref="PatientObservation"/> records for the patient.</returns>
public async Task<List<PatientObservation>> FindArchivedPatientObservationsFromPatient(ObjectId patientId)
{
return await archivedPatientObservationService.FindAllFromPatient(patientId);
@@ -4,8 +4,18 @@ using adas_core.Domain.Models.MongoModels;
namespace adas_core.Application.Services;
/// <summary>
/// Provides services for managing archived patient data, implementing the <see cref="IArchivedPatientService"/> interface.
/// </summary>
/// <remarks>
/// This class uses an <see cref="IPatientArchiveRepository"/> to perform operations on archived patient records, following the repository pattern.
/// </remarks>
public class ArchivedPatientService(IPatientArchiveRepository archivedPatientRepository) : IArchivedPatientService
{
/// <summary>
/// Retrieves all archived patients by delegating to the archived patient repository.
/// </summary>
/// <returns>A task representing the asynchronous operation, containing a list of all archived <see cref="Patient"/> entities.</returns>
public async Task<List<Patient>> FindAllPatients()
{
return await archivedPatientRepository.FindAll();
@@ -8,6 +8,11 @@ namespace adas_core.Application.Services;
public class ArchivedPatientTreatmentService(ITreatmentArchiveRepository archivedPatientTreatmentService)
: IArchivedPatientTreatmentService
{
/// <summary>
/// Retrieves all archived patient treatments associated with the specified patient by delegating to the archived patient treatment service.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose archived treatments are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientTreatment"/> records for the specified patient.</returns>
public async Task<List<PatientTreatment>> FindAllPatientTreatmentsByPatient(ObjectId patientId)
{
return await archivedPatientTreatmentService.FindAllFromPatient(patientId);
@@ -12,6 +12,10 @@ using Newtonsoft.Json;
namespace adas_core.Application.Services;
/// <summary>
/// Provides an implementation of the <see cref="IAuthService"/> interface, offering
/// authentication-related services to consuming components.
/// </summary>
public class AuthService : IAuthService
{
private readonly IAuthorityRepository _authorityRepository;
@@ -33,6 +37,10 @@ public class AuthService : IAuthService
_ = InstanceAuthUtils();
}
/// <summary>
/// Asynchronously obtains a login token from the recording API using the configured client credentials and caches it for reuse via <see cref="AuthUtils"/>.
/// </summary>
/// <returns>A <see cref="LoginResponse"/> containing the authentication token when the request succeeds and the response is valid; otherwise, <c>null</c> if the API URL is not configured, the request fails, the returned token is empty, or an exception is caught and logged.</returns>
public async Task<LoginResponse?> GetLoginResponse()
{
try
@@ -79,6 +87,11 @@ public class AuthService : IAuthService
}
}
/// <summary>
/// Retrieves an authentication token, returning the cached token if it is not empty or expired.
/// Otherwise, attempts a fresh login and returns the new token, falling back to an empty string when no token is obtained.
/// </summary>
/// <returns>A task that resolves to the authentication token, or an empty string if the token could not be obtained.</returns>
public async Task<string> GetToken()
{
var loginResponse = AuthUtils.Instance.GetLoginResponse();
@@ -89,26 +102,49 @@ public class AuthService : IAuthService
return "";
}
/// <summary>
/// Retrieves a list of authorizations associated with the specified unit identifier by delegating to the authority repository.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose authorizations are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Authorization"/> objects for the specified unit.</returns>
public async Task<List<Authorization>> GetByUnitId(ObjectId unitId)
{
return await _authorityRepository.GetByUnitId(unitId);
}
/// <summary>
/// Retrieves the list of authorizations associated with the specified user identifier.
/// </summary>
/// <param name="id">The unique identifier of the user whose authorities are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Authorization"/> entries for the user.</returns>
public async Task<List<Authorization>> GetUserAuthorities(ObjectId id)
{
return await _authorityRepository.GetUserAuthorities(id);
}
/// <summary>
/// Deletes all authorities associated with the specified unit identifier by delegating to the authority repository.
/// </summary>
/// <param name="unitId">The identifier of the unit whose authorities are to be removed.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the deletion was successful; otherwise, <c>false</c>.</returns>
public async Task<bool> DeleteByUnitId(ObjectId unitId)
{
return await _authorityRepository.DeleteAllAuthoritiesByUnit(unitId);
}
/// <summary>
/// Deletes all authorities associated with the specified display identifier by delegating to the authority repository.
/// </summary>
/// <param name="displayId">The unique identifier of the display whose related authorities should be removed.</param>
/// <returns>A task that resolves to <c>true</c> if authorities were successfully deleted; otherwise, <c>false</c>.</returns>
public async Task<bool> DeleteByDisplayId(ObjectId displayId)
{
return await _authorityRepository.DeleteAllAuthoritiesByDisplay(displayId);
}
/// <summary>
/// Ensures the authentication utilities are initialized with a valid login token. Returns early if the recording API URL is not configured; otherwise, requests a new token when the current one is missing or expired, and updates the shared <see cref="AuthUtils"/> instance with the refreshed response when successful.
/// </summary>
private async Task InstanceAuthUtils()
{
if (_recordingSettings.RecordingApiUrl.IsEmpty())
@@ -21,91 +21,189 @@ namespace adas_core.Application.Services.Caching
{
// Selección de backend
/// <summary>
/// Selects the appropriate cache backend (Redis, in-memory, or no-op) for the given key by classifying the key into an entity type and resolving its configured cache mode.
/// Unknown entity types default to in-memory caching, and unrecognized modes fall back to the no-op cache service.
/// </summary>
/// <param name="key">The cache key used to determine the entity type and the corresponding cache backend.</param>
/// <returns>The <see cref="ICacheService"/> instance that should handle caching for the supplied key.</returns>
private ICacheService SelectBackend(string key)
{
var entity = CacheKeyClassifier.Classify(key);
var mode = entity switch
{
CacheEnum.EntityType.Patients => cacheSettings.Patients,
CacheEnum.EntityType.Displays => cacheSettings.Displays,
CacheEnum.EntityType.PumpObservations => cacheSettings.PumpObservations,
CacheEnum.EntityType.Appointments => cacheSettings.Appointments,
CacheEnum.EntityType.GroupedObservations => cacheSettings.GroupedObservations,
_ => CacheEnum.Mode.Cache
};
return mode switch
{
CacheEnum.Mode.Redis => redis,
CacheEnum.Mode.Cache => memory,
_ => noop
};
}
{
var entity = CacheKeyClassifier.Classify(key);
var mode = entity switch
{
CacheEnum.EntityType.Patients => cacheSettings.Patients,
CacheEnum.EntityType.Displays => cacheSettings.Displays,
CacheEnum.EntityType.PumpObservations => cacheSettings.PumpObservations,
CacheEnum.EntityType.Appointments => cacheSettings.Appointments,
CacheEnum.EntityType.GroupedObservations => cacheSettings.GroupedObservations,
_ => CacheEnum.Mode.Cache
};
return mode switch
{
CacheEnum.Mode.Redis => redis,
CacheEnum.Mode.Cache => memory,
_ => noop
};
}
// Para GroupedObservations generamos la misma clave compuesta que el resto de servicios,
// de modo que el clasificador y la política de TTL funcionen igual.
/// <summary>
/// Builds a composite key used to identify grouped observations for a specific patient.
/// </summary>
/// <param name="gf">The grouped field whose name contributes to the key.</param>
/// <param name="patientId">The identifier of the patient associated with the grouped observation.</param>
/// <returns>A formatted key string in the form <c>GroupedObs:{patientId}:{gf.Name}</c>.</returns>
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
=> $"GroupedObs:{patientId}:{gf.Name}";
=> $"GroupedObs:{patientId}:{gf.Name}";
/// <summary>
/// Selects the appropriate cache backend for the given grouped field and patient identifier by building a grouped key and resolving the backend through the key-based overload.
/// </summary>
/// <param name="groupedField">The grouped field used to derive the cache key.</param>
/// <param name="patientId">The patient identifier used to derive the cache key.</param>
/// <returns>The <see cref="ICacheService"/> backend associated with the built grouped key.</returns>
private ICacheService SelectBackend(GroupedField groupedField, ObjectId patientId)
=> SelectBackend(BuildGroupedKey(groupedField, patientId));
=> SelectBackend(BuildGroupedKey(groupedField, patientId));
// GetOrSet (KEY string)
/// <summary>
/// Asynchronously retrieves the object associated with the specified key from the selected backend, or sets it using the provided factory if it is not already cached.
/// </summary>
/// <param name="key">The cache key used to identify the object and to select the appropriate backend.</param>
/// <param name="factory">A delegate that asynchronously produces the value to store when the key is not present in the selected backend.</param>
/// <param name="ttl">An optional time-to-live duration for the cached object. If null, the backend's default expiration is applied.</param>
/// <param name="cancellationToken">A token to observe while waiting for the operation to complete.</param>
/// <returns>A task that represents the asynchronous operation, containing the retrieved or newly created object of type <typeparamref name="T"/>.</returns>
public Task<T> GetOrSetObjectAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
=> SelectBackend(key).GetOrSetObjectAsync(key, factory, ttl, cancellationToken);
string key,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
=> SelectBackend(key).GetOrSetObjectAsync(key, factory, ttl, cancellationToken);
/// <summary>
/// Asynchronously retrieves the value associated with the specified key from the backend selected for that key,
/// or loads and stores it using the provided loader function if it is not already present.
/// Supports an optional time-to-live (TTL) for the cached entry, and the returned value may be null.
/// </summary>
/// <param name="key">The key used to identify the cached value and to select the appropriate backend.</param>
/// <param name="loader">An asynchronous function that produces the value to cache when no existing entry is found.</param>
/// <param name="ttl">An optional time-to-live duration after which the cached entry expires. If null, the backend's default TTL is used.</param>
/// <returns>A task that represents the asynchronous operation, containing the cached or loaded string value, or null if no value could be obtained.</returns>
public Task<string?> GetOrSetValueAsync(
string key,
Func<Task<string>> loader,
TimeSpan? ttl = null)
=> SelectBackend(key).GetOrSetValueAsync(key, loader, ttl);
string key,
Func<Task<string>> loader,
TimeSpan? ttl = null)
=> SelectBackend(key).GetOrSetValueAsync(key, loader, ttl);
// GetOrSet (GroupedField + PatientId)
/// <summary>
/// Asynchronously retrieves the object associated with the specified grouped field and patient, or creates and stores it using the provided factory if it does not exist.
/// The appropriate backend is selected based on the grouped field and patient identifier before the underlying get-or-set operation is performed.
/// </summary>
/// <typeparam name="T">The type of the object to retrieve or create.</typeparam>
/// <param name="groupedField">The grouped field that determines the target backend and identifies the cached object.</param>
/// <param name="patientId">The identifier of the patient whose object is being retrieved or created.</param>
/// <param name="factory">The asynchronous factory used to create the object when no cached value is available.</param>
/// <param name="ttl">An optional time-to-live applied to the cached object. When <c>null</c>, the backend's default expiration is used.</param>
/// <param name="cancellationToken">The token to observe for canceling the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous get-or-set operation, containing the retrieved or newly created object.</returns>
public Task<T> GetOrSetObjectAsync<T>(
GroupedField groupedField,
ObjectId patientId,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
=> SelectBackend(groupedField, patientId)
.GetOrSetObjectAsync(groupedField, patientId, factory, ttl, cancellationToken);
GroupedField groupedField,
ObjectId patientId,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
=> SelectBackend(groupedField, patientId)
.GetOrSetObjectAsync(groupedField, patientId, factory, ttl, cancellationToken);
// Set/Get básicos
/// <summary>
/// Sets the value associated with the specified key by selecting the appropriate backend for that key and delegating the assignment to it.
/// </summary>
/// <param name="key">The key used to select the backend and identify the value to set.</param>
/// <param name="value">The value to associate with the specified key.</param>
public void SetValue(string key, string value)
=> SelectBackend(key).SetValue(key, value);
=> SelectBackend(key).SetValue(key, value);
/// <summary>
/// Retrieves the value associated with the specified key by delegating the lookup to a backend selected for that key.
/// </summary>
/// <param name="key">The key used to select the backend and retrieve the associated value.</param>
/// <returns>The value associated with the key, or <c>null</c> if the selected backend returns no value.</returns>
public string? GetValue(string key)
=> SelectBackend(key).GetValue(key);
=> SelectBackend(key).GetValue(key);
/// <summary>
/// Retrieves an object of type <typeparamref name="T"/> from the backend selected by the given key, with an option to trigger an update.
/// </summary>
/// <param name="key">The identifier used to select the appropriate backend and to look up the object.</param>
/// <param name="upd">Indicates whether the underlying backend should perform an update during retrieval. Defaults to <c>true</c>.</param>
/// <returns>A task that represents the asynchronous retrieval operation, containing the object of type <typeparamref name="T"/> or <c>null</c> if not found.</returns>
public Task<T?> GetObjectAsync<T>(string key, bool upd = true)
=> SelectBackend(key).GetObjectAsync<T>(key, upd);
=> SelectBackend(key).GetObjectAsync<T>(key, upd);
/// <summary>
/// Asynchronously stores an object in the backend selected by the specified key.
/// </summary>
/// <typeparam name="T">The type of the object to store.</typeparam>
/// <param name="key">The key used to select the backend and identify the stored object.</param>
/// <param name="obj">The object to store in the selected backend.</param>
/// <param name="upd">Indicates whether an update operation should be performed. Defaults to <c>true</c>.</param>
/// <returns>A task that represents the asynchronous store operation.</returns>
public Task SetObjectAsync<T>(string key, T obj, bool upd = true)
=> SelectBackend(key).SetObjectAsync(key, obj, upd);
=> SelectBackend(key).SetObjectAsync(key, obj, upd);
/// <summary>
/// Retrieves an object of the specified type from the backend selected by the given key, optionally applying a time-to-live and update behavior.
/// </summary>
/// <param name="key">The identifier used to select the backend and locate the stored object.</param>
/// <param name="ttl">An optional time-to-live applied to the object; if null, the backend's default is used.</param>
/// <param name="upd">A flag indicating whether the retrieval should update the object's state (e.g., refresh expiration).</param>
/// <returns>A task containing the deserialized object, or null if the object is not found in the selected backend.</returns>
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttl, bool upd)
=> SelectBackend(key).GetObjectAsync<T>(key, ttl, upd);
=> SelectBackend(key).GetObjectAsync<T>(key, ttl, upd);
/// <summary>
/// Asynchronously stores an object in the backend selected for the specified key, with an optional time-to-live and update flag.
/// </summary>
/// <param name="key">The key used to select the target backend and identify the object.</param>
/// <param name="obj">The object to store.</param>
/// <param name="ttl">The optional time-to-live duration for the stored object.</param>
/// <param name="upd">Indicates whether to update an existing entry or create a new one.</param>
/// <returns>A task that represents the asynchronous set operation.</returns>
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttl, bool upd)
=> SelectBackend(key).SetObjectAsync(key, obj, ttl, upd);
=> SelectBackend(key).SetObjectAsync(key, obj, ttl, upd);
/// <summary>
/// Asynchronously deletes the object identified by the specified key by delegating the operation to the backend selected for that key.
/// </summary>
/// <param name="key">The identifier of the object to delete, also used to resolve the responsible backend.</param>
/// <returns>A task that represents the asynchronous delete operation.</returns>
public Task DeleteObjectAsync(string key)
=> SelectBackend(key).DeleteObjectAsync(key);
=> SelectBackend(key).DeleteObjectAsync(key);
/// <summary>
/// Deletes entries matching the specified pattern from both Redis and in-memory storage, returning the total count of deleted entries.
/// </summary>
/// <param name="pattern">The pattern used to match entries for deletion in both storage backends.</param>
/// <returns>The combined total number of entries deleted from Redis and in-memory storage.</returns>
public async Task<long> DeleteByPatternAsync(string pattern)
=> await redis.DeleteByPatternAsync(pattern) + await memory.DeleteByPatternAsync(pattern);
=> await redis.DeleteByPatternAsync(pattern) + await memory.DeleteByPatternAsync(pattern);
/// <summary>
/// Clears all cached data from both the in-memory cache and the Redis cache, ensuring that stale entries are removed across all configured cache providers.
/// </summary>
public void CleanCache()
{
memory.CleanCache();
redis.CleanCache();
}
{
memory.CleanCache();
redis.CleanCache();
}
}
}
@@ -15,131 +15,214 @@ namespace adas_core.Application.Services.Caching
private readonly ConcurrentDictionary<string, object> _mem = new();
// HELPERS
/// <summary>
/// Builds a composite key for a grouped observation field, scoped to a specific patient.
/// </summary>
/// <param name="gf">The grouped field whose name is included in the key.</param>
/// <param name="patientId">The identifier of the patient the key is scoped to.</param>
/// <returns>A formatted key string in the form <c>GroupedObs:{patientId}:{gf.Name}</c>.</returns>
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
=> $"GroupedObs:{patientId}:{gf.Name}";
=> $"GroupedObs:{patientId}:{gf.Name}";
// GET OR SET (string key)
/// <summary>
/// Retrieves an object from the in-memory cache by key, or creates and stores it using the provided factory if absent. Uses a fast path for cache hits and a lock-based path with a double-check to prevent duplicate creation across concurrent callers.
/// </summary>
/// <param name="key">The cache key used to look up and store the object.</param>
/// <param name="factory">The asynchronous factory function invoked to produce the object when it is not found in the cache.</param>
/// <param name="ttl">Optional time-to-live associated with the cached object.</param>
/// <param name="cancellationToken">A token to observe for cancellation requests.</param>
/// <returns>The cached or newly created object of type <typeparamref name="T"/>.</returns>
public async Task<T> GetOrSetObjectAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
{
// FAST PATH
if (_mem.TryGetValue(key, out var existing))
return (T)existing;
// LOCKED PATH
return await lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
string key,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
{
if (_mem.TryGetValue(key, out var again))
return (T)again;
var created = await factory();
if (created != null!)
_mem[key] = created;
return created!;
},
cancellationToken);
}
// FAST PATH
if (_mem.TryGetValue(key, out var existing))
return (T)existing;
// LOCKED PATH
return await lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
{
if (_mem.TryGetValue(key, out var again))
return (T)again;
var created = await factory();
if (created != null!)
_mem[key] = created;
return created!;
},
cancellationToken);
}
/// <summary>
/// Asynchronously retrieves the cached string value associated with the specified key, or loads and stores it using the provided loader function if absent.
/// Delegates to <see cref="GetOrSetObjectAsync"/> to handle caching, honoring the optional TTL override for the cache entry.
/// </summary>
/// <param name="key">The cache key used to identify the stored string value.</param>
/// <param name="loader">The asynchronous function invoked to load the value when no cached entry exists for the key.</param>
/// <param name="ttlOverride">An optional time span that overrides the default time-to-live for the cached value.</param>
/// <returns>The cached or newly loaded string value, or <c>null</c> when the underlying cache entry is absent or cannot be cast to a string.</returns>
public async Task<string?> GetOrSetValueAsync(
string key,
Func<Task<string>> loader,
TimeSpan? ttlOverride = null)
{
var result = await GetOrSetObjectAsync(key, loader, ttlOverride);
return (string?)result;
}
string key,
Func<Task<string>> loader,
TimeSpan? ttlOverride = null)
{
var result = await GetOrSetObjectAsync(key, loader, ttlOverride);
return (string?)result;
}
// GET OR SET (GroupedField + patientId)
/// <summary>
/// Asynchronously retrieves a cached object associated with the given grouped field and patient, or creates and caches a new one using the supplied factory when no cached value exists.
/// The factory is only invoked when the cache does not contain a value for the key, and the produced value is stored in the cache only when it is not null.
/// </summary>
/// <param name="groupedField">The grouped field that contributes to the cache key.</param>
/// <param name="patientId">The patient identifier that contributes to the cache key.</param>
/// <param name="factory">The asynchronous factory used to build the object when no cached value is available.</param>
/// <param name="ttl">Optional time-to-live for the cached entry.</param>
/// <param name="cancellationToken">Token used to cancel the asynchronous operation.</param>
/// <returns>A task containing the cached or newly created object.</returns>
public async Task<T> GetOrSetObjectAsync<T>(
GroupedField groupedField,
ObjectId patientId,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
{
var key = BuildGroupedKey(groupedField, patientId);
if (_mem.TryGetValue(key, out var existing))
return (T)existing;
return await lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
GroupedField groupedField,
ObjectId patientId,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
{
if (_mem.TryGetValue(key, out var again))
return (T)again;
var created = await factory();
if (created != null!)
_mem[key] = created;
return created!;
},
cancellationToken);
}
var key = BuildGroupedKey(groupedField, patientId);
if (_mem.TryGetValue(key, out var existing))
return (T)existing;
return await lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
{
if (_mem.TryGetValue(key, out var again))
return (T)again;
var created = await factory();
if (created != null!)
_mem[key] = created;
return created!;
},
cancellationToken);
}
// GET / SET
/// <summary>
/// Stores the specified value in the in-memory collection under the given key, overwriting any existing entry.
/// </summary>
/// <param name="key">The key that identifies where the value will be stored.</param>
/// <param name="value">The value to associate with the specified key.</param>
public void SetValue(string key, string value)
=> _mem[key] = value;
=> _mem[key] = value;
/// <summary>
/// Retrieves the string representation of the value associated with the specified key from the in-memory store.
/// Returns the value converted via <see cref="object.ToString"/> when the key is found, or <c>null</c> when the key is not present.
/// </summary>
/// <param name="key">The key used to look up the value in the underlying store.</param>
/// <returns>The string representation of the stored value if the key exists; otherwise, <c>null</c>.</returns>
public string? GetValue(string key)
=> _mem.TryGetValue(key, out var v) ? v.ToString() : null;
=> _mem.TryGetValue(key, out var v) ? v.ToString() : null;
/// <summary>
/// Asynchronously retrieves an object of type <typeparamref name="T"/> from the in-memory cache using the specified key.
/// Returns the stored value cast to <typeparamref name="T"/> if the key exists, or <c>default</c> (null for reference types) if the key is not found.
/// </summary>
/// <param name="key">The cache key used to look up the stored object.</param>
/// <param name="updateExpiration">Indicates whether the entry's expiration should be refreshed on access. Not currently used by this implementation.</param>
/// <returns>A <see cref="Task{T}"/> containing the cached value cast to <typeparamref name="T"/>, or <c>null</c> if no entry exists for the given key.</returns>
public Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
{
return Task.FromResult(
_mem.TryGetValue(key, out var v) ? (T?)v : default
);
}
{
return Task.FromResult(
_mem.TryGetValue(key, out var v) ? (T?)v : default
);
}
/// <summary>
/// Asynchronously stores the specified object in the in-memory cache using the given key.
/// </summary>
/// <param name="key">The cache key under which the object will be stored.</param>
/// <param name="obj">The object to store in the cache.</param>
/// <param name="updateExpiration">Indicates whether the cache entry's expiration should be refreshed.</param>
public Task SetObjectAsync<T>(string key, T obj, bool updateExpiration = true)
{
_mem[key] = obj!;
return Task.CompletedTask;
}
{
_mem[key] = obj!;
return Task.CompletedTask;
}
/// <summary>
/// Retrieves an object of type <typeparamref name="T"/> associated with the specified key by delegating to an overload that supports an update flag. The <paramref name="ttlOverride"/> parameter is accepted by this overload but is not forwarded to the underlying call.
/// </summary>
/// <param name="key">The identifier of the object to retrieve.</param>
/// <param name="ttlOverride">An optional time-to-live override accepted by this overload but ignored when delegating to the underlying retrieval call.</param>
/// <param name="upd">A flag indicating whether the retrieval should trigger an update on the stored object.</param>
/// <returns>A task that represents the asynchronous operation, containing the retrieved object of type <typeparamref name="T"/> or <c>null</c> if no object is found for the given key.</returns>
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool upd)
=> GetObjectAsync<T>(key, upd);
=> GetObjectAsync<T>(key, upd);
/// <summary>
/// Asynchronously stores an object associated with the specified key, with an option to override its time-to-live. The <paramref name="ttlOverride"/> parameter is accepted but is not forwarded to the underlying storage call, so the effective time-to-live is determined elsewhere.
/// </summary>
/// <param name="key">The identifier used to store and later retrieve the object.</param>
/// <param name="obj">The object to store in the underlying store.</param>
/// <param name="ttlOverride">An optional time-to-live override for the stored entry; not applied by this overload.</param>
/// <param name="upd">A flag indicating whether the operation should update an existing entry.</param>
/// <returns>A task that represents the asynchronous set operation.</returns>
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttlOverride, bool upd)
=> SetObjectAsync(key, obj, upd);
=> SetObjectAsync(key, obj, upd);
// DELETE / CLEAN
/// <summary>
/// Asynchronously removes the object associated with the specified key from the in-memory store. The operation succeeds silently whether or not the key exists.
/// </summary>
/// <param name="key">The identifier of the object to delete.</param>
public Task DeleteObjectAsync(string key)
{
_mem.TryRemove(key, out _);
return Task.CompletedTask;
}
{
_mem.TryRemove(key, out _);
return Task.CompletedTask;
}
/// <summary>
/// Asynchronously deletes cache entries whose keys contain the specified pattern, where asterisk (*) characters in the pattern are treated as wildcards (stripped and matched as substrings). Returns the number of entries successfully removed.
/// </summary>
/// <param name="pattern">The pattern to match against cache keys. Asterisk (*) characters are removed and the remaining text is used as a substring match.</param>
/// <returns>A task that represents the asynchronous operation, containing the count of entries that were removed.</returns>
public Task<long> DeleteByPatternAsync(string pattern)
{
var p = pattern.Replace("*", "");
var keys = _mem.Keys.Where(k => k.Contains(p)).ToList();
long removed = 0;
foreach (var k in keys)
if (_mem.TryRemove(k, out _))
removed++;
return Task.FromResult(removed);
}
{
var p = pattern.Replace("*", "");
var keys = _mem.Keys.Where(k => k.Contains(p)).ToList();
long removed = 0;
foreach (var k in keys)
if (_mem.TryRemove(k, out _))
removed++;
return Task.FromResult(removed);
}
/// <summary>
/// Clears all entries from the in-memory cache, removing any previously stored data.
/// </summary>
public void CleanCache() => _mem.Clear();
}
}
@@ -11,50 +11,105 @@ namespace adas_core.Application.Services.Caching
/// </summary>
public class NoCacheService : ICacheService
{
/// <summary>
/// Stub implementation that performs no action. Intended as a placeholder for storing a value associated with the specified key.
/// </summary>
/// <param name="key">The identifier used to reference the value.</param>
/// <param name="value">The value intended to be associated with the key.</param>
public void SetValue(string key, string value)
{
// No hacer nada
}
{
// No hacer nada
}
/// <summary>
/// Retrieves the string value associated with the specified key.
/// Returns <c>null</c> when no value is found for the given key.
/// </summary>
/// <param name="key">The key used to look up the associated value.</param>
/// <returns>The value associated with <paramref name="key"/>, or <c>null</c> if no value is found.</returns>
public string? GetValue(string key)
{
return null;
}
{
return null;
}
/// <summary>
/// Asynchronously retrieves an object of type <typeparamref name="T"/> associated with the specified <paramref name="key"/>.
/// </summary>
/// <param name="key">The identifier of the object to retrieve.</param>
/// <param name="updateExpiration">Indicates whether the expiration of the entry should be updated upon retrieval.</param>
/// <returns>A <see cref="Task{T}"/> that represents the asynchronous retrieval, containing the object associated with the key or the default value of <typeparamref name="T"/> if not found.</returns>
public Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
{
return Task.FromResult<T?>(default);
}
{
return Task.FromResult<T?>(default);
}
/// <summary>
/// Asynchronously stores the specified object associated with the given key in the underlying data store.
/// When <paramref name="updateExpiration"/> is true, the expiration of the entry is refreshed; otherwise the existing expiration is preserved.
/// </summary>
/// <param name="key">The unique identifier used to store and later retrieve the object.</param>
/// <param name="obj">The object to store in the data store.</param>
/// <param name="updateExpiration">Indicates whether the expiration time of the cached entry should be updated. Defaults to true.</param>
public Task SetObjectAsync<T>(string key, T obj, bool updateExpiration = true)
{
return Task.CompletedTask;
}
{
return Task.CompletedTask;
}
/// <summary>
/// Asynchronously retrieves an object of type <typeparamref name="T"/> associated with the specified <paramref name="key"/>.
/// Supports an optional time-to-live override and an option to update the expiration of the stored entry.
/// </summary>
/// <param name="key">The unique identifier used to look up the stored object.</param>
/// <param name="ttlOverride">An optional time-to-live value that, when provided, overrides the default expiration for the entry.</param>
/// <param name="updateExpiration">Indicates whether the expiration of the entry should be refreshed upon a successful retrieval.</param>
/// <returns>A <see cref="Task{T}"/> that represents the asynchronous operation, containing the retrieved object or <c>null</c> if no value is found.</returns>
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool updateExpiration)
{
return Task.FromResult<T?>(default);
}
{
return Task.FromResult<T?>(default);
}
/// <summary>
/// Asynchronously stores an object associated with the specified key, optionally overriding the time-to-live and updating the expiration.
/// </summary>
/// <param name="key">The identifier used to store and retrieve the object.</param>
/// <param name="obj">The object to be stored.</param>
/// <param name="ttlOverride">An optional time-to-live value that overrides the default expiration period; <c>null</c> uses the default.</param>
/// <param name="updateExpiration">A value indicating whether the expiration time should be updated.</param>
/// <returns>A task that represents the asynchronous set operation.</returns>
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttlOverride, bool updateExpiration)
{
return Task.CompletedTask;
}
{
return Task.CompletedTask;
}
/// <summary>
/// Asynchronously deletes items matching the specified pattern and returns the number of items removed.
/// This implementation is a stub that always returns 0, performing no actual deletion regardless of the provided pattern.
/// </summary>
/// <param name="pattern">The pattern used to identify the items to delete.</param>
/// <returns>A <see cref="Task{Int64}"/> representing the asynchronous operation, with a result of 0 indicating that no items were deleted.</returns>
public Task<long> DeleteByPatternAsync(string pattern)
{
return Task.FromResult(0L);
}
{
return Task.FromResult(0L);
}
/// <summary>
/// Asynchronously deletes the object identified by the specified key.
/// The operation completes immediately without performing an actual deletion.
/// </summary>
/// <param name="key">The identifier of the object to delete.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
public Task DeleteObjectAsync(string key)
{
return Task.CompletedTask;
}
{
return Task.CompletedTask;
}
/// <summary>
/// Performs a cleanup operation on the cache. Currently, this method has no implementation and does not perform any cleanup actions.
/// </summary>
public void CleanCache()
{
// Nada que limpiar
}
{
// Nada que limpiar
}
// ============================================================
// GET OR SET - STRING KEY
@@ -70,14 +125,21 @@ namespace adas_core.Application.Services.Caching
return await factory();
}
/// <summary>
/// Asynchronously loads a value using the provided loader function.
/// </summary>
/// <param name="key">The key associated with the value to retrieve or set.</param>
/// <param name="loader">The asynchronous function used to load the value.</param>
/// <param name="ttl">An optional time-to-live duration for the value.</param>
/// <returns>A task that represents the asynchronous operation, containing the loaded value as a nullable string.</returns>
public async Task<string?> GetOrSetValueAsync(
string key,
Func<Task<string>> loader,
TimeSpan? ttl = null)
{
var result = await loader();
return (string?)result;
}
string key,
Func<Task<string>> loader,
TimeSpan? ttl = null)
{
var result = await loader();
return (string?)result;
}
// ============================================================
// GET OR SET - GroupedField + patientId
@@ -24,44 +24,55 @@ namespace adas_core.Application.Services.Caching
private readonly ConcurrentDictionary<string, string> _tokens =
new(StringComparer.Ordinal);
/// <summary>
/// Attempts to acquire a distributed lock for the specified key using Redis, retrying until the timeout expires.
/// Returns <c>false</c> if the Redis database is unavailable or if the lock cannot be acquired within the given timeout.
/// </summary>
/// <param name="key">The identifier of the resource to lock.</param>
/// <param name="timeout">The maximum duration to keep retrying before giving up.</param>
/// <returns><c>true</c> if the lock was successfully acquired; otherwise, <c>false</c>.</returns>
public async Task<bool> AcquireAsync(string key, TimeSpan timeout)
{
var redis = getDatabase();
if (redis is null) return false;
var redisKey = (RedisKey)(_prefix + key);
var token = Guid.NewGuid().ToString("N");
var end = DateTime.UtcNow.Add(timeout);
while (DateTime.UtcNow < end)
{
if (await redis.StringSetAsync(redisKey, token, _ttl, When.NotExists))
{
_tokens[key] = token;
return true;
var redis = getDatabase();
if (redis is null) return false;
var redisKey = (RedisKey)(_prefix + key);
var token = Guid.NewGuid().ToString("N");
var end = DateTime.UtcNow.Add(timeout);
while (DateTime.UtcNow < end)
{
if (await redis.StringSetAsync(redisKey, token, _ttl, When.NotExists))
{
_tokens[key] = token;
return true;
}
await Task.Delay(_retryDelay);
}
return false;
}
await Task.Delay(_retryDelay);
}
return false;
}
/// <summary>
/// Asynchronously releases the token associated with the specified key by removing it from the in-memory token store and executing a Lua release script against Redis. If the key is not found in the local store, or the Redis database is unavailable, the method returns without performing any further action.
/// </summary>
/// <param name="key">The identifier of the token to release.</param>
public async Task ReleaseAsync(string key)
{
if (!_tokens.TryRemove(key, out var token))
return;
var redis = getDatabase();
if (redis is null) return;
var redisKey = (RedisKey)(_prefix + key);
await redis.ScriptEvaluateAsync(
LuaReleaseScript,
[redisKey],
[token]
);
}
{
if (!_tokens.TryRemove(key, out var token))
return;
var redis = getDatabase();
if (redis is null) return;
var redisKey = (RedisKey)(_prefix + key);
await redis.ScriptEvaluateAsync(
LuaReleaseScript,
[redisKey],
[token]
);
}
}
}
@@ -13,6 +13,9 @@ using StackExchange.Redis;
namespace adas_core.Application.Services.Caching
{
/// <summary>
/// Represents a Redis-based implementation of the <see cref="ICacheService"/> interface for caching operations.
/// </summary>
public class RedisService : ICacheService
{
private readonly ILogger<RedisService> _logger;
@@ -40,236 +43,334 @@ namespace adas_core.Application.Services.Caching
// GET OR SET (string key)
/// <summary>
/// Retrieves an object of type T from the cache using the specified key, or creates and caches a new instance using the provided factory if no cached value exists.
/// Uses a distributed lock to prevent concurrent cache misses from creating duplicate objects, and falls back to calling the factory directly when Redis is unavailable.
/// </summary>
/// <param name="key">The cache key used to identify the stored object.</param>
/// <param name="factory">The asynchronous factory function invoked to create a new instance when the object is not present in the cache.</param>
/// <param name="ttl">Optional time-to-live duration for the cached object. If null, the cache default is used.</param>
/// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation, containing the cached or newly created object.</returns>
public async Task<T> GetOrSetObjectAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
{
if (!_isRedisAvailable)
return await factory();
var direct = await GetObjectAsync<T>(key);
if (direct is not null)
return direct;
return await _lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
string key,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
{
var again = await GetObjectAsync<T>(key);
if (again is not null)
return again;
var created = await factory();
if (created != null)
await SetObjectAsync(key, created, ttl, true);
return created!;
},
cancellationToken);
}
if (!_isRedisAvailable)
return await factory();
var direct = await GetObjectAsync<T>(key);
if (direct is not null)
return direct;
return await _lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
{
var again = await GetObjectAsync<T>(key);
if (again is not null)
return again;
var created = await factory();
if (created != null)
await SetObjectAsync(key, created, ttl, true);
return created!;
},
cancellationToken);
}
/// <summary>
/// Retrieves a cached value for the specified key, or loads, caches, and returns it via the supplied loader if absent.
/// Falls back to invoking the loader directly when Redis is unavailable, and uses a distributed lock to prevent duplicate loads under concurrent access.
/// </summary>
/// <param name="key">The cache key used to identify the stored value.</param>
/// <param name="loader">The asynchronous function invoked to produce the value when it is not present in the cache.</param>
/// <param name="ttl">Optional time-to-live applied to the cached value; if not provided, the default caching policy is used.</param>
/// <returns>The cached value when available, or the value produced by the loader when the cache is empty or Redis is unavailable.</returns>
public async Task<string?> GetOrSetValueAsync(
string key,
Func<Task<string>> loader,
TimeSpan? ttl = null)
{
if (!_isRedisAvailable)
return await loader();
var direct = GetValue(key);
if (direct is not null)
return direct;
return await _lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
string key,
Func<Task<string>> loader,
TimeSpan? ttl = null)
{
var again = GetValue(key);
if (again is not null)
return again;
var created = await loader();
SetValue(key, created);
return created;
});
}
if (!_isRedisAvailable)
return await loader();
var direct = GetValue(key);
if (direct is not null)
return direct;
return await _lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
{
var again = GetValue(key);
if (again is not null)
return again;
var created = await loader();
SetValue(key, created);
return created;
});
}
// GET OR SET (GroupedField + patientId)
/// <summary>
/// Builds a unique cache key for a grouped observation associated with a specific patient.
/// </summary>
/// <param name="gf">The grouped field whose name is used to identify the observation group.</param>
/// <param name="patientId">The identifier of the patient the observation belongs to.</param>
/// <returns>A formatted string key combining the <c>GroupedObs</c> prefix, the patient identifier, and the grouped field name.</returns>
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
=> $"GroupedObs:{patientId}:{gf.Name}";
=> $"GroupedObs:{patientId}:{gf.Name}";
/// <summary>
/// Retrieves a cached object associated with the specified grouped field and patient identifier, or creates and stores it using the provided factory if absent. Uses a distributed lock to prevent duplicate creation under cache misses and falls back to invoking the factory directly when Redis is unavailable.
/// </summary>
/// <param name="groupedField">The grouped field used, together with the patient identifier, to build the cache key.</param>
/// <param name="patientId">The patient identifier used to build the cache key.</param>
/// <param name="factory">Asynchronous factory invoked to produce the object when no cached value exists.</param>
/// <param name="ttl">Optional time-to-live applied to the stored cache entry. If null, no expiration is set.</param>
/// <param name="cancellationToken">Token used to cancel the distributed lock operation.</param>
/// <returns>The cached object if present, otherwise the object produced by <paramref name="factory"/>.</returns>
public async Task<T> GetOrSetObjectAsync<T>(
GroupedField groupedField,
ObjectId patientId,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
{
var key = BuildGroupedKey(groupedField, patientId);
if (!_isRedisAvailable)
return await factory();
var direct = await GetObjectAsync<T>(key);
if (direct is not null)
return direct;
return await _lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
GroupedField groupedField,
ObjectId patientId,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
{
var again = await GetObjectAsync<T>(key);
if (again is not null)
return again;
var created = await factory();
if (created != null)
await SetObjectAsync(key, created, ttl, true);
return created!;
},
cancellationToken);
}
var key = BuildGroupedKey(groupedField, patientId);
if (!_isRedisAvailable)
return await factory();
var direct = await GetObjectAsync<T>(key);
if (direct is not null)
return direct;
return await _lockManager.WithLockAsync(
$"getorset:{key}",
TimeSpan.FromSeconds(5),
async () =>
{
var again = await GetObjectAsync<T>(key);
if (again is not null)
return again;
var created = await factory();
if (created != null)
await SetObjectAsync(key, created, ttl, true);
return created!;
},
cancellationToken);
}
// BASIC OPERATIONS
/// <summary>
/// Stores a string value in the database under the specified key, applying a TTL resolved from <c>GetEntityTtl</c> and preserving any existing TTL on overwrite. If the underlying database is not initialized, the operation is skipped.
/// </summary>
/// <param name="key">The key under which the value will be stored.</param>
/// <param name="value">The string value to persist.</param>
public void SetValue(string key, string value)
=> _database?.StringSet(key, value, GetEntityTtl(key), true);
=> _database?.StringSet(key, value, GetEntityTtl(key), true);
/// <summary>
/// Retrieves a string value from the underlying data store by its key, and conditionally renews the entity's time-to-live when the key is found and renewal is permitted by policy.
/// </summary>
/// <param name="key">The identifier of the value to look up in the data store.</param>
/// <returns>The stored string value, or <c>null</c> if the key does not exist or the data store is unavailable.</returns>
public string? GetValue(string key)
{
var val = _database?.StringGet(key);
if (val.HasValue && ShouldRenewTtl(key))
_database?.KeyExpire(key, GetEntityTtl(key));
return val;
}
{
var val = _database?.StringGet(key);
if (val.HasValue && ShouldRenewTtl(key))
_database?.KeyExpire(key, GetEntityTtl(key));
return val;
}
/// <summary>
/// Asynchronously retrieves and deserializes an object of type <typeparamref name="T"/> from Redis using the specified key.
/// Returns <c>default</c> when Redis is unavailable or when the key is not found or holds an empty value, and optionally refreshes the key's expiration time on a successful hit.
/// </summary>
/// <param name="key">The Redis key identifying the stored object to retrieve.</param>
/// <param name="updateExpiration">When <c>true</c> (the default), resets the key's time-to-live to the configured entity TTL on a successful read, implementing sliding expiration.</param>
/// <returns>A <see cref="Task{T}"/> containing the deserialized object, or <c>default</c> if Redis is unavailable or the key is missing/empty.</returns>
/// <exception cref="Exception">Thrown when the stored JSON payload cannot be deserialized into <typeparamref name="T"/>; the original exception is wrapped and rethrown.</exception>
public async Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
{
if (!_isRedisAvailable)
return default;
var json = await _database!.StringGetAsync(key);
if (json.IsNullOrEmpty)
return default;
if (updateExpiration)
_database!.KeyExpire(key, GetEntityTtl(key));
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
};
try
{
return JsonConvert.DeserializeObject<T>(json!, settings);
}
catch (Exception e)
{
_logger.LogError("Error deserializing object in RedisService {e}",e.ToString());
throw new Exception($"Error deserializing object in RedisService {e}", e);
}
}
{
if (!_isRedisAvailable)
return default;
var json = await _database!.StringGetAsync(key);
if (json.IsNullOrEmpty)
return default;
if (updateExpiration)
_database!.KeyExpire(key, GetEntityTtl(key));
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
};
try
{
return JsonConvert.DeserializeObject<T>(json!, settings);
}
catch (Exception e)
{
_logger.LogError("Error deserializing object in RedisService {e}",e.ToString());
throw new Exception($"Error deserializing object in RedisService {e}", e);
}
}
/// <summary>
/// Asynchronously stores an object associated with the specified key, optionally refreshing its expiration time.
/// </summary>
/// <param name="key">The key under which the object will be stored.</param>
/// <param name="obj">The object to store.</param>
/// <param name="updateExpiration">Indicates whether the expiration time of the entry should be updated.</param>
public async Task SetObjectAsync<T>(
string key,
T obj,
bool updateExpiration = true)
=> await SetObjectAsync(key, obj, null, updateExpiration);
string key,
T obj,
bool updateExpiration = true)
=> await SetObjectAsync(key, obj, null, updateExpiration);
/// <summary>
/// Asynchronously serializes the specified object to JSON and stores it in Redis under the given key, using the provided TTL override or the default entity TTL when not specified. The operation is skipped when Redis is unavailable, and the object is serialized using camelCase property names with string enum and ObjectId converters.
/// </summary>
/// <param name="key">The Redis key under which the serialized object will be stored.</param>
/// <param name="obj">The object to serialize and persist to Redis.</param>
/// <param name="ttlOverride">An optional time-to-live override; when null, the entity's default TTL is applied.</param>
/// <param name="updateExpiration">Flag indicating whether the expiration should be updated.</param>
public async Task SetObjectAsync<T>(
string key,
T obj,
TimeSpan? ttlOverride,
bool updateExpiration)
{
if (!_isRedisAvailable)
return;
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
};
var json = JsonConvert.SerializeObject(obj, settings);
await _database!.StringSetAsync(key, json, ttlOverride ?? GetEntityTtl(key), true);
}
string key,
T obj,
TimeSpan? ttlOverride,
bool updateExpiration)
{
if (!_isRedisAvailable)
return;
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
};
var json = JsonConvert.SerializeObject(obj, settings);
await _database!.StringSetAsync(key, json, ttlOverride ?? GetEntityTtl(key), true);
}
/// <summary>
/// Asynchronously deletes an object from the Redis cache using the specified key.
/// When the Redis backend is unavailable, the call is skipped silently as a no-op fallback.
/// </summary>
/// <param name="key">The unique identifier of the cached object to remove.</param>
public async Task DeleteObjectAsync(string key)
{
if (_isRedisAvailable)
await _database!.KeyDeleteAsync(key);
}
{
if (_isRedisAvailable)
await _database!.KeyDeleteAsync(key);
}
/// <summary>
/// Asynchronously deletes all Redis keys matching the specified pattern.
/// Returns 0 if Redis is unavailable or the server is not initialized.
/// </summary>
/// <param name="pattern">The pattern used to match Redis keys to be deleted.</param>
/// <returns>The number of keys that were deleted.</returns>
public async Task<long> DeleteByPatternAsync(string pattern)
{
if (!_isRedisAvailable || _server == null)
return 0;
var keys = _server.Keys(pattern: pattern).ToArray();
foreach (var key in keys)
await _database!.KeyDeleteAsync(key);
return keys.Length;
}
{
if (!_isRedisAvailable || _server == null)
return 0;
var keys = _server.Keys(pattern: pattern).ToArray();
foreach (var key in keys)
await _database!.KeyDeleteAsync(key);
return keys.Length;
}
/// <summary>
/// Clears all cached data by flushing the underlying server database. If the server instance is <see langword="null"/>, the call is safely skipped as a no-op.
/// </summary>
public void CleanCache()
=> _server?.FlushDatabase();
=> _server?.FlushDatabase();
// TTL
/// <summary>
/// Resolves the time-to-live (TTL) for a cache entity based on the entity type inferred from the cache key, returning entity-specific TTL values for Patients and Displays while falling back to the global TTL for any other entity. Returns <c>null</c> when the resolved TTL in seconds is zero or negative, indicating that the entity should not be cached.
/// </summary>
/// <param name="key">The cache key used to classify the entity type and determine the applicable TTL.</param>
/// <returns>A <see cref="TimeSpan"/> representing the configured TTL, or <c>null</c> if the resolved seconds value is not positive.</returns>
private TimeSpan? GetEntityTtl(string key)
{
var entity = CacheKeyClassifier.Classify(key);
var ttl = _cacheSettings.Redis.Ttl;
int? seconds = entity switch
{
CacheEnum.EntityType.Patients => ttl.PatientsSeconds,
CacheEnum.EntityType.Displays => ttl.DisplaysSeconds,
_ => ttl.GlobalSeconds
};
return seconds > 0 ? TimeSpan.FromSeconds(seconds.Value) : null;
}
{
var entity = CacheKeyClassifier.Classify(key);
var ttl = _cacheSettings.Redis.Ttl;
int? seconds = entity switch
{
CacheEnum.EntityType.Patients => ttl.PatientsSeconds,
CacheEnum.EntityType.Displays => ttl.DisplaysSeconds,
_ => ttl.GlobalSeconds
};
return seconds > 0 ? TimeSpan.FromSeconds(seconds.Value) : null;
}
/// <summary>
/// Determines whether the time-to-live (TTL) for the entity associated with the specified key should be renewed, based on whether an existing TTL value is found.
/// </summary>
/// <param name="key">The key identifying the entity whose TTL presence is being checked.</param>
/// <returns><c>true</c> if a TTL value is found for the specified key; otherwise, <c>false</c>.</returns>
private bool ShouldRenewTtl(string key)
=> GetEntityTtl(key) != null;
=> GetEntityTtl(key) != null;
// INITIALIZATION
/// <summary>
/// Initializes the Redis connection for caching by connecting asynchronously, obtaining the database and server, and marking the connection as available on success. Returns early without establishing a connection when the configured Redis connection string is null, and logs any exception that occurs during initialization without rethrowing, leaving the connection marked as unavailable.
/// </summary>
private async Task InitializeRedisConnectionAsync()
{
_isRedisAvailable = false;
try
{
if (_cacheSettings.Redis.ConnectionString == null)
return;
_connection = await ConnectionMultiplexer.ConnectAsync(_cacheSettings.Redis.ConnectionString);
_database = _connection.GetDatabase();
_server = _connection.GetServer(_cacheSettings.Redis.ConnectionString);
_isRedisAvailable = true;
}
catch(Exception ex)
{
_logger.LogError( "Error Initializing Redis Connection. Exception:{ex}", ex.Message);
}
}
{
_isRedisAvailable = false;
try
{
if (_cacheSettings.Redis.ConnectionString == null)
return;
_connection = await ConnectionMultiplexer.ConnectAsync(_cacheSettings.Redis.ConnectionString);
_database = _connection.GetDatabase();
_server = _connection.GetServer(_cacheSettings.Redis.ConnectionString);
_isRedisAvailable = true;
}
catch(Exception ex)
{
_logger.LogError( "Error Initializing Redis Connection. Exception:{ex}", ex.Message);
}
}
/// <summary>
/// Retrieves an object asynchronously from the cache, optionally updating its expiration time.
/// The optional TTL override is ignored by this overload and is not passed to the underlying call.
/// </summary>
/// <param name="key">The cache key identifying the object to retrieve.</param>
/// <param name="ttlOverride">An optional time-to-live override; not applied by this overload.</param>
/// <param name="updateExpiration">When true, the expiration of the cached entry is refreshed on retrieval.</param>
/// <returns>A task that resolves to the cached object, or null if no entry exists for the specified key.</returns>
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool updateExpiration)
=> GetObjectAsync<T>(key, updateExpiration);
=> GetObjectAsync<T>(key, updateExpiration);
}
}
@@ -8,6 +8,9 @@ using MongoDB.Bson;
namespace adas_core.Application.Services;
/// <summary>
/// Implements the <see cref="ICalculatedObservationsService"/> contract, providing a service for working with calculated observations.
/// </summary>
public class CalculatedObservationsService : ICalculatedObservationsService
{
private static ICalculatedObservations? _service;
@@ -61,6 +64,13 @@ public class CalculatedObservationsService : ICalculatedObservationsService
}
/// <summary>
/// Maps a <see cref="PatientObservation"/> by delegating to the configured mapping service.
/// If no service is available, the original observation is returned unchanged; if the service produces no mapping, a debug message is logged and <c>null</c> is returned.
/// </summary>
/// <param name="obs">The patient observation to map.</param>
/// <param name="onlyByName">When <c>true</c>, restricts the mapping to lookups by name only.</param>
/// <returns>The mapped <see cref="PatientObservation"/>, or <c>null</c> when the service yields no result; the input <paramref name="obs"/> is returned unchanged when no mapping service is configured.</returns>
public virtual async Task<PatientObservation?> Map(PatientObservation obs, bool onlyByName = false)
{
if (_service == null) return obs;
@@ -71,6 +81,12 @@ public class CalculatedObservationsService : ICalculatedObservationsService
return result;
}
/// <summary>
/// Maps a <see cref="PatientObservationAlarm"/> using the configured mapping service. Falls back to returning the original alarm unchanged when no service is available, and logs a debug entry when the service produces a null result (i.e., the alarm is ignored).
/// </summary>
/// <param name="obs">The patient observation alarm to be mapped.</param>
/// <param name="onlyByName">When true, restricts the mapping to a name-based lookup only.</param>
/// <returns>The mapped <see cref="PatientObservationAlarm"/>, or <c>null</c> if the service mapped it to null, or the original <paramref name="obs"/> when no service is configured.</returns>
public virtual async Task<PatientObservationAlarm?> Map(PatientObservationAlarm obs, bool onlyByName = false)
{
if (_service == null) return obs;
@@ -81,6 +97,12 @@ public class CalculatedObservationsService : ICalculatedObservationsService
return result;
}
/// <summary>
/// Maps the supplied <see cref="PumpObservation"/> by delegating to the configured mapping service.
/// Returns <see langword="null"/> when the underlying service has not been initialized.
/// </summary>
/// <param name="obs">The pump observation to be mapped.</param>
/// <returns>A task that yields the mapped <see cref="PumpObservation"/>, or <see langword="null"/> if no mapping service is available.</returns>
public virtual async Task<PumpObservation?> Map(PumpObservation obs)
{
if (_service == null) return null;
@@ -88,6 +110,11 @@ public class CalculatedObservationsService : ICalculatedObservationsService
return result;
}
/// <summary>
/// Maps a <see cref="PatientRecordingAlert"/> using the configured mapping service. Falls back to returning the original alert when no service is configured, and logs a debug entry when the service produces no mapping.
/// </summary>
/// <param name="obs">The <see cref="PatientRecordingAlert"/> instance to be mapped.</param>
/// <returns>The mapped <see cref="PatientRecordingAlert"/>, the original <paramref name="obs"/> if no service is available, or <c>null</c> if the service returned no result.</returns>
public async Task<PatientRecordingAlert?> Map(PatientRecordingAlert obs)
{
if (_service == null) return obs;
@@ -98,6 +125,11 @@ public class CalculatedObservationsService : ICalculatedObservationsService
return result;
}
/// <summary>
/// Asynchronously maps a <see cref="PatientTreatment"/> using the underlying service. If the service is unavailable (null), the original treatment is returned unchanged as a fallback.
/// </summary>
/// <param name="treatment">The patient treatment instance to be mapped or transformed.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the mapped <see cref="PatientTreatment"/> or <c>null</c> if the service returns no result.</returns>
public async Task<PatientTreatment?> Map(PatientTreatment treatment)
{
if (_service == null) return treatment;
@@ -105,6 +137,11 @@ public class CalculatedObservationsService : ICalculatedObservationsService
return result;
}
/// <summary>
/// Maps a <see cref="PatientDiagnosis"/> by delegating to the configured mapping service. If no service is available, the original <paramref name="diagnosis"/> is returned as a fallback.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to be mapped.</param>
/// <returns>A task that yields the mapped <see cref="PatientDiagnosis"/>, or <c>null</c> if the underlying service returns no result.</returns>
public virtual async Task<PatientDiagnosis?> Map(PatientDiagnosis diagnosis)
{
if (_service == null) return diagnosis;
@@ -112,30 +149,58 @@ public class CalculatedObservationsService : ICalculatedObservationsService
return result;
}
/// <summary>
/// Asynchronously calculates medicine observations for a patient based on their active medicines.
/// Delegates the calculation to the underlying service if it has been initialized; otherwise, the call is silently skipped.
/// </summary>
/// <param name="activeMedicines">The list of medicines currently active for the patient.</param>
/// <param name="patientId">The unique identifier of the patient whose medicine observations are being calculated.</param>
public virtual async Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
if (_service != null) await _service.CalculateMedicineObservation(activeMedicines, patientId);
}
/// <summary>
/// Asynchronously calculates the active bolus dosage of opiates for the specified patient by delegating to the underlying service.
/// If the service dependency is not initialized, the call is skipped silently as a no-op fallback.
/// </summary>
/// <param name="patientId">The unique identifier of the patient for whom the active bolus opiates calculation is performed.</param>
public async Task CalculateBolusOpiates(ObjectId patientId)
{
if (_service != null) await _service.CalculateActiveBolus(patientId);
}
/// <summary>
/// Retrieves the active treatments associated with the specified patient.
/// If the underlying service is not available, returns an empty list as a fallback instead of throwing.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments should be retrieved.</param>
/// <returns>A task that yields the collection of active <see cref="PatientTreatment"/> entries for the patient, or an empty list when the service is unavailable.</returns>
public virtual async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
if (_service != null) return await _service.GetActiveTreatmentsByPatient(id);
return new List<PatientTreatment?>();
}
/// <summary>
/// Maps a list of patient observations by delegating to the configured service's pre-mapping logic when available; otherwise, returns an empty list.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be mapped.</param>
/// <returns>A task containing the mapped list of patient observations, or an empty list if no service is configured.</returns>
public virtual async Task<List<PatientObservation>> MapList(List<PatientObservation> listToInsert)
{
if (_service != null) return await _service.PreMapList(listToInsert);
return [];
}
/// <summary>
/// Maps a source alarm onto the given patient observation by delegating to the configured service when available; if no service is configured, returns the original observation unchanged.
/// </summary>
/// <param name="observation">The patient observation onto which the source alarm will be mapped.</param>
/// <param name="observationAlarm">The source alarm to be applied to the observation.</param>
/// <returns>The <see cref="PatientObservation"/> produced by the service mapping, or the original <paramref name="observation"/> when no service is configured.</returns>
public virtual async Task<PatientObservation> MapSourceAlarm(PatientObservation observation,
PatientObservationAlarm observationAlarm)
PatientObservationAlarm observationAlarm)
{
if (_service != null) return await _service.MapSourceAlarm(observation, observationAlarm);
return observation;
@@ -9,18 +9,40 @@ using MongoDB.Driver;
namespace adas_core.Application.Services;
/// <summary>
/// Provides camera-related operations by coordinating the camera repository and point-of-care service, and logging diagnostic information.
/// </summary>
/// <remarks>
/// This service implements <see cref="ICameraService"/> and serves as the application-layer entry point for camera functionality.
/// </remarks>
public class CameraService(ILogger<CameraService> logger, ICameraRepository cameraRepository, IPointOfCareService pointOfCareService) : ICameraService
{
private ICameraRepository _cameraRepository = cameraRepository;
/// <summary>
/// Retrieves a camera by its associated relay identifier from the camera repository.
/// Returns null when no camera is found for the specified relay identifier.
/// </summary>
/// <param name="relayId">The unique identifier of the relay used to look up the camera.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="Camera"/>, or null if no camera is found.</returns>
public Task<Camera?> GetById(ObjectId relayId)
{
return _cameraRepository.GetById(relayId);
}
/// <summary>
/// Retrieves the list of cameras associated with the specified configuration relay identifiers.
/// </summary>
/// <param name="configurationRelayList">The list of configuration relay identifiers used to look up the corresponding cameras.</param>
/// <returns>A <see cref="List{Camera}"/> containing the cameras linked to the provided configuration relay identifiers.</returns>
public List<Camera> GetCameraInList(List<ObjectId> configurationRelayList)
{
return _cameraRepository.GetCameraInList(configurationRelayList);
}
/// <summary>
/// Retrieves a paginated list of cameras, optionally filtered by whether they are currently in use, and resolves the in-use status for each returned camera using the point-of-care service.
/// </summary>
/// <param name="filter">The pagination filter that controls page number, page size, and optional filtering criteria such as the in-use flag.</param>
/// <returns>A paginated response containing the requested cameras, the current page metadata, and the total document count; if no data is found, an empty paginated response is returned.</returns>
public async Task<PaginationResponse<Camera>> GetPaginatedCameras(PaginationFilter filter)
{
var usedCameraIds = await pointOfCareService.FindAllIdCamerasInUse();
@@ -31,8 +53,8 @@ public class CameraService(ILogger<CameraService> logger, ICameraRepository came
{
bool filterInUse = filter.FilteredRequest.InUse.Value;
var filterBuilder = Builders<Camera>.Filter;
var idFilter = filterInUse
var idFilter = filterInUse
? filterBuilder.In(c => c.Id, usedCameraIds)
: filterBuilder.Not(filterBuilder.In(c => c.Id, usedCameraIds));
@@ -45,42 +67,61 @@ public class CameraService(ILogger<CameraService> logger, ICameraRepository came
.Limit(filter.PageSize)
.ToListAsync();
if(data == null) return new PaginationResponse<Camera>([], filter.PageNumber, filter.PageSize, count);
if (data == null) return new PaginationResponse<Camera>([], filter.PageNumber, filter.PageSize, count);
foreach (var camera in data)
{
if (camera == null) continue;
bool isInUse = usedCameraIds.Contains(camera.Id);
// Asignación mediante reflexión para el private set
camera.GetType().GetProperty(nameof(Camera.InUse))
?.SetValue(camera, isInUse);
}
return new PaginationResponse<Camera>(data, filter.PageNumber, filter.PageSize, count);
}
/// <summary>
/// Inserts a new camera into the system after validating its name and ensuring no duplicate exists.
/// Throws an exception if the camera name is null or if another camera with the same name already exists.
/// </summary>
/// <param name="camera">The camera entity to insert.</param>
/// <returns>The inserted camera, or null if the insertion did not return a result.</returns>
/// <exception cref="System.Exception">Thrown when the camera name is null.</exception>
/// <exception cref="System.Exception">Thrown when a camera with the same name already exists.</exception>
public async Task<Camera?> InsertCamera(Camera camera)
{
if (camera.Name == null) throw new Exception("Camera name cannot be null");
var cameraFound = await _cameraRepository.GetByName(camera.Name);
if(cameraFound != null) throw new Exception($"Camera with name {camera.Name} already exists");
if (cameraFound != null) throw new Exception($"Camera with name {camera.Name} already exists");
return await _cameraRepository.InsertOneCamera(camera);
}
/// <summary>
/// Updates an existing camera identified by its unique identifier, returning the updated entity if the operation succeeds.
/// </summary>
/// <param name="objectId">The unique identifier of the camera to update.</param>
/// <param name="camera">The camera data containing the updated values.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated <see cref="Camera"/>, or <c>null</c> if no camera with the specified identifier was found.</returns>
public async Task<Camera?> UpdateCameraById(ObjectId objectId, Camera camera)
{
return await _cameraRepository.UpdateCameraAsync(objectId, camera);
}
/// <summary>
/// Deletes a camera identified by the specified object identifier. Returns <c>false</c> when the camera is not found, and logs and returns <c>false</c> if an error occurs during the operation.
/// </summary>
/// <param name="objectId">The unique identifier of the camera to delete.</param>
/// <returns><c>true</c> if the camera was successfully deleted; otherwise, <c>false</c>.</returns>
public async Task<bool> DeleteCamera(ObjectId objectId)
{
try
{
var cameraToDelete = await _cameraRepository.GetById(objectId);
if(cameraToDelete == null) return false;
if (cameraToDelete == null) return false;
await _cameraRepository.DeleteAsync(cameraToDelete.Id);
return true;
}
@@ -89,9 +130,14 @@ public class CameraService(ILogger<CameraService> logger, ICameraRepository came
logger.LogError(e, e.Message);
return false;
}
}
/// <summary>
/// Retrieves a list of cameras matching the specified search text by delegating to the camera repository.
/// </summary>
/// <param name="textToSearch">The search text used to find cameras by name.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Camera"/> objects that match the search criteria.</returns>
public async Task<List<Camera>> GetSearchByNameCameras(string textToSearch)
{
return await _cameraRepository.GetSearchByNameCameras(textToSearch);
@@ -21,6 +21,12 @@ using MongoDB.Bson;
namespace adas_core.Application.Services;
/// <summary>
/// Provides a concrete implementation of the <see cref="IConfigObservationService"/> contract for observing configuration state.
/// </summary>
/// <remarks>
/// Acts as the default service type that fulfills the configuration observation interface.
/// </remarks>
public class ConfigObservationService : IConfigObservationService
{
private static readonly ConcurrentDictionary<ObjectId, ConfigObservationCached> CachedConfigObservations = new();
@@ -39,8 +45,8 @@ public class ConfigObservationService : IConfigObservationService
private readonly int? _refreshTimeout;
private readonly IUnitService _unitService;
private readonly ICacheService _cacheService;
private readonly CacheSettings? _cacheSettings;
private readonly CacheSettings? _cacheSettings;
private bool IgnoreUnknownObservation =>
_apiSettings.Value.ConfigObservation?.IgnoreUnknownObservation ?? false;
@@ -73,9 +79,14 @@ public class ConfigObservationService : IConfigObservationService
_auditService = auditService;
}
/// <summary>
/// Retrieves all configuration observations using a cache-aside strategy. If no cached value exists, the data is fetched from the repository and cached using the key and TTL determined by the cache settings.
/// </summary>
/// <param name="ct">A token to monitor for cancellation requests.</param>
/// <returns>A collection of all <see cref="ConfigObservation"/> entries, sourced from cache when available or from the repository otherwise.</returns>
public async Task<ICollection<ConfigObservation>> GetAllConfigs(CancellationToken ct = default)
{
var (key, ttl) = CacheKeys.ConfigObservationsAllKeyWithTtl(_cacheSettings);
var result = await _cacheService.GetOrSetObjectAsync(
@@ -88,12 +99,21 @@ public class ConfigObservationService : IConfigObservationService
}
/// <summary>
/// Retrieves a compact representation of all configuration observations by returning the total item count.
/// </summary>
/// <returns>A <see cref="ConfigObservationDto"/> containing the total number of configuration observations.</returns>
public async Task<ConfigObservationDto> GetAllCompact()
{
var count = await _configObservationRepository.Count();
return new ConfigObservationDto { ItemCount = count };
}
/// <summary>
/// Retrieves a paginated list of <see cref="ConfigObservation"/> items from the repository along with the total count, used to build pagination metadata for the response.
/// </summary>
/// <param name="filter">The pagination filter containing the requested page number and page size used to retrieve the items and populate the response metadata.</param>
/// <returns>A <see cref="PaginationResponse{ConfigObservation}"/> containing the items for the requested page and the total count of all available items.</returns>
public async Task<PaginationResponse<ConfigObservation>> GetPaginatedItems(PaginationFilter filter)
{
var result = await _configObservationRepository.GetPaginatedItems(filter);
@@ -103,35 +123,66 @@ public class ConfigObservationService : IConfigObservationService
}
/// <summary>
/// Retrieves a configuration observation by its unique identifier from the repository.
/// Returns null if no matching configuration observation is found.
/// </summary>
/// <param name="id">The unique identifier of the configuration observation to retrieve.</param>
/// <returns>The configuration observation matching the specified identifier, or null if not found.</returns>
public async Task<ConfigObservation?> GetConfigById(ObjectId id)
{
return await _configObservationRepository.FindById(id) ?? null;
}
/// <summary>
/// Retrieves the list of configuration names associated with the specified identifier by delegating to the configuration observation repository.
/// </summary>
/// <param name="id">The identifier used to look up the related configuration names.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of configuration names matching the given identifier.</returns>
public async Task<List<string>> GetConfigNames(string id)
{
return await _configObservationRepository.GetConfigNames(id);
}
/// <summary>
/// Retrieves the list of configuration names from the configuration observation repository.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of configuration names.</returns>
public async Task<List<string>> GetConfigNames()
{
return await _configObservationRepository.GetConfigNames();
}
/// <summary>
/// Determines the retention action to apply for a patient observation by resolving its configured retention policy. Falls back to a "NoDelete" retention policy with no value when the observation has no associated configuration or its retention policy is null.
/// </summary>
/// <typeparam name="T">The patient observation type, constrained to <see cref="BasePatientObservation"/>.</typeparam>
/// <param name="obs">The patient observation for which the retention action is being evaluated.</param>
/// <returns>A task containing the resolved <see cref="ObservatitonRetentionResult"/>, or null when no configuration is available.</returns>
public async Task<ObservatitonRetentionResult?> RetentionActions<T>(T obs) where T : BasePatientObservation
{
var conf = await Get(obs);
return conf is { RetentionPolicy: not null } ?
new ObservatitonRetentionResult(conf.RetentionPolicy.Value, conf.RetentionPolicyValue) :
return conf is { RetentionPolicy: not null } ?
new ObservatitonRetentionResult(conf.RetentionPolicy.Value, conf.RetentionPolicyValue) :
new ObservatitonRetentionResult(RetentionPolicy.NoDelete, null);
}
/// <summary>
/// Resolves the status of a grouped patient observation by retrieving the configuration for the given name and mapping the observation through group-specific, result-specific, or default configuration. Returns <see cref="StatusEnum.Type.Ok"/> when no configuration exists for the name or when the mapping does not produce a status.
/// </summary>
/// <param name="groupedField">The grouped field whose <c>Group</c> key is used to look up group-specific configuration.</param>
/// <param name="result">The grouped observation result whose name is used to look up result-specific configuration.</param>
/// <param name="name">The observation name used to retrieve the configuration.</param>
/// <param name="value">The observation value included in the mapping.</param>
/// <param name="min">The optional minimum reference value included in the mapping.</param>
/// <param name="max">The optional maximum reference value included in the mapping.</param>
/// <returns>A task that resolves to the <see cref="StatusEnum.Type"/> computed from the mapped observation, or <see cref="StatusEnum.Type.Ok"/> when no applicable configuration or mapping status is found.</returns>
public async Task<StatusEnum.Type> GroupedObservationStatus(GroupedField groupedField,
GroupedObservationEnum.Result result,
string name, object value, double? min, double? max)
GroupedObservationEnum.Result result,
string name, object value, double? min, double? max)
{
var conf = await Get(name);
if (conf == null) return StatusEnum.Type.Ok;
@@ -156,6 +207,15 @@ public class ConfigObservationService : IConfigObservationService
return StatusEnum.Type.Ok;
}
/// <summary>
/// Maps a patient observation to a configured representation by looking up its corresponding configuration
/// and applying the mapping. When no matching configuration is found, returns <c>null</c> if unknown
/// observations should be ignored, or the original observation otherwise. If the resolved configuration
/// has an empty name, the mapping is aborted and <c>null</c> is returned.
/// </summary>
/// <param name="obs">The patient observation to be mapped.</param>
/// <param name="onlyByName">If <c>true</c>, the configuration lookup is performed by name only; otherwise the full lookup is used.</param>
/// <returns>The mapped observation, the original observation when unknown observations are allowed, or <c>null</c> when mapping is ignored or the configuration is invalid.</returns>
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
var conf = onlyByName ? await Get(obs, onlyByName) : await Get(obs);
@@ -175,11 +235,17 @@ public class ConfigObservationService : IConfigObservationService
return await MapConf(obs, conf);
}
/// <summary>
/// Removes a configuration observation item by its identifier. Returns <c>null</c> when the item does not exist,
/// otherwise deletes it from the repository and invalidates the cached collection of configuration observations.
/// </summary>
/// <param name="id">The unique identifier of the configuration observation to remove.</param>
/// <returns>The removed <see cref="ConfigObservation"/> if it was found and deleted; otherwise, <c>null</c>.</returns>
public async Task<ConfigObservation?> RemoveConfigItem(ObjectId id)
{
var item = await _configObservationRepository.FindById(id);
if (item == null) return null;
var deleted = await _configObservationRepository.Delete(id);
// Invalidar CACHE (colección completa)
@@ -189,6 +255,11 @@ public class ConfigObservationService : IConfigObservationService
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> by resolving the configuration observation for each of its requested give codes. Looks up the configuration by text when both the coding system and identifier are empty, otherwise by coding system and identifier. Returns <c>null</c> when the configuration is unknown and unknown treatments are ignored, or when the resolved configuration has no name; otherwise returns the original treatment.
/// </summary>
/// <param name="treatment">The patient treatment whose requested give codes are resolved against the configuration store.</param>
/// <returns>The original <see cref="PatientTreatment"/> if a valid configuration is found, or <c>null</c> when the treatment should be discarded.</returns>
public async Task<PatientTreatment?> Map(PatientTreatment treatment)
{
ConfigObservation? conf = null;
@@ -203,8 +274,14 @@ public class ConfigObservationService : IConfigObservationService
return conf.Name == null ? null : treatment;
}
/// <summary>
/// Retrieves a <see cref="ConfigObservation"/> that matches the supplied patient observation, either by name only or by a combination of code, coding system, and parent data fields.
/// </summary>
/// <param name="obs">The patient observation whose matching configuration should be resolved.</param>
/// <param name="onlyByName">When <c>true</c>, the lookup is restricted to matching by <see cref="BasePatientObservation.Name"/> only; otherwise matching also considers code, coding system, and parent observation data.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the matched <see cref="ConfigObservation"/> processed via <c>Process</c>, or <c>null</c> if no configuration items are available or no match is found.</returns>
public async Task<ConfigObservation?> Get<T>(T obs, bool onlyByName = false)
where T : BasePatientObservation
where T : BasePatientObservation
{
var items = await GetAllConfigs();
if (items.Count == 0) return null;
@@ -273,6 +350,13 @@ public class ConfigObservationService : IConfigObservationService
// .ToList() ?? [];
// }
/// <summary>
/// Retrieves a <see cref="ConfigObservation"/> that matches the specified coding system and code, processing it before returning.
/// If no matching observation is found, a warning is logged and <see langword="null"/> is returned.
/// </summary>
/// <param name="codingSystem">The coding system identifier used to filter the observation. May be <see langword="null"/>.</param>
/// <param name="code">The code value used to filter the observation. May be <see langword="null"/>.</param>
/// <returns>A processed <see cref="ConfigObservation"/> if a match is found; otherwise, <see langword="null"/>.</returns>
public async Task<ConfigObservation?> GetByCodeSysAndCode(string? codingSystem, string? code)
{
var filteredResult = await _configObservationRepository.GetByCodeSysAndCode(codingSystem, code);
@@ -283,6 +367,11 @@ public class ConfigObservationService : IConfigObservationService
}
/// <summary>
/// Retrieves a <see cref="ConfigObservation"/> by its name, returning <c>null</c> when the name is not provided or the configuration is not found.
/// </summary>
/// <param name="name">The name of the configuration to look up; if null or empty, the method returns <c>null</c>.</param>
/// <returns>A <see cref="ConfigObservation"/> when a matching configuration is found and successfully processed; otherwise, <c>null</c>.</returns>
public async Task<ConfigObservation?> Get(string? name)
{
if (string.IsNullOrEmpty(name)) return null;
@@ -298,6 +387,13 @@ public class ConfigObservationService : IConfigObservationService
return await Process(result);
}
/// <summary>
/// Updates an existing <see cref="ConfigObservation"/> identified by its id and returns the updated entity.
/// Throws a not found exception when the configuration observation does not exist, invalidates the related cache entries, and records an audit log of the change.
/// </summary>
/// <param name="configObservationItem">The configuration observation payload containing the identifier of the record to update.</param>
/// <returns>The updated <see cref="ConfigObservation"/>.</returns>
/// <exception cref="NotFoundException">Thrown when no configuration observation exists for the supplied id.</exception>
public async Task<ConfigObservation?> UpdateConfig(ConfigObservation configObservationItem)
{
// if (!configObservationItem.Id.HasValue)
@@ -306,15 +402,21 @@ public class ConfigObservationService : IConfigObservationService
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var updatedConfig = await _configObservationRepository.Update(configObservation);
// Invalidar CACHE (colección completa)
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, configObservation,
updatedConfig!);
return updatedConfig;
}
/// <summary>
/// Retrieves configuration observation items matching the specified name. Returns the matching items if any are found; otherwise logs a warning and throws a <see cref="NotFoundException"/>.
/// </summary>
/// <param name="name">The name used to look up the configuration observation items.</param>
/// <returns>A collection of <see cref="ConfigObservation"/> items matching the specified name.</returns>
/// <exception cref="NotFoundException">Thrown when no configuration observation items are found for the given name.</exception>
public async Task<IEnumerable<ConfigObservation>?> GetConfigObservationItem(string name)
{
var configObservationItems = await _configObservationRepository.FindAllByName(name);
@@ -324,8 +426,18 @@ public class ConfigObservationService : IConfigObservationService
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
/// <summary>
/// Retrieves a single <see cref="ConfigObservation"/> item that matches the specified code, coding system, name, and original name.
/// Throws a not-found exception when no matching item exists in the repository.
/// </summary>
/// <param name="code">The code used to identify the configuration observation item.</param>
/// <param name="codingSystem">The coding system associated with the item.</param>
/// <param name="name">The name of the configuration observation item.</param>
/// <param name="originalName">The original name of the configuration observation item.</param>
/// <returns>The matching <see cref="ConfigObservation"/> item.</returns>
/// <exception cref="NotFoundException">Thrown when no matching configuration observation item is found.</exception>
public async Task<ConfigObservation?> GetSingleConfigObservationItem(string? code, string? codingSystem,
string? name, string? originalName)
string? name, string? originalName)
{
var matchingItem =
await _configObservationRepository.GetSingleConfigObservationItem(code, codingSystem, name, originalName);
@@ -335,6 +447,13 @@ public class ConfigObservationService : IConfigObservationService
return matchingItem;
}
/// <summary>
/// Deletes a single configuration observation item, invalidating the related cache entries and recording an audit log of the operation.
/// Throws a conflict exception if the item does not exist or the delete operation cannot be completed.
/// </summary>
/// <param name="configObservationItem">The configuration observation item to delete; its identifier is used to locate the existing record.</param>
/// <returns>A task that resolves to <c>true</c> when the item is successfully deleted.</returns>
/// <exception cref="ConflictException">Thrown when no configuration observation is found with the specified identifier, or when the underlying delete operation fails.</exception>
public async Task<bool> DeleteSingleConfigObservationItem(ConfigObservation configObservationItem)
{
var configObservation = await _configObservationRepository.FindById(configObservationItem.Id) ??
@@ -344,23 +463,34 @@ public class ConfigObservationService : IConfigObservationService
_ = await _configObservationRepository.DeleteAsync(configObservation.Id) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
// Invalidar CACHE (colección completa)
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation,
configObservation);
return true;
}
/// <summary>
/// Retrieves configuration observation items that match the specified name.
/// </summary>
/// <param name="name">The name used to filter configuration observation items.</param>
/// <returns>A collection of <see cref="ConfigObservation"/> items matching the specified name.</returns>
public async Task<IEnumerable<ConfigObservation>> GetConfigObservationItemsByName(string name)
{
return await _configObservationRepository.FindAllByName(name);
}
/// <summary>
/// Creates a new configuration observation after verifying that no existing record shares the same identifier.
/// </summary>
/// <param name="configObservation">The configuration observation entity to persist.</param>
/// <returns>The created <see cref="ConfigObservation"/> if the operation succeeds.</returns>
/// <exception cref="BadRequestException">Thrown when a configuration observation with the same identifier already exists.</exception>
public async Task<ConfigObservation?> CreateConfig(ConfigObservation configObservation)
{
var existing = await _configObservationRepository.FindById(configObservation.Id);
if (existing != null)
throw new BadRequestException(HttpEnum.ErrorMessage.ConflictCreationFailed);
@@ -371,6 +501,13 @@ public class ConfigObservationService : IConfigObservationService
}
/// <summary>
/// Removes a configuration item identified by its name, creating an audit log entry prior to deletion and invalidating the related cache.
/// Throws a conflict exception when no configuration item with the specified name is found.
/// </summary>
/// <param name="itemName">The name of the configuration item to remove.</param>
/// <returns>The removed <see cref="ConfigObservation"/>, or <c>null</c> if the repository did not return a result.</returns>
/// <exception cref="ConflictException">Thrown when no configuration item is found with the specified name.</exception>
public async Task<ConfigObservation?> RemoveConfigItem(string itemName)
{
var configObservation = await GetConfigByName(itemName) ??
@@ -380,14 +517,20 @@ public class ConfigObservationService : IConfigObservationService
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation,
configObservation);
var result = await _configObservationRepository.Delete(configObservation.Id!);
var result = await _configObservationRepository.Delete(configObservation.Id!);
// Invalidar CACHE (colección completa)
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
return result;
}
/// <summary>
/// Retrieves a configuration observation by its identifier, using an in-memory cache with a configurable refresh timeout to reduce repository calls.
/// Returns the cached value when available and not yet expired; otherwise fetches from the repository and caches the result, falling back to a new empty <see cref="ConfigObservation"/> when the repository does not find a matching record.
/// </summary>
/// <param name="configObservationId">The unique identifier of the configuration observation to retrieve.</param>
/// <returns>The configuration observation obtained from cache or repository, or a new empty instance when no matching record exists.</returns>
public async Task<ConfigObservation?> GetConfig(ObjectId configObservationId)
{
RefreshCachedConfigObservations();
@@ -408,6 +551,13 @@ public class ConfigObservationService : IConfigObservationService
return cached.ConfigObservation;
}
/// <summary>
/// Retrieves a configuration observation by its name, using a time-limited in-memory cache before falling back to the repository.
/// Returns <c>null</c> if <paramref name="name"/> is null, empty, or whitespace, or if no matching configuration exists in the cache or repository.
/// Cache hits require a non-expired <c>NextRefresh</c> and use a case-insensitive name comparison.
/// </summary>
/// <param name="name">The case-insensitive name of the configuration observation to look up.</param>
/// <returns>The matching <see cref="ConfigObservation"/>, or <c>null</c> if not found or the name is invalid.</returns>
public async Task<ConfigObservation?> GetConfigByName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return null;
@@ -432,10 +582,14 @@ public class ConfigObservationService : IConfigObservationService
};
CachedConfigObservations[config.Id!] = newCachedItem;
return config;
}
/// <summary>
/// Removes expired entries from the cached configuration observations when a refresh timeout is configured,
/// performing an early return if no refresh timeout is set.
/// </summary>
private void RefreshCachedConfigObservations()
{
if (!_refreshTimeout.HasValue) return;
@@ -449,6 +603,11 @@ public class ConfigObservationService : IConfigObservationService
foreach (var key in keysToRemove) CachedConfigObservations.TryRemove(key, out _);
}
/// <summary>
/// Processes a configuration observation by applying the default retention policy when none is set, and normalizing the associated retention policy value based on the selected policy.
/// </summary>
/// <param name="confItem">The configuration observation to process. Its retention policy and retention policy value are updated in place.</param>
/// <returns>A task containing the processed <see cref="ConfigObservation"/>.</returns>
private Task<ConfigObservation> Process(ConfigObservation confItem)
{
confItem.RetentionPolicy ??= _defaultRetentionPolicy;
@@ -465,6 +624,11 @@ public class ConfigObservationService : IConfigObservationService
return Task.FromResult(confItem);
}
/// <summary>
/// Removes expired entries from the cached configuration observation keys based on the configured refresh timeout.
/// If no refresh timeout is set, the method returns without performing any cleanup.
/// Any exceptions encountered during the cleanup are caught and logged.
/// </summary>
private void RefreshCachedConfigObservationKeys()
{
try
@@ -485,6 +649,15 @@ public class ConfigObservationService : IConfigObservationService
}
}
/// <summary>
/// Maps configuration settings from a <see cref="ConfigObservation"/> onto a <see cref="BasePatientObservation"/>,
/// applying alert and warning thresholds, evaluating dynamic level conditions, and computing the observation status
/// for numeric and string values. Handles <see cref="PatientObservation"/> and <see cref="PatientObservationAlarm"/>
/// subtypes with their respective properties, including expiration, units, colors, and UI configuration.
/// </summary>
/// <param name="obs">The observation instance to enrich with configuration values. Modified in place.</param>
/// <param name="conf">The configuration observation providing thresholds, colors, expiration, and other settings to apply.</param>
/// <returns>The mapped observation, returned as-is when the observation's coding system is configured to skip status calculation.</returns>
private async Task<T?> MapConf<T>(T obs, ConfigObservation conf) where T : BasePatientObservation
{
_logger.LogTrace("Mapping observation: {obs}", obs);
@@ -628,12 +801,21 @@ public class ConfigObservationService : IConfigObservationService
return obs;
}
/// <summary>
/// Represents a private cached container for configuration observation data.
/// </summary>
/// <remarks>
/// This type is intended to be used internally to store and reuse configuration observation results.
/// </remarks>
private class ConfigObservationCached
{
public DateTime NextRefresh { get; set; }
public ConfigObservation? ConfigObservation { get; set; }
}
/// <summary>
/// Represents a private cache entry for configuration observation keys, used to store and retrieve previously computed key values associated with configuration observations.
/// </summary>
private class ConfigObservationKeyCached
{
public DateTime NextRefresh { get; set; }
@@ -27,6 +27,11 @@ public class ConfigPumpsService(
private readonly string _key = apiSettings.Value.ConfigPumpsKey ?? "PV1";
private readonly int? _refreshTimeout = apiSettings.Value.ConfigObservation?.Refresh;
/// <summary>
/// Maps a <see cref="PumpObservation"/> using its alarm type configuration. Returns the original observation unchanged when configuration-based pump mapping is not required or when no matching configuration is found.
/// </summary>
/// <param name="obs">The pump observation to map, containing the alarm type used for configuration lookup.</param>
/// <returns>The mapped pump observation, or the original observation when mapping is skipped or the configuration lookup yields no result.</returns>
public async Task<PumpObservation> Map(PumpObservation obs)
{
if (!_configPumpsRequired) return obs;
@@ -40,22 +45,44 @@ public class ConfigPumpsService(
return await MapConf(obs, conf);
}
/// <summary>
/// Retrieves all available pump configurations from the underlying repository.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="ConfigPumps"/> if any are available, or <c>null</c> when no configurations exist.</returns>
public async Task<List<ConfigPumps>?> GetAllPumpConfigs()
{
return await configPumpsRepository.GetAllConfigs();
}
/// <summary>
/// Retrieves the configuration for a pump identified by its unique identifier from the configuration repository.
/// Returns <c>null</c> when no matching pump configuration is found.
/// </summary>
/// <param name="id">The unique identifier of the pump configuration to retrieve.</param>
/// <returns>A <see cref="ConfigPumps"/> instance if a matching configuration is found; otherwise, <c>null</c>.</returns>
public async Task<ConfigPumps?> GetPumpConfigById(string id)
{
return await configPumpsRepository.FindById(id);
}
/// <summary>
/// Retrieves the list of configuration items associated with the config pump identified by the given identifier. Returns <c>null</c> when no matching config pump is found in the repository.
/// </summary>
/// <param name="id">The unique identifier of the config pump to look up.</param>
/// <returns>A task that resolves to the list of <see cref="ConfigPumpItem"/> entries, or <c>null</c> if the config pump does not exist.</returns>
public async Task<List<ConfigPumpItem>?> GetConfigItems(string id)
{
var result = await configPumpsRepository.FindById(id);
return result?.Items;
}
/// <summary>
/// Updates the pump configuration in the repository and records an audit log of the change.
/// Throws a <see cref="ConflictException"/> if the update operation returns null.
/// </summary>
/// <param name="pumpConfig">The pump configuration to update, identified by its <see cref="ConfigPumps.Id"/>.</param>
/// <returns>A task representing the asynchronous operation, containing the updated <see cref="ConfigPumps"/>.</returns>
/// <exception cref="ConflictException">Thrown when the update operation fails.</exception>
public async Task<ConfigPumps?> UpdatePumpConfig(ConfigPumps pumpConfig)
{
var oldPumpConfig = configPumpsRepository.FindById(pumpConfig.Id);
@@ -65,6 +92,12 @@ public class ConfigPumpsService(
return newPumpConfig;
}
/// <summary>
/// Inserts a new pump configuration into the repository, records an audit log entry for the operation, and returns the inserted configuration. If the configuration cannot be retrieved after insertion or any exception occurs, the error is logged and the method returns <c>null</c>.
/// </summary>
/// <param name="pumpConfig">The pump configuration to insert.</param>
/// <returns>The inserted <see cref="ConfigPumps"/> on success; otherwise, <c>null</c> when an error occurs during the operation.</returns>
/// <exception cref="ConflictException">Thrown when the inserted configuration cannot be found in the repository after insertion.</exception>
public async Task<ConfigPumps?> InsertPumpConfig(ConfigPumps pumpConfig)
{
try
@@ -82,6 +115,11 @@ public class ConfigPumpsService(
}
}
/// <summary>
/// Deletes a pump configuration asynchronously, auditing the change on success and returning <c>false</c> if the repository operation reports an error or an exception is thrown.
/// </summary>
/// <param name="config">The <see cref="ConfigPumps"/> instance representing the pump configuration to delete.</param>
/// <returns>A task that resolves to <c>true</c> when the configuration is deleted and the audit log is recorded; <c>false</c> when the delete operation fails or an exception occurs.</returns>
public async Task<bool> DeletePumpConfig(ConfigPumps config)
{
try
@@ -104,23 +142,40 @@ public class ConfigPumpsService(
}
}
/// <summary>
/// Determines the retention actions to apply for a pump observation based on its configuration.
/// When a retention policy is configured, returns the configured policy and its value; otherwise, falls back to <see cref="RetentionPolicy.NoDelete"/> with a null value.
/// </summary>
/// <param name="obs">The pump observation for which to evaluate the retention policy.</param>
/// <returns>A task containing the retention result with the applicable policy and associated value, or a default NoDelete result when no policy is configured.</returns>
public async Task<ObservatitonRetentionResult?> RetentionActions(PumpObservation obs)
{
//TODO sacarlo de la configuración específica de Bombas
var conf = await Get(obs);
return conf is { RetentionPolicy: not null } ?
new ObservatitonRetentionResult(conf.RetentionPolicy.Value, conf.RetentionPolicyValue) :
return conf is { RetentionPolicy: not null } ?
new ObservatitonRetentionResult(conf.RetentionPolicy.Value, conf.RetentionPolicyValue) :
new ObservatitonRetentionResult(RetentionPolicy.NoDelete, null);
}
/// <summary>
/// Maps the UI configuration from a <see cref="ConfigPumpItem"/> onto a <see cref="PumpObservation"/>, assigning the configuration only when it is provided and non-empty.
/// </summary>
/// <param name="obs">The pump observation that will receive the UI configuration.</param>
/// <param name="conf">The configuration source whose UI configuration is applied to <paramref name="obs"/> when present.</param>
/// <returns>A completed <see cref="Task{PumpObservation}"/> containing the updated observation.</returns>
private static Task<PumpObservation> MapConf(PumpObservation obs, ConfigPumpItem conf)
{
if (conf.UiConfiguration != null && conf.UiConfiguration.Count != 0)
if (conf.UiConfiguration != null && conf.UiConfiguration.Count != 0)
obs.UiConfiguration = conf.UiConfiguration;
return Task.FromResult(obs);
}
/// <summary>
/// Retrieves a <see cref="ConfigPumpItem"/> matching the specified alarm type by parsing the input string into a <see cref="PumpEnum.AlarmType"/> and searching the configuration items.
/// </summary>
/// <param name="alarmType">The string representation of the alarm type to look up; if it cannot be parsed into a valid <see cref="PumpEnum.AlarmType"/>, the method returns <c>null</c>.</param>
/// <returns>A <see cref="ConfigPumpItem"/> whose <c>AlarmType</c> matches the parsed value, or <c>null</c> if parsing fails or no matching item is found.</returns>
private async Task<ConfigPumpItem?> Get(string alarmType)
{
if (!Enum.TryParse(alarmType, out PumpEnum.AlarmType alarmTypeParsed))
@@ -129,11 +184,22 @@ public class ConfigPumpsService(
return result?.Items?.FirstOrDefault(i => i.AlarmType == alarmTypeParsed);
}
/// <summary>
/// Retrieves the configuration item associated with the specified pump observation by matching its message type against the loaded configuration entries.
/// Returns null when the configuration, its items collection, or a matching entry is not found.
/// </summary>
/// <param name="pobs">The pump observation whose <c>MessageType</c> is used to locate the corresponding configuration entry.</param>
/// <returns>A <see cref="ConfigPumpItem"/> matching the observation's message type, or <c>null</c> if the configuration is unavailable or no matching item exists.</returns>
private async Task<ConfigPumpItem?> Get(PumpObservation pobs)
{
var config = await GetConfig();
var config = await GetConfig();
return config?.Items?.FirstOrDefault(i => i.Type == pobs.MessageType);
}
/// <summary>
/// Asynchronously retrieves the list of configuration pump items by fetching the current configuration.
/// Returns a null list if the underlying configuration is not available.
/// </summary>
/// <returns>A task containing a list of <see cref="ConfigPumpItem"/> objects, or <c>null</c> if the configuration could not be retrieved.</returns>
public async Task<List<ConfigPumpItem>?> Get()
{
var result = await GetConfig();
@@ -141,11 +207,16 @@ public class ConfigPumpsService(
}
/// <summary>
/// Retrieves the configuration associated with the current key, returning a cached value when it has not yet expired.
/// Falls back to fetching from the repository when the cache is missing or stale, and returns <c>null</c> if an error occurs during retrieval.
/// </summary>
/// <returns>A task containing the <see cref="ConfigPumps"/> instance if available; otherwise, <c>null</c> when the repository lookup fails.</returns>
private async Task<ConfigPumps?> GetConfig()
{
try
{
if (_config != null && DateTime.Now <= _nextRefresh)
if (_config != null && DateTime.Now <= _nextRefresh)
return _config;
_config = await configPumpsRepository.FindById(_key);
_nextRefresh = _refreshTimeout.HasValue
@@ -24,15 +24,27 @@ public class ConfigUnitsService(
private readonly string _key = apiSettings.Value.ConfigUnitsKey ?? "PV1";
private readonly int? _refreshTimeout = apiSettings.Value.ConfigObservation?.Refresh;
/// <summary>
/// Maps a patient observation using the unit configuration when configuration units are required.
/// If configuration units are not required, the observation's units are not specified, or no matching configuration is found, the original observation is returned unchanged.
/// </summary>
/// <param name="obs">The patient observation to be mapped.</param>
/// <returns>The mapped observation when a matching unit configuration is resolved; otherwise, the original observation.</returns>
public async Task<T> Map<T>(T obs) where T : BasePatientObservation
{
if (!_configUnitsRequired) return obs;
var conf = !string.IsNullOrEmpty(obs.Units) ? await Get(obs.Units) : null;
return conf == null ? obs : MapConf(obs, conf);
}
/// <summary>
/// Maps a <see cref="PumpObservation"/> by processing its pump value properties.
/// If configuration units are not required, the observation is returned unchanged; otherwise, the pump values are mapped via <c>MapPumpValues</c> and the observation is returned.
/// </summary>
/// <param name="obs">The <see cref="PumpObservation"/> to be mapped.</param>
/// <returns>The mapped <see cref="PumpObservation"/>, returned as-is when units configuration is not required or after pump value mapping otherwise.</returns>
public async Task<PumpObservation> Map(PumpObservation obs)
{
if (!_configUnitsRequired) return obs;
@@ -42,7 +54,7 @@ public class ConfigUnitsService(
// var conf = !string.IsNullOrEmpty(obs.Units) ? await Get(obs.Units) : null;
// return conf == null ? obs : MapConf(obs, conf);
return obs;
}
@@ -81,6 +93,12 @@ public class ConfigUnitsService(
}
/// <summary>
/// Maps a configuration unit value onto a patient observation. If the observation is not a <see cref="PatientObservation"/>, the original observation is returned unchanged; otherwise, its <c>Units</c> property is assigned from the configuration unit item's value.
/// </summary>
/// <param name="obs">The base patient observation to which the configured unit value will be applied.</param>
/// <param name="conf">The configuration unit item whose <c>Value</c> is used as the unit to assign.</param>
/// <returns>The input observation, with <c>Units</c> set from <paramref name="conf"/> when applicable, or the unchanged observation when it is not a <see cref="PatientObservation"/>.</returns>
private T MapConf<T>(T obs, ConfigUnitItem conf) where T : BasePatientObservation
{
if (obs is not PatientObservation pobs) return obs;
@@ -91,6 +109,12 @@ public class ConfigUnitsService(
return obs;
}
/// <summary>
/// Retrieves a configuration unit item by its unique code from the configuration store.
/// Returns null when the configuration cannot be loaded or when no item matches the specified code.
/// </summary>
/// <param name="code">The unique code identifier of the configuration unit item to look up.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the matching <see cref="ConfigUnitItem"/>, or null if the configuration is unavailable or no item is found.</returns>
public async Task<ConfigUnitItem?> Get(string code)
{
var result = await GetConfig();
@@ -98,6 +122,15 @@ public class ConfigUnitsService(
return result?.Items?.FirstOrDefault(i => i.Code == code);
}
/// <summary>
/// Retrieves the <see cref="ConfigUnits"/> configuration associated with the current key, using a time-based cache
/// to avoid repeated repository calls before the configured refresh timeout elapses. On any failure during the
/// repository lookup, the error is logged and <c>null</c> is returned.
/// </summary>
/// <returns>
/// A <see cref="Task{TResult}"/> containing the cached or freshly fetched <see cref="ConfigUnits"/>, or
/// <c>null</c> if the repository lookup fails.
/// </returns>
private async Task<ConfigUnits?> GetConfig()
{
try
@@ -6,60 +6,124 @@ using MongoDB.Bson;
namespace adas_core.Application.Services;
/// <summary>
/// Provides the default implementation of the <see cref="ICalculatedObservations"/> interface.
/// </summary>
public class DefaultCalculatedObservations : ICalculatedObservations
{
/// <summary>
/// Asynchronously calculates medicine observations for a patient based on their currently active medicines.
/// </summary>
/// <param name="activeMedicines">The list of medicines currently active for the patient, used as the basis for the observation calculation.</param>
/// <param name="patientId">The identifier of the patient whose medicine observations are being calculated.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
{
return Task.CompletedTask;
}
/// <summary>
/// Calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active bolus is being calculated.</param>
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
{
return Task.CompletedTask;
}
/// <summary>
/// Returns the provided patient observation as a completed task, preserving the original instance for further processing.
/// </summary>
/// <param name="obs">The patient observation to map.</param>
/// <param name="onlyByName">A flag indicating whether mapping should be performed by name only.</param>
/// <returns>A completed <see cref="Task{T}"/> containing the provided observation.</returns>
public Task<T?> Map<T>(T obs, bool onlyByName) where T : BasePatientObservation
{
return Task.FromResult(obs)!;
}
{
return Task.FromResult(obs)!;
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> instance to a completed task, returning the provided treatment unchanged.
/// </summary>
/// <param name="treatment">The patient treatment to map.</param>
/// <returns>A <see cref="Task{PatientTreatment}"/> that completes with the supplied <paramref name="treatment"/>.</returns>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
return Task.FromResult(treatment);
}
{
return Task.FromResult(treatment);
}
/// <summary>
/// Retrieves the active treatments associated with the specified patient identifier.
/// Returns an empty collection, typically serving as a stub or fallback when no treatments are available.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments are being retrieved.</param>
/// <returns>A task representing the asynchronous operation, containing an enumerable collection of the patient's active treatments, or an empty collection if none are found.</returns>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
return Task.FromResult<IEnumerable<PatientTreatment?>>(new List<PatientTreatment?>());
}
{
return Task.FromResult<IEnumerable<PatientTreatment?>>(new List<PatientTreatment?>());
}
/// <summary>
/// Returns the provided patient diagnosis as-is, wrapped in a completed task for asynchronous compatibility.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to map.</param>
/// <returns>A task that represents the asynchronous operation, containing the provided patient diagnosis.</returns>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
{
return Task.FromResult(diagnosis);
}
/// <summary>
/// Maps a <see cref="PumpObservation"/> instance to a target <see cref="PumpObservation"/> representation asynchronously.
/// </summary>
/// <param name="pumpObservation">The source <see cref="PumpObservation"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the resulting <see cref="PumpObservation"/>.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the mapping logic has not been implemented yet.</exception>
public Task<PumpObservation> Map(PumpObservation pumpObservation)
{
throw new NotImplementedException();
}
{
throw new NotImplementedException();
}
/// <summary>
/// Processes a new patient observation to address potential time inconsistency with the previous observation, returning the observation unchanged.
/// </summary>
/// <param name="newObservation">The new patient observation to evaluate for time consistency with the prior observation.</param>
/// <returns>A task that represents the asynchronous operation, containing the provided patient observation.</returns>
public Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
return Task.FromResult<PatientObservation?>(newObservation);
}
{
return Task.FromResult<PatientObservation?>(newObservation);
}
/// <summary>
/// Performs a pre-mapping step on a list of patient observations before further processing, returning the list unchanged.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped.</param>
/// <returns>A completed task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm onto a <see cref="PatientObservation"/> and returns the observation wrapped in a completed task.
/// In the current implementation, the observation is returned as-is without applying the supplied alarm.
/// </summary>
/// <param name="obs">The patient observation to be returned by the mapping.</param>
/// <param name="alarmToInsert">The patient observation alarm intended to be associated with the observation.</param>
/// <returns>A completed <see cref="Task{TResult}"/> containing the <see cref="PatientObservation"/>.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm notification based on the provided patient observation, associated name, and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm being sent.</param>
/// <param name="code">The optional alarm code that categorizes the type of alarm; may be null when no specific code applies.</param>
/// <exception cref="NotImplementedException">Thrown in all cases, as the method has not been implemented yet.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
{
throw new NotImplementedException();
}
}
+230 -176
View File
@@ -9,6 +9,10 @@ using MongoDB.Bson;
namespace adas_core.Application.Services;
/// <summary>
/// Provides the concrete implementation of the <see cref="IDeviceService"/> contract,
/// handling device-related service operations defined by the interface.
/// </summary>
public class DeviceService : IDeviceService
{
private readonly IDeviceRepository _deviceRepository;
@@ -34,213 +38,263 @@ public class DeviceService : IDeviceService
_alarmService = alarmService;
}
/// <summary>
/// Converts a <see cref="DeviceDto"/> into a <see cref="Device"/> entity by mapping its properties. Applies a fallback to an empty list when <c>PointOfCareIds</c> is null, and to a new <see cref="DeviceSettings"/> instance when <c>Settings</c> is null.
/// </summary>
/// <param name="dto">The data transfer object containing the device information to convert.</param>
/// <returns>A new <see cref="Device"/> entity populated with the values from the supplied DTO.</returns>
public Device ToEntity(DeviceDto dto)
{
return new Device()
{
DeviceType = dto.DeviceType,
MacAddr = dto.MacAddr,
SerialNumber = dto.SerialNumber,
Name = dto.Name,
Battery = dto.Battery,
Color = dto.Color,
Connected = dto.Connected,
Ready = dto.Ready,
Uuid = dto.Uuid,
Key = dto.Key,
CreatedAt = dto.CreatedAt,
UpdatedAt = dto.UpdatedAt,
PointOfCareIds = dto.PointOfCareIds ?? new List<ObjectId>(),
Settings = dto.Settings ?? new DeviceSettings()
};
}
return new Device()
{
DeviceType = dto.DeviceType,
MacAddr = dto.MacAddr,
SerialNumber = dto.SerialNumber,
Name = dto.Name,
Battery = dto.Battery,
Color = dto.Color,
Connected = dto.Connected,
Ready = dto.Ready,
Uuid = dto.Uuid,
Key = dto.Key,
CreatedAt = dto.CreatedAt,
UpdatedAt = dto.UpdatedAt,
PointOfCareIds = dto.PointOfCareIds ?? new List<ObjectId>(),
Settings = dto.Settings ?? new DeviceSettings()
};
}
/// <summary>
/// Creates a new device entity from the provided DTO, sets the creation and update timestamps to the current UTC time, and persists it via the device repository.
/// </summary>
/// <param name="deviceDto">The data transfer object containing the device information to be mapped and stored.</param>
/// <returns>The created <see cref="Device"/> entity after successful insertion, or <see langword="null"/> if the device could not be created.</returns>
public async Task<Device?> Create(DeviceDto deviceDto)
{
var device = ToEntity(deviceDto);
device.CreatedAt = DateTime.UtcNow;
device.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.InsertOneAsync(device);
return device;
}
{
var device = ToEntity(deviceDto);
device.CreatedAt = DateTime.UtcNow;
device.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.InsertOneAsync(device);
return device;
}
/// <summary>
/// Deletes an object identified by the specified <paramref name="objectId"/> by delegating to the device repository.
/// Returns <c>true</c> when the repository's delete operation yields a non-null result, and <c>false</c> when the result is <c>null</c> (e.g., the object was not found or could not be deleted).
/// </summary>
/// <param name="objectId">The unique identifier of the object to delete.</param>
/// <returns>A task that resolves to <c>true</c> if the object was deleted; otherwise, <c>false</c>.</returns>
public async Task<bool> Delete(ObjectId objectId)
{
return await _deviceRepository.DeleteAsync(objectId) != null;
}
{
return await _deviceRepository.DeleteAsync(objectId) != null;
}
/// <summary>
/// Updates an existing device by mapping the provided DTO to a device entity, setting the update timestamp, and persisting the changes through the repository.
/// </summary>
/// <param name="deviceDto">The data transfer object containing the updated device information.</param>
/// <returns>The updated <see cref="Device"/> entity, or <c>null</c> if no device is returned.</returns>
public async Task<Device?> Update(DeviceDto deviceDto)
{
var device = ToEntity(deviceDto);
device.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.UpdateOneAsync(device.Id, device);
return device;
}
{
var device = ToEntity(deviceDto);
device.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.UpdateOneAsync(device.Id, device);
return device;
}
/// <summary>
/// Processes an incoming device event by locating an existing device or creating a new one, then handling device-type-specific logic. Looks up the device using the MAC address, serial number, UUID, or key in that order, falling back to creating a new record when no match is found. When the device is a button, additional button management logic is invoked.
/// </summary>
/// <param name="deviceDto">The data transfer object containing the device information from the event, used for lookup and creation.</param>
/// <returns>The existing or newly created <see cref="Device"/> associated with the event.</returns>
public async Task<Device?> ReceiveEvent(DeviceDto deviceDto)
{
Device? deviceExist = null;
if (!string.IsNullOrEmpty(deviceDto.MacAddr))
{
deviceExist = await _deviceRepository.FindByMacAddr(deviceDto.MacAddr);
Device? deviceExist = null;
if (!string.IsNullOrEmpty(deviceDto.MacAddr))
{
deviceExist = await _deviceRepository.FindByMacAddr(deviceDto.MacAddr);
}
if (deviceExist == null && deviceDto.SerialNumber != null)
{
deviceExist = await _deviceRepository.FindBySerialNumber(deviceDto.SerialNumber);
}
if (deviceExist == null && deviceDto.Uuid != null)
{
deviceExist = await _deviceRepository.FindByUuid(deviceDto.Uuid);
}
if (deviceExist == null && deviceDto.Key != null)
{
deviceExist = await _deviceRepository.FindByKey(deviceDto.Key);
}
if (deviceExist == null)
{
deviceExist = ToEntity(deviceDto);
deviceExist.CreatedAt = DateTime.UtcNow;
deviceExist.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.InsertOneAsync(deviceExist);
}
else
{
await _deviceRepository.UpdateDeviceStats(deviceExist.Id, deviceDto);
}
switch (deviceDto.DeviceType)
{
case DeviceType.Unknown:
break;
case DeviceType.Button:
await ManageDeviceButton(deviceExist, deviceDto);
break;
}
return deviceExist;
}
if (deviceExist == null && deviceDto.SerialNumber != null)
{
deviceExist = await _deviceRepository.FindBySerialNumber(deviceDto.SerialNumber);
}
if (deviceExist == null && deviceDto.Uuid != null)
{
deviceExist = await _deviceRepository.FindByUuid(deviceDto.Uuid);
}
if (deviceExist == null && deviceDto.Key != null)
{
deviceExist = await _deviceRepository.FindByKey(deviceDto.Key);
}
if (deviceExist == null)
{
deviceExist = ToEntity(deviceDto);
deviceExist.CreatedAt = DateTime.UtcNow;
deviceExist.UpdatedAt = DateTime.UtcNow;
await _deviceRepository.InsertOneAsync(deviceExist);
}
else
{
await _deviceRepository.UpdateDeviceStats(deviceExist.Id, deviceDto);
}
switch (deviceDto.DeviceType)
{
case DeviceType.Unknown:
break;
case DeviceType.Button:
await ManageDeviceButton(deviceExist, deviceDto);
break;
}
return deviceExist;
}
/// <summary>
/// Handles device button actions by dispatching the configured action type (sending an observation or alarm) when both the device's configured action and the received click event are present.
/// </summary>
/// <param name="deviceExist">The existing device whose configured action settings determine which action to execute.</param>
/// <param name="deviceDto">The incoming device event payload providing the click type that triggers the action.</param>
private async Task ManageDeviceButton(Device deviceExist, DeviceDto deviceDto)
{
if (deviceExist.Settings?.Action?.Type != null && deviceDto.Event?.ClickType != null)
{
switch (deviceExist.Settings.Action.Type)
if (deviceExist.Settings?.Action?.Type != null && deviceDto.Event?.ClickType != null)
{
case DeviceActionType.SendObs:
await SendObservationOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds);
break;
case DeviceActionType.SendAlarm:
await SendAlarmOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds);
break;
switch (deviceExist.Settings.Action.Type)
{
case DeviceActionType.SendObs:
await SendObservationOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds);
break;
case DeviceActionType.SendAlarm:
await SendAlarmOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds);
break;
}
}
}
}
/// <summary>
/// Sends a patient observation alarm based on a device action and the type of click event that triggered it.
/// The alarm value is selected from the device action's configuration according to the click type
/// (SingleClick, DoubleClick, or Hold) and is dispatched for each point of care that has an associated patient.
/// The method exits early when the configuration observation cannot be found or when the action has no value defined for the current click type.
/// </summary>
/// <param name="settingsAction">The device action containing the configuration observation and the per-click-type alarm values to use.</param>
/// <param name="eventClickType">The click event that triggered the action, which determines which value from the device action is sent.</param>
/// <param name="deviceExistPointOfCareIds">The list of point of care identifiers whose patients should receive the alarm.</param>
private async Task SendAlarmOnAction(
DeviceAction settingsAction,
ClickType eventClickType,
List<ObjectId> deviceExistPointOfCareIds)
{
var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId);
if(configObs == null) return;
var obsData = new ObservationData
DeviceAction settingsAction,
ClickType eventClickType,
List<ObjectId> deviceExistPointOfCareIds)
{
Code = configObs.Code,
CodingSystem = configObs.CodingSystem,
Time = DateTime.UtcNow,
Text = configObs.Name,
};
var obs = new PatientObservationAlarm
{
InactivationState = new InactivationState{ Visual = AlarmEnum.AudioVideoState.Enabled, Audio = AlarmEnum.AudioVideoState.Enabled},
MessageTime = DateTime.UtcNow,
Persist = true,
Code = obsData.Code,
CodingSystem = obsData.CodingSystem,
Name = configObs.Name,
Time = DateTime.UtcNow
};
foreach (var pocId in deviceExistPointOfCareIds)
{
var data = await _pointOfCareService.GetInfo(pocId, null, true);
if (data != null && data.Patient?.Id != null)
var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId);
if(configObs == null) return;
var obsData = new ObservationData
{
obs.PatientId = data.Patient.Id;
obs.Patient = data.Patient;
switch (eventClickType)
Code = configObs.Code,
CodingSystem = configObs.CodingSystem,
Time = DateTime.UtcNow,
Text = configObs.Name,
};
var obs = new PatientObservationAlarm
{
InactivationState = new InactivationState{ Visual = AlarmEnum.AudioVideoState.Enabled, Audio = AlarmEnum.AudioVideoState.Enabled},
MessageTime = DateTime.UtcNow,
Persist = true,
Code = obsData.Code,
CodingSystem = obsData.CodingSystem,
Name = configObs.Name,
Time = DateTime.UtcNow
};
foreach (var pocId in deviceExistPointOfCareIds)
{
var data = await _pointOfCareService.GetInfo(pocId, null, true);
if (data != null && data.Patient?.Id != null)
{
case ClickType.SingleClick:
if(settingsAction.ValueOnSingleClick == null) return;
obs.Value = settingsAction.ValueOnSingleClick;
break;
case ClickType.DoubleClick:
if(settingsAction.ValueOnDoubleClick == null) return;
obs.Value = settingsAction.ValueOnDoubleClick;
break;
case ClickType.Hold:
if(settingsAction.ValueOnHoldClick == null) return;
obs.Value = settingsAction.ValueOnHoldClick;
break;
obs.PatientId = data.Patient.Id;
obs.Patient = data.Patient;
switch (eventClickType)
{
case ClickType.SingleClick:
if(settingsAction.ValueOnSingleClick == null) return;
obs.Value = settingsAction.ValueOnSingleClick;
break;
case ClickType.DoubleClick:
if(settingsAction.ValueOnDoubleClick == null) return;
obs.Value = settingsAction.ValueOnDoubleClick;
break;
case ClickType.Hold:
if(settingsAction.ValueOnHoldClick == null) return;
obs.Value = settingsAction.ValueOnHoldClick;
break;
}
// Process Obs on service
await _alarmService.ProcessAlarmObservations(new List<PatientObservationAlarm>{obs},new List<PatientObservation>(){new (){Name = configObs.Name, Value = obs.Value, Code = obsData.Code, CodingSystem = obsData.CodingSystem}}, data.Patient, DateTime.UtcNow, obsData);
}
// Process Obs on service
await _alarmService.ProcessAlarmObservations(new List<PatientObservationAlarm>{obs},new List<PatientObservation>(){new (){Name = configObs.Name, Value = obs.Value, Code = obsData.Code, CodingSystem = obsData.CodingSystem}}, data.Patient, DateTime.UtcNow, obsData);
}
}
}
/// <summary>
/// Sends a patient observation derived from the configuration linked to a device action, applying the value
/// associated with the specified click type (single, double, or hold) for each point of care patient found.
/// The method short-circuits when the configuration observation is missing or when no value is defined for
/// the given click type.
/// </summary>
/// <param name="settingsAction">The device action whose associated configuration observation and per-click values drive the observation payload.</param>
/// <param name="eventClickType">The type of click event that triggered the action; selects which value (single, double, or hold) is assigned to the observation.</param>
/// <param name="deviceExistPointOfCareIds">The list of point of care identifiers whose resolved patients will receive the generated observation.</param>
/// <returns>A task that represents the asynchronous send operation; no meaningful business result is returned.</returns>
private async Task SendObservationOnAction(
DeviceAction settingsAction,
ClickType eventClickType,
List<ObjectId> deviceExistPointOfCareIds)
{
var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId);
if(configObs == null) return;
var obsData = new ObservationData
DeviceAction settingsAction,
ClickType eventClickType,
List<ObjectId> deviceExistPointOfCareIds)
{
Code = configObs.Code,
CodingSystem = configObs.CodingSystem,
Time = DateTime.UtcNow,
Text = configObs.Name,
};
var obs = new PatientObservation
{
MessageTime = DateTime.UtcNow,
Persist = true,
Code = obsData.Code,
CodingSystem = obsData.CodingSystem,
Name = configObs.Name,
Time = DateTime.UtcNow
};
foreach (var pocId in deviceExistPointOfCareIds)
{
var data = await _pointOfCareService.GetInfo(pocId, null, true);
if (data != null && data.Patient?.Id != null)
var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId);
if(configObs == null) return;
var obsData = new ObservationData
{
obs.PatientId = data.Patient.Id;
obs.Patient = data.Patient;
switch (eventClickType)
Code = configObs.Code,
CodingSystem = configObs.CodingSystem,
Time = DateTime.UtcNow,
Text = configObs.Name,
};
var obs = new PatientObservation
{
MessageTime = DateTime.UtcNow,
Persist = true,
Code = obsData.Code,
CodingSystem = obsData.CodingSystem,
Name = configObs.Name,
Time = DateTime.UtcNow
};
foreach (var pocId in deviceExistPointOfCareIds)
{
var data = await _pointOfCareService.GetInfo(pocId, null, true);
if (data != null && data.Patient?.Id != null)
{
case ClickType.SingleClick:
if(settingsAction.ValueOnSingleClick == null) return;
obs.Value = settingsAction.ValueOnSingleClick;
break;
case ClickType.DoubleClick:
if(settingsAction.ValueOnDoubleClick == null) return;
obs.Value = settingsAction.ValueOnDoubleClick;
break;
case ClickType.Hold:
if(settingsAction.ValueOnHoldClick == null) return;
obs.Value = settingsAction.ValueOnHoldClick;
break;
obs.PatientId = data.Patient.Id;
obs.Patient = data.Patient;
switch (eventClickType)
{
case ClickType.SingleClick:
if(settingsAction.ValueOnSingleClick == null) return;
obs.Value = settingsAction.ValueOnSingleClick;
break;
case ClickType.DoubleClick:
if(settingsAction.ValueOnDoubleClick == null) return;
obs.Value = settingsAction.ValueOnDoubleClick;
break;
case ClickType.Hold:
if(settingsAction.ValueOnHoldClick == null) return;
obs.Value = settingsAction.ValueOnHoldClick;
break;
}
// Process Obs on service
_observationService.ProcessObservations(new List<PatientObservation>{obs}, data.Patient, DateTime.UtcNow, obsData);
}
// Process Obs on service
_observationService.ProcessObservations(new List<PatientObservation>{obs}, data.Patient, DateTime.UtcNow, obsData);
}
}
}
}
+320 -233
View File
@@ -15,6 +15,10 @@ using MongoDB.Driver;
namespace adas_core.Application.Services;
/// <summary>
/// Provides the implementation of the <see cref="IDiagnosisService"/> contract,
/// offering diagnosis-related operations as defined by the interface.
/// </summary>
public class DiagnosisService : IDiagnosisService
{
private readonly ILocalAuditService _auditService;
@@ -81,283 +85,366 @@ public class DiagnosisService : IDiagnosisService
// await _clientMessageService.SendAsync(subscriber.Id, OperationType.Diagnosis, diagnosis);
// }
/// <summary>
/// Archives the specified patient by delegating to the archival routine keyed by the patient's identifier.
/// </summary>
/// <param name="patient">The patient to be archived. Its <c>Id</c> is used to locate the record to archive.</param>
public async Task Archive(Patient patient)
{
await ArchiveByPatientId(patient.Id);
}
{
await ArchiveByPatientId(patient.Id);
}
/// <summary>
/// Archives all diagnoses associated with the specified patient by copying them to the diagnosis archive repository and then deleting the original records.
/// </summary>
/// <param name="id">The identifier of the patient whose diagnoses will be archived.</param>
public async Task ArchiveByPatientId(ObjectId id)
{
_logger.LogDebug("Archive Diagnoses by Patient Id {id}", id);
using (var cursor = await FindByPatientIdAsync(id))
{
while (await cursor.MoveNextAsync())
foreach (var current in cursor.Current)
await _diagnosisArchiveRepository.InsertOneAsync(current);
_logger.LogDebug("Archive Diagnoses by Patient Id {id}", id);
using (var cursor = await FindByPatientIdAsync(id))
{
while (await cursor.MoveNextAsync())
foreach (var current in cursor.Current)
await _diagnosisArchiveRepository.InsertOneAsync(current);
}
await DeleteByPatientId(id);
}
await DeleteByPatientId(id);
}
/// <summary>
/// Deletes all diagnoses associated with the specified patient identifier and records an audit log entry capturing the previous state of the records.
/// </summary>
/// <param name="id">The unique identifier of the patient whose diagnoses should be deleted.</param>
public async Task DeleteByPatientId(ObjectId id)
{
_logger.LogDebug("Delete Diagnoses by Patient Id {id}", id);
var oldPatient = await _diagnosisRepository.GetByPatient(id);
await _diagnosisRepository.DeleteByPatientId(id);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, null);
}
{
_logger.LogDebug("Delete Diagnoses by Patient Id {id}", id);
var oldPatient = await _diagnosisRepository.GetByPatient(id);
await _diagnosisRepository.DeleteByPatientId(id);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, null);
}
/// <summary>
/// Asynchronously retrieves the list of patient diagnoses associated with the specified patient identifier by delegating to the diagnosis repository.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose diagnoses are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientDiagnosis"/> records for the given patient.</returns>
public async Task<List<PatientDiagnosis>> GetByPatientId(ObjectId patientId)
{
var diagnosis = await _diagnosisRepository.GetByPatient(patientId);
return diagnosis;
}
{
var diagnosis = await _diagnosisRepository.GetByPatient(patientId);
return diagnosis;
}
/// <summary>
/// Asynchronously saves the specified API request by delegating to an overload that accepts a secondary parameter, which is passed as null.
/// </summary>
/// <param name="apiRequest">The API request to save.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
public Task SaveRequest(ApiRequest apiRequest)
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); }));
}
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); }));
}
/// <summary>
/// Asynchronously saves the specified API request by delegating to the underlying save operation with a null secondary parameter.
/// </summary>
/// <param name="apiRequest">The API request instance to persist.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
public Task SaveRequestAsync(ApiRequest apiRequest)
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); }));
}
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); }));
}
/// <summary>
/// Processes a diagnosis observation from an API request and persists it as a patient diagnosis. Maps SNOMED-coded observation values to diagnosis properties (description, label, code, state, category, start/end time), falling back to the current date when the observation time is missing, and skipping processing if the observations collection is null.
/// </summary>
/// <param name="apiRequest">The API request containing the observation codes, values, message time, and optional observation time used to build the diagnosis.</param>
/// <param name="patient">The patient associated with the diagnosis, whose identifier is assigned to the new <see cref="PatientDiagnosis"/>.</param>
/// <returns>A task that represents the asynchronous insertion of the resulting <see cref="PatientDiagnosis"/>.</returns>
public async Task ProcessDiagnosisObservation(ApiRequest apiRequest, Patient patient)
{
_logger.LogDebug("INSERT DiagnosisObservation from obsservation");
var time = apiRequest.ObservationData?.Time;
if (time == null)
_logger.LogWarning("Processing diagnosis without time. Set Datetime now {DateTimeNow}. ", DateTime.Now);
var obs = new PatientDiagnosis
{
CodingSystem = _diagnosisSystem,
Time = time ?? DateTime.Now,
PatientId = patient.Id,
MessageTime = apiRequest.MessageTime
};
if (apiRequest.Observations == null)
{
_logger.LogError("ApiRequest Observations null. ");
return;
}
for (var i = 0; i <= apiRequest.Observations.Count - 1; i++)
{
var value = apiRequest.Observations[i].Value;
var strValue = value.ToString() ?? "null";
switch (apiRequest.Observations[i].Code)
_logger.LogDebug("INSERT DiagnosisObservation from obsservation");
var time = apiRequest.ObservationData?.Time;
if (time == null)
_logger.LogWarning("Processing diagnosis without time. Set Datetime now {DateTimeNow}. ", DateTime.Now);
var obs = new PatientDiagnosis
{
case "272099008":
obs.Description = strValue;
break;
case "1000000013":
obs.Label = strValue;
break;
case "1000000014":
obs.Code = strValue;
break;
case "394731006":
obs.State = strValue;
break;
case "272125009":
obs.Category = strValue;
break;
case "398201009":
if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var startTime))
obs.StartTime = startTime;
break;
case "397898000":
if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var endTime))
obs.EndTime = endTime;
break;
}
}
await InsertDiagnosis(obs);
}
public async Task SaveRequest(ApiRequest apiRequest, Patient? patient)
{
if (patient == null)
{
_logger.LogDebug("message:ApiRequest Diagnosis");
if (string.IsNullOrEmpty(apiRequest.PatientNumber) &&
string.IsNullOrEmpty(apiRequest.Location?.UnitName))
CodingSystem = _diagnosisSystem,
Time = time ?? DateTime.Now,
PatientId = patient.Id,
MessageTime = apiRequest.MessageTime
};
if (apiRequest.Observations == null)
{
_logger.LogDebug("person and PointOfCare are nulls");
throw new ApiRequestException("person and PointOfCare are nulls");
_logger.LogError("ApiRequest Observations null. ");
return;
}
_logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}",
apiRequest.PatientNumber, apiRequest.Location);
_logger.LogDebug("RequestType: {apiRequesttype}", apiRequest.Type);
patient = await _patientService.Value.FindPatientByApiRequest(apiRequest);
}
if (patient == null)
{
// NO PATIENTS OR LOCATIONS WERE FOUND
_logger.LogWarning(
"Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring",
apiRequest.PatientNumber, apiRequest.Location);
return;
}
var unitConfig = await _unitService.FindById(patient.UnitId);
if (!Hl7Utils.ManageAutoAdt(unitConfig, null, _logger, "ORU Diagnosis")) return;
switch (apiRequest.Type)
{
//* ORU_R01 - Unsolicited transmission of an observation message
//* ORU_R40 - Unsolicited transmission of an alert observation message
case "ORU_R01":
case "ORU_R40":
// OBSERVATIONS
if (apiRequest.Observation != null && apiRequest.Observations?.FirstOrDefault() == null)
apiRequest.Observations = [apiRequest.Observation];
if (apiRequest.Observations != null)
for (var i = 0; i <= apiRequest.Observations.Count - 1; i++)
{
var value = apiRequest.Observations[i].Value;
var strValue = value.ToString() ?? "null";
switch (apiRequest.Observations[i].Code)
{
var obrcode = apiRequest.ObservationData?.Code;
if (apiRequest.ObservationData?.Value != null)
apiRequest.Observations.Add(new PatientObservation
{ Value = apiRequest.ObservationData.Value });
if (obrcode != null && _diagnosisCode.Contains(obrcode))
_ = ProcessDiagnosisObservation(apiRequest, patient);
case "272099008":
obs.Description = strValue;
break;
case "1000000013":
obs.Label = strValue;
break;
case "1000000014":
obs.Code = strValue;
break;
case "394731006":
obs.State = strValue;
break;
case "272125009":
obs.Category = strValue;
break;
case "398201009":
if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var startTime))
obs.StartTime = startTime;
break;
case "397898000":
if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var endTime))
obs.EndTime = endTime;
break;
}
break;
default:
_logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Diagnosis",
apiRequest.Type);
throw new ApiRequestException("ApiRequest type " + apiRequest.Type +
" is not valid for Diagnosis");
}
await InsertDiagnosis(obs);
}
}
/// <summary>
/// Processes an incoming API request for diagnosis-related observations, resolving the patient from the request when not supplied. Validates that at least one of the patient number or location unit name is provided, handles ORU_R01 and ORU_R40 observation messages, and triggers diagnosis observation processing when the observation code is recognized.
/// </summary>
/// <param name="apiRequest">The API request containing the type, patient identifiers, location, and observation data to be processed.</param>
/// <param name="patient">An optional pre-resolved patient; when null, the patient is resolved via the patient service using the request data.</param>
/// <exception cref="ApiRequestException">Thrown when both the patient number and the location unit name are missing from the request, or when the request type is not valid for diagnosis processing.</exception>
public async Task SaveRequest(ApiRequest apiRequest, Patient? patient)
{
if (patient == null)
{
_logger.LogDebug("message:ApiRequest Diagnosis");
if (string.IsNullOrEmpty(apiRequest.PatientNumber) &&
string.IsNullOrEmpty(apiRequest.Location?.UnitName))
{
_logger.LogDebug("person and PointOfCare are nulls");
throw new ApiRequestException("person and PointOfCare are nulls");
}
_logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}",
apiRequest.PatientNumber, apiRequest.Location);
_logger.LogDebug("RequestType: {apiRequesttype}", apiRequest.Type);
patient = await _patientService.Value.FindPatientByApiRequest(apiRequest);
}
if (patient == null)
{
// NO PATIENTS OR LOCATIONS WERE FOUND
_logger.LogWarning(
"Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring",
apiRequest.PatientNumber, apiRequest.Location);
return;
}
var unitConfig = await _unitService.FindById(patient.UnitId);
if (!Hl7Utils.ManageAutoAdt(unitConfig, null, _logger, "ORU Diagnosis")) return;
switch (apiRequest.Type)
{
//* ORU_R01 - Unsolicited transmission of an observation message
//* ORU_R40 - Unsolicited transmission of an alert observation message
case "ORU_R01":
case "ORU_R40":
// OBSERVATIONS
if (apiRequest.Observation != null && apiRequest.Observations?.FirstOrDefault() == null)
apiRequest.Observations = [apiRequest.Observation];
if (apiRequest.Observations != null)
{
var obrcode = apiRequest.ObservationData?.Code;
if (apiRequest.ObservationData?.Value != null)
apiRequest.Observations.Add(new PatientObservation
{ Value = apiRequest.ObservationData.Value });
if (obrcode != null && _diagnosisCode.Contains(obrcode))
_ = ProcessDiagnosisObservation(apiRequest, patient);
}
break;
default:
_logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Diagnosis",
apiRequest.Type);
throw new ApiRequestException("ApiRequest type " + apiRequest.Type +
" is not valid for Diagnosis");
}
}
/// <summary>
/// Processes a list of patient diagnoses by associating each entry with the specified patient and message time, then inserting them as diagnosis observations.
/// </summary>
/// <param name="diagnosis">The list of patient diagnoses to be processed and inserted.</param>
/// <param name="patient">The patient whose identifier is assigned to each diagnosis entry.</param>
/// <param name="messageTime">The timestamp assigned to each diagnosis entry during processing.</param>
public async Task ProcessDiagnosis(List<PatientDiagnosis> diagnosis, Patient patient, DateTime messageTime)
{
_logger.LogDebug("INSERT DiagnosisObservation from diagnosis");
foreach (var d in diagnosis)
{
d.PatientId = patient.Id;
d.Time = messageTime;
await InsertDiagnosis(d);
_logger.LogDebug("INSERT DiagnosisObservation from diagnosis");
foreach (var d in diagnosis)
{
d.PatientId = patient.Id;
d.Time = messageTime;
await InsertDiagnosis(d);
}
}
}
/// <summary>
/// Updates many diagnosis records, replacing the <paramref name="oldId"/> with the new <paramref name="id"/> for the specified <paramref name="nameId"/> field, by delegating to the diagnosis repository.
/// </summary>
/// <param name="nameId">The name of the identifier field used to locate the records to update.</param>
/// <param name="id">The new ObjectId to assign to the matching records.</param>
/// <param name="oldId">The existing ObjectId to be replaced in the matching records.</param>
public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
{
await _diagnosisRepository.UpdateManyObjectId(nameId, id, oldId);
}
{
await _diagnosisRepository.UpdateManyObjectId(nameId, id, oldId);
}
/// <summary>
/// Retrieves all diagnoses associated with the specified patient identifier from the diagnosis repository.
/// </summary>
/// <param name="id">The unique identifier of the patient whose diagnoses are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientDiagnosis"/> records for the given patient.</returns>
public async Task<List<PatientDiagnosis>> GetByPatient(ObjectId id)
{
return await _diagnosisRepository.GetByPatient(id);
}
{
return await _diagnosisRepository.GetByPatient(id);
}
/// <summary>
/// Inserts a patient diagnosis into the repository after mapping it to the underlying data model, and broadcasts the stored record.
/// If the diagnosis cannot be mapped (returns null), the insert and broadcast operations are skipped.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to insert.</param>
public async Task Insert(PatientDiagnosis diagnosis)
{
_logger.LogDebug("Insert {diagnosis}", diagnosis);
var diag = await MapDiagnosis(diagnosis);
if (diag != null)
{
await _diagnosisRepository.InsertOneAsync(diag);
await SendBroadcast(diag);
}
}
private async Task<PatientDiagnosis?> MapDiagnosis(PatientDiagnosis diagnosis)
{
var diagnosis2 = await _calculatedObservations.Value.Map(diagnosis);
if (diagnosis2 == null) _logger.LogDebug("Mapping {diagnosis}: Ignored", diagnosis2);
return diagnosis2;
}
private async Task SendBroadcast(PatientDiagnosis diagnosis)
{
var patient = await _patientService.Value.FindById(diagnosis.PatientId);
if (patient == null) return;
var displaySubscribers = _subscribersService.GetSubscribers().Where(s =>
!s.Locations.IsNullOrEmpty() && s.Locations.Any(c =>
c.UnitName == patient.Location.UnitName &&
c.Bed == patient.Location.Bed &&
c.Room == patient.Location.Room
)).ToList();
displaySubscribers.ForEach(Action);
return;
void Action(WsSubscriber subscriber)
{
_clientMessageService.SendAsync(subscriber.Id, OperationType.Diagnosis, diagnosis);
}
}
public Task<IAsyncCursor<PatientDiagnosis>> FindByPatientIdAsync(ObjectId patientId)
{
return _diagnosisRepository.FindByPatientIdAsync(patientId);
}
public async Task InsertDiagnosis(PatientDiagnosis diagnosis)
{
try
{
_logger.LogDebug("Insert {diagnosis}", diagnosis);
var diag = await MapDiagnosis(diagnosis);
if (diag == null)
if (diag != null)
{
_logger.LogError("Mepped diagnosis is null. Diagnosis: {diagnosis}", diagnosis);
await _diagnosisRepository.InsertOneAsync(diag);
await SendBroadcast(diag);
}
else
{
var dgdb = await _diagnosisRepository.FindByPatientIdAndCode(diag.PatientId, diag.Code,
diag.CodingSystem);
}
if (dgdb != null)
/// <summary>
/// Maps a <see cref="PatientDiagnosis"/> through the calculated observations mapper to produce a transformed diagnosis instance.
/// When the mapper yields a <see langword="null"/> result, indicating the diagnosis is not applicable or cannot be mapped, a debug message is logged and the result is returned as-is.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to be mapped.</param>
/// <returns>The mapped <see cref="PatientDiagnosis"/> produced by the calculated observations mapper, or <see langword="null"/> if the diagnosis was ignored.</returns>
private async Task<PatientDiagnosis?> MapDiagnosis(PatientDiagnosis diagnosis)
{
var diagnosis2 = await _calculatedObservations.Value.Map(diagnosis);
if (diagnosis2 == null) _logger.LogDebug("Mapping {diagnosis}: Ignored", diagnosis2);
return diagnosis2;
}
/// <summary>
/// Sends a patient diagnosis broadcast to all subscribers whose registered location matches the patient's location (unit, room, and bed). Returns early without broadcasting if the patient cannot be found.
/// </summary>
/// <param name="diagnosis">The patient diagnosis payload to broadcast to the matching subscribers.</param>
private async Task SendBroadcast(PatientDiagnosis diagnosis)
{
var patient = await _patientService.Value.FindById(diagnosis.PatientId);
if (patient == null) return;
var displaySubscribers = _subscribersService.GetSubscribers().Where(s =>
!s.Locations.IsNullOrEmpty() && s.Locations.Any(c =>
c.UnitName == patient.Location.UnitName &&
c.Bed == patient.Location.Bed &&
c.Room == patient.Location.Room
)).ToList();
displaySubscribers.ForEach(Action);
return;
void Action(WsSubscriber subscriber)
{
_clientMessageService.SendAsync(subscriber.Id, OperationType.Diagnosis, diagnosis);
}
}
/// <summary>
/// Asynchronously retrieves all patient diagnosis records associated with the specified patient identifier by delegating to the diagnosis repository.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose diagnosis records are to be retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing an asynchronous cursor over the matching <see cref="PatientDiagnosis"/> records.</returns>
public Task<IAsyncCursor<PatientDiagnosis>> FindByPatientIdAsync(ObjectId patientId)
{
return _diagnosisRepository.FindByPatientIdAsync(patientId);
}
/// <summary>
/// Inserts or updates a patient diagnosis, creating an audit log entry. If a diagnosis
/// already exists for the same patient, code, and coding system, it is updated while
/// preserving its identifier and original timestamp; otherwise a new record is inserted.
/// A broadcast is dispatched asynchronously after a successful insert or update.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to persist.</param>
public async Task InsertDiagnosis(PatientDiagnosis diagnosis)
{
try
{
_logger.LogDebug("Insert {diagnosis}", diagnosis);
var diag = await MapDiagnosis(diagnosis);
if (diag == null)
{
var auxDgdb = dgdb;
diag.Id = dgdb.Id;
diag.Time = dgdb.Time;
diag.UpdateDate = diagnosis.Time;
await _diagnosisRepository.UpdateOneAsync(diag.Id, diag);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxDgdb, dgdb);
_logger.LogError("Mepped diagnosis is null. Diagnosis: {diagnosis}", diagnosis);
}
else
{
await _diagnosisRepository.InsertOneAsync(diagnosis);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, diagnosis);
var dgdb = await _diagnosisRepository.FindByPatientIdAndCode(diag.PatientId, diag.Code,
diag.CodingSystem);
if (dgdb != null)
{
var auxDgdb = dgdb;
diag.Id = dgdb.Id;
diag.Time = dgdb.Time;
diag.UpdateDate = diagnosis.Time;
await _diagnosisRepository.UpdateOneAsync(diag.Id, diag);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxDgdb, dgdb);
}
else
{
await _diagnosisRepository.InsertOneAsync(diagnosis);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, diagnosis);
}
_ = SendBroadcast(diagnosis);
}
_ = SendBroadcast(diagnosis);
}
catch (Exception ex)
{
_logger.LogError("Error inserting diagnosis. Diagnosis: {diagnosis}. Exception:{ex}", diagnosis, ex);
}
}
catch (Exception ex)
{
_logger.LogError("Error inserting diagnosis. Diagnosis: {diagnosis}. Exception:{ex}", diagnosis, ex);
}
}
}
@@ -14,6 +14,14 @@ using MongoDB.Bson;
namespace adas_core.Application.Services;
/// <summary>
/// Provides the concrete implementation of the <see cref="IDischargeService"/> contract,
/// encapsulating the business logic required to process discharge operations.
/// </summary>
/// <remarks>
/// This service acts as the default in-memory or infrastructure-backed implementation
/// of the discharge operations defined by <see cref="IDischargeService"/>.
/// </remarks>
public class DischargeService : IDischargeService
{
private readonly ILocalAuditService _auditService;
@@ -52,6 +60,12 @@ public class DischargeService : IDischargeService
}
/// <summary>
/// Deletes a discharge record asynchronously. The method is intended to validate that the patient
/// can be discharged (requiring both medical and administrative discharge values and an allowed
/// discharge status), and otherwise falls back to deleting the discharge by its identifier.
/// </summary>
/// <param name="discharge">The discharge entity to be deleted.</param>
public async Task DeleteDischargeAsync(Discharge discharge)
{
//var patient = await _patientServiceLazy.Value.FindById(discharge.PatientId);
@@ -65,6 +79,10 @@ public class DischargeService : IDischargeService
await DeleteDischargeByIdAsync(discharge.Id);
}
/// <summary>
/// Deletes a discharge record by its identifier. If the discharge is not found, an error is logged and the operation is skipped; otherwise the record is removed, an audit log entry is created, and a delete broadcast is sent.
/// </summary>
/// <param name="dischargeId">The unique identifier of the discharge to delete.</param>
public async Task DeleteDischargeByIdAsync(ObjectId dischargeId)
{
try
@@ -95,11 +113,25 @@ public class DischargeService : IDischargeService
}
}
/// <summary>
/// Asynchronously counts the number of discharge records associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The identifier of the unit whose discharge records will be counted.</param>
/// <returns>A task representing the asynchronous operation, containing the total number of discharges for the given unit.</returns>
public async Task<long> CountDischargesByUnitId(ObjectId unitId)
{
return await _dischargeRepository.CountByUnitId(unitId);
}
/// <summary>
/// Retrieves a discharge by its identifier, enriching the result with patient location details
/// when an associated point of care is available.
/// </summary>
/// <param name="dischargeId">The unique identifier of the discharge to retrieve.</param>
/// <returns>The matching <see cref="Discharge"/>, populated with <see cref="PatientLocation"/>
/// information if a point of care is linked; otherwise the discharge as stored.</returns>
/// <exception cref="NotFoundException">Thrown when no discharge is found for the specified
/// <paramref name="dischargeId"/>.</exception>
public async Task<Discharge?> GetDischargeByIdAsync(ObjectId dischargeId)
{
var result = await _dischargeRepository.FindById(dischargeId) ??
@@ -107,17 +139,26 @@ public class DischargeService : IDischargeService
if (result.PointOfCareId == null)
return result;
var poc = await _pointOfCareService.GetInfo(result.PointOfCareId.Value, null,false);
var poc = await _pointOfCareService.GetInfo(result.PointOfCareId.Value, null, false);
result.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room);
return result;
}
/// <summary>
/// Asynchronously retrieves all discharge records from the repository.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing an enumerable collection of <see cref="Discharge"/> records.</returns>
public async Task<IEnumerable<Discharge>> GetDischargesAsync()
{
return await _dischargeRepository.FindAll();
}
/// <summary>
/// Retrieves the discharge record associated with the specified patient identifier, enriching it with patient location details when a point of care is linked.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose discharge record should be retrieved.</param>
/// <returns>A <see cref="Discharge"/> object populated with patient location information when a point of care is associated, or <c>null</c> if no discharge is found or an error occurs.</returns>
public async Task<Discharge?> GetDischargeByPatientId(ObjectId patientId)
{
try
@@ -127,7 +168,7 @@ public class DischargeService : IDischargeService
_logger.LogError("Discharge not found by patient Id {id}", patientId);
if (discharge is { PointOfCareId: not null })
{
var poc = await _pointOfCareService.GetInfo(discharge.PointOfCareId.Value, null,false);
var poc = await _pointOfCareService.GetInfo(discharge.PointOfCareId.Value, null, false);
discharge.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room);
}
@@ -140,6 +181,13 @@ public class DischargeService : IDischargeService
}
}
/// <summary>
/// Inserts a new discharge record into the repository, verifies its persistence, logs the operation, and broadcasts a notification.
/// Throws a <see cref="ConflictException"/> if the discharge cannot be retrieved after insertion, indicating a creation failure.
/// </summary>
/// <param name="discharge">The discharge entity to be inserted.</param>
/// <returns>The persisted <see cref="Discharge"/> entity retrieved from the repository after insertion.</returns>
/// <exception cref="ConflictException">Thrown when the inserted discharge cannot be found by its identifier, indicating that the creation failed.</exception>
public async Task<Discharge?> InsertDischarge(Discharge discharge)
{
await _dischargeRepository.InsertOneAsync(discharge);
@@ -152,6 +200,12 @@ public class DischargeService : IDischargeService
return dischargeAux;
}
/// <summary>
/// Asynchronously retrieves the <see cref="Discharge"/> associated with the specified patient location.
/// Returns <c>null</c> if an exception occurs while accessing the underlying repository.
/// </summary>
/// <param name="location">The patient location used to look up the associated discharge record.</param>
/// <returns>A <see cref="Discharge"/> if one is found for the given location; otherwise, <c>null</c> when an error occurs.</returns>
public async Task<Discharge?> GetDischargeByLocation(PatientLocation location)
{
try
@@ -165,6 +219,13 @@ public class DischargeService : IDischargeService
}
}
/// <summary>
/// Retrieves a discharge record by the specified point of care identifier and enriches it with patient location details.
/// If the discharge is not found, an information message is logged; when a related point of care is available, its unit, bed, and room are mapped to the discharge's <see cref="PatientLocation"/>.
/// On failure, the error is logged and <c>null</c> is returned.
/// </summary>
/// <param name="poc">The point of care identifier used to look up the discharge record.</param>
/// <returns>A <see cref="Discharge"/> with the patient location populated when available, or <c>null</c> if the discharge is not found or an error occurs.</returns>
public async Task<Discharge?> GetDischargeByPointOfCareId(ObjectId poc)
{
try
@@ -174,7 +235,7 @@ public class DischargeService : IDischargeService
_logger.LogInformation("Discharge not found by PointOfCareId {id}", poc);
if (discharge is { PointOfCareId: not null })
{
var pocInfo = await _pointOfCareService.GetInfo(discharge.PointOfCareId.Value, null,false);
var pocInfo = await _pointOfCareService.GetInfo(discharge.PointOfCareId.Value, null, false);
discharge.PatientLocation = new PatientLocation(pocInfo?.UnitName, pocInfo?.Bed, pocInfo?.Room);
}
@@ -187,6 +248,13 @@ public class DischargeService : IDischargeService
}
}
/// <summary>
/// Retrieves a discharge record by its point of care (location) identifier and applies locale-specific data using the associated unit.
/// Returns the discharge as-is if no associated unit is found, or <c>null</c> if no discharge exists for the given location.
/// </summary>
/// <param name="location">The ObjectId identifying the point of care (location) whose discharge record should be retrieved.</param>
/// <param name="dataLocale">The locale used to localize the discharge data when the associated unit is found.</param>
/// <returns>A <see cref="Task{Discharge}"/> containing the localized discharge, the unmodified discharge when its unit cannot be found, or <c>null</c> when no discharge exists for the specified location.</returns>
public async Task<Discharge?> GetDischargeByPointOfCareIdWithLocale(ObjectId location, LocaleEnum dataLocale)
{
var discharge = await GetDischargeByPointOfCareId(location);
@@ -197,6 +265,11 @@ public class DischargeService : IDischargeService
return dischargeWithLocale;
}
/// <summary>
/// Updates an existing discharge record, auditing the change and broadcasting the update. Throws a conflict exception if the discharge cannot be found by its identifier.
/// </summary>
/// <param name="discharge">The discharge entity to be updated.</param>
/// <exception cref="ConflictException">Thrown when no discharge is found with the specified identifier, preventing the update from proceeding.</exception>
public async Task UpdateDischargeAsync(Discharge discharge)
{
var oldDischarge = await GetDischargeByIdAsync(discharge.Id) ??
@@ -228,39 +301,39 @@ public class DischargeService : IDischargeService
switch (apiRequest.Type)
{
case "NewDischarge":
{
//TODO: ver qué tipos llegan
if (!"Altable".Equals(apiRequest.Discharge.Patient.DischargeStatus?.OptionType))
{
_logger.LogError("Error discharging. Patient not altable: {patient}", patient);
return;
//TODO: ver qué tipos llegan
if (!"Altable".Equals(apiRequest.Discharge.Patient.DischargeStatus?.OptionType))
{
_logger.LogError("Error discharging. Patient not altable: {patient}", patient);
return;
}
await _dischargeRepository.InsertOneAsync(apiRequest.Discharge);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null,
apiRequest.Discharge);
SendDischargeBroadcast(apiRequest.Discharge, OperationType.NewDischarge);
break;
}
await _dischargeRepository.InsertOneAsync(apiRequest.Discharge);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null,
apiRequest.Discharge);
SendDischargeBroadcast(apiRequest.Discharge, OperationType.NewDischarge);
break;
}
case "UpdateDischarge":
{
await GetDischargeByIdAsync(apiRequest.Discharge.Id);
await UpdateDischargeAsync(apiRequest.Discharge);
break;
}
case "DeleteDischarge":
{
if (StatusEnum.Discharge.NoAltable.ToString().Equals(patient.DischargeStatus?.OptionType))
{
_logger.LogError("Error deleting discharge. Patient altable: {patient}", patient);
return;
await GetDischargeByIdAsync(apiRequest.Discharge.Id);
await UpdateDischargeAsync(apiRequest.Discharge);
break;
}
case "DeleteDischarge":
{
if (StatusEnum.Discharge.NoAltable.ToString().Equals(patient.DischargeStatus?.OptionType))
{
_logger.LogError("Error deleting discharge. Patient altable: {patient}", patient);
return;
}
await DeleteDischargeAsync(apiRequest.Discharge);
break;
}
await DeleteDischargeAsync(apiRequest.Discharge);
break;
}
}
}
catch (Exception ex)
@@ -270,12 +343,22 @@ public class DischargeService : IDischargeService
}
}
/// <summary>
/// Asynchronously persists the specified API request by executing the save operation on a background thread.
/// </summary>
/// <param name="apiRequest">The API request to save.</param>
/// <returns>A task that completes when the request has been saved.</returns>
public Task SaveRequestAsync(ApiRequest apiRequest)
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
}
// Revisar
/// <summary>
/// Sends a discharge broadcast to all subscribers associated with the discharge's point of care, grouped by their locale. Validates that the point of care identifier is present; logs and returns early if it is null. Exceptions during the broadcast are caught and logged without rethrowing.
/// </summary>
/// <param name="discharge">The discharge whose point of care is used to locate matching subscribers and whose data is sent in the broadcast.</param>
/// <param name="operation">The operation type associated with the outgoing message sent to each subscriber.</param>
public async void SendDischargeBroadcast(Discharge discharge, OperationType operation)
{
try
@@ -311,8 +394,14 @@ public class DischargeService : IDischargeService
}
}
/// <summary>
/// Updates the master list option for the specified units and type, then records an audit log entry and broadcasts the change for each affected discharge.
/// </summary>
/// <param name="opt">The master list update options to apply to the discharges.</param>
/// <param name="unitList">The collection of units whose associated discharges will be updated.</param>
/// <param name="typeName">The name of the master list type used to target the update.</param>
public async Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable<Unit> unitList,
string typeName)
string typeName)
{
var unitIds = unitList.Select(x => x.Id).ToList();
await _dischargeRepository.GetDischargesByUnitIds(unitIds);
@@ -326,6 +415,12 @@ public class DischargeService : IDischargeService
}
}
/// <summary>
/// Deletes a patient master list option for the specified units and type, then processes the resulting updated discharge records by creating audit log entries and broadcasting update notifications (only when the updated discharge record is found).
/// </summary>
/// <param name="opt">The option list entry to be removed from the patient master list.</param>
/// <param name="unitList">The collection of units whose identifiers are used to scope the deletion.</param>
/// <param name="typeName">The name of the master list type/category to which the option belongs.</param>
public async Task DeletePatientMasterListItem(OptionList opt, IEnumerable<Unit> unitList, string typeName)
{
var unitIds = unitList.Select(x => x.Id).ToList();
@@ -339,11 +434,24 @@ public class DischargeService : IDischargeService
}
}
/// <summary>
/// Asynchronously deletes all discharge records associated with the specified unit identifier by delegating the operation to the discharge repository.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose discharge records should be removed.</param>
public async Task DeleteDischargesByUnitId(ObjectId unitId)
{
await _dischargeRepository.DeleteByUnitId(unitId);
}
/// <summary>
/// Returns the given <paramref name="discharge"/> with its configurable option names translated according to the specified <paramref name="locale"/>.
/// When <paramref name="unit"/> is null or the locale is <see cref="LocaleEnum.Default"/>, the discharge is returned unchanged.
/// Otherwise, the configured service and destination options are looked up in the locale-specific master lists and their <c>Name</c> values are updated; fields without a matching list, missing properties, null values, or without a translation are left untouched.
/// </summary>
/// <param name="unit">Source of the master list identifiers used to resolve locale-specific options; when null, no translation is performed.</param>
/// <param name="discharge">Discharge instance whose option names may be translated in place.</param>
/// <param name="locale">Target locale used to load the appropriate master list; when set to <see cref="LocaleEnum.Default"/>, the discharge is returned without changes.</param>
/// <returns>The same <paramref name="discharge"/> instance, with translated option names when a matching locale-specific entry is found.</returns>
private async Task<Discharge> GetDischargeWithLocale(Unit? unit, Discharge discharge, LocaleEnum locale)
{
if (unit == null)
@@ -31,6 +31,11 @@ public class DisplayConfigService(
IDisplayChartConfigRepository displayChartRepository)
: IDisplayConfigService
{
/// <summary>
/// Retrieves all display configurations from the repository and enriches each one with its minimal display section.
/// Configurations for which the minimal display section cannot be resolved are excluded from the result.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of enriched <see cref="DisplayConfig"/> items, omitting any entries that could not be enriched.</returns>
public async Task<List<DisplayConfig>> GetAll()
{
var result = await displayConfigRepository.GetAll();
@@ -44,6 +49,12 @@ public class DisplayConfigService(
return resultToReturn;
}
/// <summary>
/// Retrieves a paginated list of display configurations in a compact form, including a flag indicating whether each configuration is currently in use.
/// Applies server-side pagination using the provided filter and maps each result to a <see cref="DisplayConfigMinimalResponse"/> enriched with its usage status.
/// </summary>
/// <param name="filter">The pagination filter containing the page number, page size, and any filtering criteria used to retrieve and paginate the display configurations.</param>
/// <returns>A task that represents the asynchronous operation, containing a <see cref="PaginationResponse{T}"/> with the compact display configurations, current page metadata, and total document count.</returns>
public async Task<PaginationResponse<DisplayConfigMinimalResponse>> GetAllCompactPaginated(PaginationFilter filter)
{
var result = displayConfigRepository.GetAllPaginated(filter);
@@ -66,6 +77,11 @@ public class DisplayConfigService(
count);
}
/// <summary>
/// Retrieves display configurations matching the specified display type and enriches each one with its minimal display section. Configurations for which the enrichment returns null are excluded from the result list.
/// </summary>
/// <param name="type">The display type used to filter the configurations.</param>
/// <returns>A list of enriched display configurations; entries whose display section could not be resolved are omitted.</returns>
public async Task<List<DisplayConfig>> GetByType(DisplayConfigEnums.DisplayType type)
{
var result = await displayConfigRepository.GetByType(type);
@@ -79,14 +95,28 @@ public class DisplayConfigService(
return resultToReturn;
}
/// <summary>
/// Retrieves a <see cref="DisplayConfig"/> by its identifier and enriches it with minimal display section data.
/// Throws a <see cref="NotFoundException"/> if no configuration is found for the given id.
/// </summary>
/// <param name="id">The unique identifier of the display configuration to retrieve.</param>
/// <returns>The display configuration with the minimal display section applied.</returns>
/// <exception cref="NotFoundException">Thrown when no display configuration exists for the specified <paramref name="id"/>.</exception>
public async Task<DisplayConfig> GetById(ObjectId id)
{
return await AddDisplaySectionMinimal(await displayConfigRepository.GetById(id)) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
/// <summary>
/// Retrieves a <see cref="DisplayConfig"/> by its identifier, falling back to the default configuration for the given unit and display type when the identifier is not provided or not found. When both a configuration by id and a default configuration are found, the two are merged and the merged result is returned.
/// </summary>
/// <param name="configId">The optional configuration identifier. When null, only the default configuration is returned.</param>
/// <param name="unitId">The unit identifier used to look up the default configuration.</param>
/// <param name="displayType">The display type used to look up the default configuration.</param>
/// <returns>The current configuration, the default configuration, the merged configuration, or null when neither is available.</returns>
public async Task<DisplayConfig?> GetById(ObjectId? configId, ObjectId unitId,
DisplayConfigEnums.DisplayType displayType)
DisplayConfigEnums.DisplayType displayType)
{
if (configId.HasValue)
{
@@ -118,12 +148,22 @@ public class DisplayConfigService(
return await GetDefaultByUnitIdAndType(unitId, displayType);
}
/// <summary>
/// Inserts a new display configuration into the repository while recording an audit log entry for the operation.
/// </summary>
/// <param name="config">The display configuration to insert.</param>
/// <returns>The inserted display configuration, or null if no entity was returned by the repository.</returns>
public async Task<DisplayConfig?> InsertOne(DisplayConfig config)
{
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, config);
return await displayConfigRepository.InsertOneAsyncAndReturn(config);
}
/// <summary>
/// Inserts a minimal display configuration, creating a <see cref="DisplayNurse"/> instance when the type is <see cref="DisplayConfigEnums.DisplayType.DisplayNurse"/> and a base <see cref="DisplayConfig"/> otherwise, while recording an audit log for the creation.
/// </summary>
/// <param name="config">The DTO containing the hospital and display type used to build the new configuration entity.</param>
/// <returns>The inserted <see cref="DisplayConfig"/> entity, or <c>null</c> if the repository did not return a result.</returns>
public async Task<DisplayConfig?> InsertOneMinimal(CreateDisplayConfigDto config)
{
DisplayConfig newDisplayConfig;
@@ -147,6 +187,10 @@ public class DisplayConfigService(
return result;
}
/// <summary>
/// Inserts test records for the DisplayNurse and SmartDisplay display configuration types into the repository and returns the inserted DisplayNurse record.
/// </summary>
/// <returns>The inserted <see cref="DisplayConfig"/> instance of type DisplayNurse.</returns>
public async Task<DisplayConfig> InsertOneTest()
{
var d = new DisplayNurse
@@ -162,6 +206,13 @@ public class DisplayConfigService(
return d;
}
/// <summary>
/// Updates a display configuration, dispatching to a type-specific update path for <see cref="DisplayConfigEnums.DisplayType.SmartDisplay"/> or <see cref="DisplayConfigEnums.DisplayType.DisplayNurse"/>, broadcasting the change and writing an audit log when the update succeeds.
/// </summary>
/// <param name="displayConfigId">The identifier of the display configuration to update.</param>
/// <param name="newDisplayConfig">The new configuration payload, deserialized internally into the appropriate DTO based on its <c>Type</c>.</param>
/// <returns>The updated <see cref="DisplayConfig"/> when the corresponding update succeeds; otherwise the method throws.</returns>
/// <exception cref="NotFoundException">Thrown when the configuration's <c>Type</c> is not handled, or when the underlying update returns no result.</exception>
public async Task<DisplayConfig?> UpdateConfig(ObjectId displayConfigId, object newDisplayConfig)
{
var baseType = JsonConvert.DeserializeObject<DisplayConfigDto>(newDisplayConfig.ToString()!);
@@ -209,11 +260,24 @@ public class DisplayConfigService(
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
/// <summary>
/// Updates the list of fields associated with a display configuration identified by the specified object ID.
/// </summary>
/// <param name="objectIdConfigDisplay">The unique identifier of the display configuration whose field list is being updated.</param>
/// <param name="fields">The collection of fields to be associated with the display configuration.</param>
/// <returns>A task that represents the asynchronous operation, containing a boolean value indicating whether the update was successful.</returns>
public Task<bool> UpdateFieldList(ObjectId objectIdConfigDisplay, List<Field> fields)
{
return displayConfigRepository.UpdateFieldList(objectIdConfigDisplay, fields);
}
/// <summary>
/// Updates the color configuration of a display, initializing an empty color configuration when none exists, and records an audit log of the change.
/// </summary>
/// <param name="objectIdConfigDisplay">The identifier of the display configuration to update.</param>
/// <param name="colorConfig">The new color configuration to apply to the display.</param>
/// <returns>True if the color configuration was updated successfully; otherwise, false.</returns>
/// <exception cref="NotFoundException">Thrown when the display configuration identified by <paramref name="objectIdConfigDisplay"/> cannot be found before or after the update.</exception>
public async Task<bool> UpdateConfigColor(ObjectId objectIdConfigDisplay, ColorConfig colorConfig)
{
var oldDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ??
@@ -232,6 +296,14 @@ public class DisplayConfigService(
return result;
}
/// <summary>
/// Updates the header configuration of a display configuration record and creates an audit log entry
/// capturing the previous and updated values.
/// </summary>
/// <param name="objectIdConfigDisplay">The unique identifier of the display configuration to update.</param>
/// <param name="headerConfig">The new header configuration to apply to the display configuration.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the update was successful; otherwise, <c>false</c>.</returns>
/// <exception cref="NotFoundException">Thrown when the display configuration is not found before the update, or when the updated display configuration cannot be retrieved afterwards.</exception>
public async Task<bool> UpdateHeaderConfig(ObjectId objectIdConfigDisplay, HeaderConfig headerConfig)
{
var oldDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ??
@@ -244,6 +316,13 @@ public class DisplayConfigService(
return result;
}
/// <summary>
/// Updates the home banner configuration for the specified display configuration and records an audit log comparing the old and new states.
/// </summary>
/// <param name="objectIdConfigDisplay">The identifier of the display configuration to update.</param>
/// <param name="bannerItems">The list of banner items to set for the home banner.</param>
/// <returns>A task that resolves to <c>true</c> if the update succeeds; otherwise, <c>false</c>.</returns>
/// <exception cref="NotFoundException">Thrown when the display configuration cannot be found before or after the update.</exception>
public async Task<bool> UpdateSetHomeBanner(ObjectId objectIdConfigDisplay, List<BannerItem> bannerItems)
{
var oldDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ??
@@ -256,6 +335,12 @@ public class DisplayConfigService(
return result;
}
/// <summary>
/// Updates the base display configuration and records an audit log comparing the previous and updated configurations.
/// </summary>
/// <param name="baseConfig">The display configuration containing the updated values, identified by its <see cref="DisplayConfig.Id"/>.</param>
/// <returns>A task that represents the asynchronous operation. The result indicates whether the update was successful.</returns>
/// <exception cref="NotFoundException">Thrown when the display configuration with the specified identifier does not exist before or after the update.</exception>
public async Task<bool> UpdateBaseConfig(DisplayConfig baseConfig)
{
var oldDisplayConfig = await displayConfigRepository.GetById(baseConfig.Id) ??
@@ -268,6 +353,13 @@ public class DisplayConfigService(
return result;
}
/// <summary>
/// Updates the hospital name of an existing display configuration and records an audit log entry comparing the previous and updated values.
/// </summary>
/// <param name="objectIdConfigDisplay">The identifier of the display configuration to update.</param>
/// <param name="name">The new hospital name to apply to the display configuration.</param>
/// <returns>A task that resolves to <c>true</c> if the update was successful; otherwise, <c>false</c>.</returns>
/// <exception cref="NotFoundException">Thrown when the display configuration identified by <paramref name="objectIdConfigDisplay"/> cannot be found before or after the update.</exception>
public async Task<bool> UpdateDisplayConfigHospitalName(ObjectId objectIdConfigDisplay, string name)
{
var oldDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ??
@@ -280,6 +372,12 @@ public class DisplayConfigService(
return result;
}
/// <summary>
/// Deletes a display configuration by its identifier. When displays are still associated with the configuration, they are reassigned to a default configuration for the same type before deletion, and an audit log entry is recorded on success.
/// </summary>
/// <param name="objectIdConfigDisplay">The identifier of the display configuration to delete.</param>
/// <returns>A task that resolves to <c>true</c> if the configuration was successfully deleted; otherwise, <c>false</c>.</returns>
/// <exception cref="ConflictException">Thrown when no default configuration is found for the related display type, or when reassigning a display to the default configuration fails.</exception>
public async Task<bool> DeleteDisplayConfig(ObjectId objectIdConfigDisplay)
{
var displays = await displayService.Value.GetByConfigId(objectIdConfigDisplay);
@@ -303,18 +401,34 @@ public class DisplayConfigService(
return false;
}
/// <summary>
/// Retrieves the list of display configuration locations associated with the specified configuration display identifier.
/// </summary>
/// <param name="objectIdConfigDisplay">The identifier of the configuration display whose locations are being retrieved.</param>
/// <returns>A task that returns a list of <see cref="DisplayConfigLocationDto"/> items for the given configuration display.</returns>
public async Task<List<DisplayConfigLocationDto>> GetDisplayConfigLocations(ObjectId objectIdConfigDisplay)
{
return await displayService.Value.GetDisplayConfigLocations(objectIdConfigDisplay);
}
/// <summary>
/// Asynchronously retrieves a compact list of all display configurations from the repository.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="DisplayConfigMinimalResponse"/> objects representing the compact display configuration data.</returns>
public async Task<List<DisplayConfigMinimalResponse>> GetAllCompact()
{
return await displayConfigRepository.GetAllCompact();
}
/// <summary>
/// Creates a new display configuration by cloning an existing template identified by its object ID, applying the specified hospital and display type, and inserting it into the data store. Returns <c>null</c> when the object ID cannot be parsed, the template cannot be retrieved, the retrieved template does not match the requested display type, or the display type is not one of the handled cases.
/// </summary>
/// <param name="objectId">The object ID of the existing template used as the base for the new configuration.</param>
/// <param name="configType">The display type of the configuration to create; determines which template subtype is expected and produced.</param>
/// <param name="configHospital">The hospital to associate with the newly created configuration.</param>
/// <returns>The inserted <see cref="DisplayConfig"/> when the template is found and matches the requested type; otherwise, <c>null</c>.</returns>
public async Task<DisplayConfig?> InsertOneWithTemplate(string objectId,
DisplayConfigEnums.DisplayType configType, string? configHospital)
DisplayConfigEnums.DisplayType configType, string? configHospital)
{
if (ObjectId.TryParse(objectId, out var objectIdConfigDisplay))
{
@@ -325,7 +439,7 @@ public class DisplayConfigService(
if (template is StandarDisplay standardTemplate)
{
var standarConfigg = new StandarDisplay
{ Hospital = configHospital, Type = DisplayConfigEnums.DisplayType.StandarDisplay };
{ Hospital = configHospital, Type = DisplayConfigEnums.DisplayType.StandarDisplay };
standarConfigg.MergeConfig(standardTemplate);
await InsertOne(standarConfigg);
return standarConfigg;
@@ -337,7 +451,7 @@ public class DisplayConfigService(
if (template is DisplayNurse nurseTemplate)
{
var nurseConfig = new DisplayNurse
{ Hospital = configHospital, Type = DisplayConfigEnums.DisplayType.DisplayNurse };
{ Hospital = configHospital, Type = DisplayConfigEnums.DisplayType.DisplayNurse };
nurseConfig.MergeConfig(nurseTemplate);
await InsertOne(nurseConfig);
return nurseConfig;
@@ -349,7 +463,7 @@ public class DisplayConfigService(
if (template is SmartDisplay smartTemplate)
{
var smartConfig = new SmartDisplay
{ Hospital = configHospital, Type = DisplayConfigEnums.DisplayType.DisplayNurse };
{ Hospital = configHospital, Type = DisplayConfigEnums.DisplayType.DisplayNurse };
smartConfig.MergeConfig(smartTemplate);
await InsertOne(smartConfig);
return smartConfig;
@@ -365,10 +479,15 @@ public class DisplayConfigService(
return null;
}
/// <summary>
/// Updates an existing card configuration. When the update is successful, propagates the new configuration to related rotating display configs by refreshing their nurse data, and broadcasts the card display config update to all associated displays.
/// </summary>
/// <param name="baseConfig">The card configuration to update.</param>
/// <returns>True if the update was applied (repository reported changes); otherwise, false.</returns>
public async Task<bool> UpdateCardConfig(CardConfig baseConfig)
{
var result = await displayCardConfigRepository.UpdateOne(baseConfig);
if (result.Changes > 0)
{
var displayConfigs = await displayConfigRepository.GetAllByCardConfigIdAndRotating(baseConfig.Id);
@@ -377,7 +496,7 @@ public class DisplayConfigService(
await displayConfigRepository.UpdateDisplayNurse(displayConfig,
new DisplayNurseDto() { CardConfig = result.Data }, masterListServiceFactory.StringNurseObs());
}
var displays = await displayConfigRepository.GetAllByCardConfigId(baseConfig.Id);
foreach (var display in displays)
SendDisplayConfigBroadcast(display, OperationType.UpdateCardDisplayConfig, result.Data);
@@ -388,6 +507,12 @@ public class DisplayConfigService(
return false;
}
/// <summary>
/// Updates an existing card detail configuration and propagates the change by broadcasting an update event to all related displays.
/// Returns <c>true</c> if the update modified at least one record, otherwise <c>false</c>.
/// </summary>
/// <param name="baseConfig">The card detail configuration to update, identified by its <c>Id</c>.</param>
/// <returns>A task that resolves to <c>true</c> when the update affected one or more records; <c>false</c> when no changes were made.</returns>
public async Task<bool> UpdateDetailConfig(CardDetailsConfig baseConfig)
{
var result = await displayDetailConfigRepository.UpdateOne(baseConfig);
@@ -398,6 +523,11 @@ public class DisplayConfigService(
return false;
}
/// <summary>
/// Updates an existing chart configuration in the repository and returns whether the operation modified any records.
/// </summary>
/// <param name="baseConfig">The chart configuration to be updated.</param>
/// <returns>A task that resolves to <c>true</c> if the update changed at least one record; otherwise, <c>false</c>.</returns>
public async Task<bool> UpdateChartConfig(ChartConfig baseConfig)
{
var result = await displayChartRepository.UpdateOne(baseConfig);
@@ -405,6 +535,12 @@ public class DisplayConfigService(
return false;
}
/// <summary>
/// Deletes the chart configuration identified by the given display object ID.
/// If the repository deletion succeeds, the deleted chart configuration is updated and the method returns <c>true</c>; otherwise, it returns <c>false</c>.
/// </summary>
/// <param name="objectIdConfigDisplay">The object ID of the chart configuration display to delete.</param>
/// <returns><c>true</c> if the chart configuration was successfully deleted; <c>false</c> if no matching configuration was found.</returns>
public async Task<bool> DeleteChartConfig(ObjectId objectIdConfigDisplay)
{
var res = await displayChartRepository.DeleteOne(objectIdConfigDisplay);
@@ -417,11 +553,21 @@ public class DisplayConfigService(
return false;
}
/// <summary>
/// Retrieves a chart configuration by its unique identifier from the display chart repository.
/// </summary>
/// <param name="objectIdConfigChart">The unique identifier of the chart configuration to retrieve.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="ChartConfig"/> if found; otherwise, <c>null</c>.</returns>
public async Task<ChartConfig?> GetChartConfig(ObjectId objectIdConfigChart)
{
return await displayChartRepository.GetById(objectIdConfigChart);
}
/// <summary>
/// Inserts a new card detail configuration and optionally links it to an existing display configuration, broadcasting the change when the link is successfully established.
/// </summary>
/// <param name="updateDisplayConfigNameDto">The DTO containing the detail configuration to insert and, optionally, the ID of the display configuration to associate it with.</param>
/// <returns>The newly inserted <see cref="CardDetailsConfig"/>, or <c>null</c> when no detail configuration is provided in the DTO.</returns>
public async Task<CardDetailsConfig?> InsertCardDetailConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto)
{
if (updateDisplayConfigNameDto.DetailConfig == null) return null;
@@ -438,6 +584,11 @@ public class DisplayConfigService(
return result;
}
/// <summary>
/// Inserts a new chart configuration and, when a display configuration identifier is provided, links the inserted chart to that display configuration and broadcasts the update.
/// </summary>
/// <param name="updateDisplayConfigNameDto">The data transfer object containing the chart configuration to insert and, optionally, the target display configuration identifier.</param>
/// <returns>The inserted <see cref="ChartConfig"/>, or <c>null</c> if the supplied chart configuration is <c>null</c>.</returns>
public async Task<ChartConfig?> InsertChartConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto)
{
if (updateDisplayConfigNameDto.ChartConfig == null) return null;
@@ -453,9 +604,14 @@ public class DisplayConfigService(
return result;
}
/// <summary>
/// Inserts a new card configuration and optionally links it to an existing display configuration by updating the card config id and broadcasting the change.
/// </summary>
/// <param name="updateDisplayConfigNameDto">The DTO containing the card configuration to insert and, optionally, the display configuration id to associate it with.</param>
/// <returns>The inserted <see cref="CardConfig"/>, or <c>null</c> when the provided card configuration is null.</returns>
public async Task<CardConfig?> InsertCardConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto)
{
if(updateDisplayConfigNameDto.CardConfig == null) return null;
if (updateDisplayConfigNameDto.CardConfig == null) return null;
var result = await displayCardConfigRepository.InsertOneAsyncAndReturn(updateDisplayConfigNameDto.CardConfig);
if (updateDisplayConfigNameDto.DisplayConfigId != null && result != null)
{
@@ -468,29 +624,57 @@ public class DisplayConfigService(
return result;
}
/// <summary>
/// Asynchronously retrieves all card configurations from the display card config repository.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of all <see cref="CardConfig"/> entities.</returns>
public async Task<List<CardConfig>> GetCardConfigAll()
{
return await displayCardConfigRepository.GetAll();
}
/// <summary>
/// Retrieves a card configuration by its unique identifier from the display card configuration repository.
/// </summary>
/// <param name="id">The unique identifier of the card configuration to retrieve.</param>
/// <returns>The matching <see cref="CardConfig"/> if found; otherwise, <c>null</c>.</returns>
public async Task<CardConfig?> GetCardConfigById(ObjectId id)
{
return await displayCardConfigRepository.GetById(id);
}
/// <summary>
/// Retrieves the default <see cref="DisplayConfig"/> for the specified display type by delegating to the underlying repository.
/// Returns <see langword="null"/> when no default configuration exists for the given type.
/// </summary>
/// <param name="type">The display type used to look up the default configuration.</param>
/// <returns>A <see cref="DisplayConfig"/> representing the default configuration for the specified type, or <see langword="null"/> if no default is found.</returns>
public async Task<DisplayConfig?> GetDefaultConfig(DisplayConfigEnums.DisplayType type)
{
return await displayConfigRepository.GetDefault(type);
}
/// <summary>
/// Retrieves the default <see cref="DisplayConfig"/> for the specified unit and display type by delegating to the repository.
/// Returns <c>null</c> when no matching default configuration exists, as the not-found exception is currently commented out.
/// </summary>
/// <param name="unitId">The identifier of the unit whose default display configuration is being requested.</param>
/// <param name="displayType">The display type used to filter the default configuration lookup.</param>
/// <returns>A <see cref="DisplayConfig"/> instance if a default is found; otherwise, <c>null</c>.</returns>
private async Task<DisplayConfig?> GetDefaultByUnitIdAndType(ObjectId unitId,
DisplayConfigEnums.DisplayType displayType)
DisplayConfigEnums.DisplayType displayType)
{
return
await displayConfigRepository.GetDefaultByUnitIdAndType(unitId,
displayType); //?? throw new NotFoundException(ErrorMessage.NotFound_ResourceMissing);
}
/// <summary>
/// Enriches the given <see cref="DisplayConfig"/> with minimal display section information when it is a SmartDisplay and has associated display section IDs, and records an audit log for the change.
/// </summary>
/// <param name="displayConfig">The display configuration to augment with minimal display section data, or <see langword="null"/>.</param>
/// <returns>The updated <see cref="DisplayConfig"/>, or <see langword="null"/> if the input was <see langword="null"/>.</returns>
/// <exception cref="NotFoundException">Thrown when the display configuration cannot be found in the repository by its identifier.</exception>
private async Task<DisplayConfig?> AddDisplaySectionMinimal(DisplayConfig? displayConfig)
{
if (displayConfig == null) return null;
@@ -520,31 +704,61 @@ public class DisplayConfigService(
return displayConfig;
}
/// <summary>
/// Updates the chart configuration to mark the specified entry as deleted by delegating to the display configuration repository.
/// </summary>
/// <param name="deletedId">The identifier of the chart configuration entry to mark as deleted.</param>
private async Task UpdateDeletedChartConfig(ObjectId deletedId)
{
await displayConfigRepository.UpdateDeletedChartConfig(deletedId);
}
/// <summary>
/// Adds a chart ID to the specified display configuration by delegating the operation to the display configuration repository.
/// </summary>
/// <param name="displayConfigId">The identifier of the display configuration to which the chart ID will be associated. May be null.</param>
/// <param name="resultId">The identifier of the chart result to add to the display configuration.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the chart ID was successfully added.</returns>
private async Task<bool> AddChartId(ObjectId? displayConfigId, ObjectId resultId)
{
var result = await displayConfigRepository.AddChartId(displayConfigId, resultId);
return result;
}
/// <summary>
/// Updates the card configuration identifier by delegating the operation to the display configuration repository.
/// </summary>
/// <param name="displayConfigId">The identifier of the display configuration to update, or null to skip.</param>
/// <param name="resultId">The identifier of the result to associate with the card configuration, or null to skip.</param>
/// <returns>A task that resolves to true if the update was successful; otherwise, false.</returns>
private async Task<bool> UpdateCardConfigId(ObjectId? displayConfigId, ObjectId? resultId)
{
var result = await displayConfigRepository.UpdateCardConfigId(displayConfigId, resultId);
return result;
}
/// <summary>
/// Updates the detail configuration identifier for the specified result by delegating the operation to the display configuration repository.
/// </summary>
/// <param name="displayConfigId">The display configuration identifier to associate with the result, or <c>null</c> if not specified.</param>
/// <param name="resultId">The result identifier whose detail configuration should be updated, or <c>null</c> if not specified.</param>
/// <returns>A task that resolves to <c>true</c> if the update was successful; otherwise, <c>false</c>.</returns>
private async Task<bool> UpdateDetailConfigId(ObjectId? displayConfigId, ObjectId? resultId)
{
var result = await displayConfigRepository.UpdateDetailConfigId(displayConfigId, resultId);
return result;
}
/// <summary>
/// Broadcasts a display configuration change to all subscribers whose display is linked to the given configuration.
/// Looks up displays by the configuration id, filters subscribers matching those display ids, and sends the operation and new configuration to each subscriber asynchronously.
/// Logs and swallows any errors that occur while sending the broadcast.
/// </summary>
/// <param name="displayConfigId">The identifier of the display configuration whose change should be broadcast to related subscribers.</param>
/// <param name="operationType">The type of operation performed on the configuration (e.g., create, update, delete) to convey to subscribers.</param>
/// <param name="newDisplayConfig">The new display configuration payload to send to subscribers, or null if not applicable for the operation.</param>
private async void SendDisplayConfigBroadcast(ObjectId displayConfigId, OperationType operationType,
object? newDisplayConfig)
object? newDisplayConfig)
{
try
{
@@ -564,6 +778,14 @@ public class DisplayConfigService(
}
}
/// <summary>
/// Broadcasts a SmartDisplay configuration update to the relevant subscribers and notifies all clients of the latest display configuration.
/// When both the new and old configurations are provided, the update is dispatched to the matching subscribers; otherwise, an error is logged.
/// Regardless of the outcome, a global update message is sent to all clients so they can refresh the display configuration.
/// </summary>
/// <param name="displayConfigId">The identifier of the display configuration used to look up the associated displays and to broadcast the global update.</param>
/// <param name="newDisplayConfig">The new SmartDisplay configuration to propagate to subscribers, or null when not available.</param>
/// <param name="oldDisplayConfig">The previous SmartDisplay configuration used to build the update payload, or null when not available.</param>
private async void SendSmartDisplayConfigBroadcast(ObjectId displayConfigId, SmartDisplay? newDisplayConfig,
SmartDisplay? oldDisplayConfig)
{
@@ -592,8 +814,14 @@ public class DisplayConfigService(
}
}
/// <summary>
/// Sends updated display configuration values to all WebSocket subscribers for each property that has changed between the old and new configurations. If the old configuration is null, no update messages are sent.
/// </summary>
/// <param name="subscribers">The list of WebSocket subscribers that will receive the display configuration update messages.</param>
/// <param name="oldDisplayDisplayConfig">The previous smart display configuration, or null if there is no prior configuration to compare against.</param>
/// <param name="newDisplayDisplayConfig">The new smart display configuration whose values will be sent to subscribers.</param>
private void SendSmartDisplayConfigUpdate(List<WsSubscriber> subscribers,
SmartDisplay? oldDisplayDisplayConfig, SmartDisplay? newDisplayDisplayConfig)
SmartDisplay? oldDisplayDisplayConfig, SmartDisplay? newDisplayDisplayConfig)
{
// Obtener las propiedades que han cambiado
var differentProperties = oldDisplayDisplayConfig?.GetDifferentProperties(newDisplayDisplayConfig);
@@ -601,8 +829,8 @@ public class DisplayConfigService(
// Enviar un mensaje a los clientes por cada propiedad que haya cambiado
if (differentProperties != null)
foreach (var property in differentProperties)
foreach (var sub in subscribers)
_ = clientMessageService.SendAsync(sub.Id, OperationType.UpdateDisplayConfig,
newDisplayDisplayConfig?.GetType().GetProperty(property)?.GetValue(newDisplayDisplayConfig));
foreach (var sub in subscribers)
_ = clientMessageService.SendAsync(sub.Id, OperationType.UpdateDisplayConfig,
newDisplayDisplayConfig?.GetType().GetProperty(property)?.GetValue(newDisplayDisplayConfig));
}
}
+241 -34
View File
@@ -36,12 +36,20 @@ public class DisplayService(
IOptions<CacheSettings> cacheSettings)
: IDisplayService
{
private readonly CacheSettings? _cacheSettings = cacheSettings.Value ;
private readonly CacheSettings? _cacheSettings = cacheSettings.Value;
#region Methods
#region Create
/// <summary>
/// Inserts a new <see cref="Display"/> using the default configuration for its type.
/// If no default configuration exists for the display type, a <see cref="NotFoundException"/> is thrown.
/// An audit log entry is created after the display is persisted.
/// </summary>
/// <param name="display">The display to insert. Its <c>DisplayConfigId</c> is assigned from the resolved default configuration.</param>
/// <returns>The inserted <see cref="Display"/> with its <c>DisplayConfigId</c> populated.</returns>
/// <exception cref="NotFoundException">Thrown when no default configuration is found for the specified display type.</exception>
public async Task<Display> InsertOne(Display display)
{
var defaultConfig = await displayConfigService.GetDefaultConfig(display.Type) ??
@@ -52,6 +60,10 @@ public class DisplayService(
return display;
}
/// <summary>
/// Inserts a test Display record into the repository and records a corresponding audit log entry using the current HTTP context user.
/// </summary>
/// <returns>The newly created <see cref="Display"/> entity.</returns>
public async Task<Display> InsertOneTest()
{
var d = new Display
@@ -69,6 +81,10 @@ public class DisplayService(
#region Read
/// <summary>
/// Retrieves all displays from the repository and maps them to a compact representation.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="DisplayMinimalDto"/> with the mapped display data.</returns>
public async Task<List<DisplayMinimalDto>> GetAllCompact()
{
var result = await displayRepository.GetAll();
@@ -77,11 +93,22 @@ public class DisplayService(
return listToReturn;
}
/// <summary>
/// Retrieves all display items, optionally filtered by the specified user name.
/// </summary>
/// <param name="userName">The user name used to filter the display items, or <see langword="null"/> to retrieve all items.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="Display"/> items.</returns>
/// <exception cref="NotImplementedException">The method has not been implemented yet.</exception>
public Task<List<Display>> GetAll(string? userName)
{
throw new NotImplementedException();
}
/// <summary>
/// Retrieves a paginated list of <see cref="Display"/> items along with the total document count, applying page number and page size from the provided filter.
/// </summary>
/// <param name="filter">The pagination filter containing the page number and page size used to determine the slice of results to return.</param>
/// <returns>A <see cref="Task{PaginationResponse{Display}}"/> containing the requested page of displays, the current page number, the page size, and the total number of documents.</returns>
public async Task<PaginationResponse<Display>> GetPaginatedDisplays(PaginationFilter filter)
{
var result = displayRepository.GetPaginatedDisplays(filter);
@@ -97,6 +124,13 @@ public class DisplayService(
return new PaginationResponse<Display>(dataList, filter.PageNumber, filter.PageSize, count);
}
/// <summary>
/// Retrieves all displays accessible to the specified user, together with their associated permissions, by resolving the user's authorizations (both unit-scoped and display-scoped).
/// Returns an empty list when the username is null, when the user cannot be found, or when no authorizations are available; throws an exception if permissions for a unit-scoped display cannot be resolved.
/// </summary>
/// <param name="userName">The username whose displays should be retrieved; when null, the method returns an empty list.</param>
/// <returns>A task that yields a list of <see cref="DisplayWithPermissionsDto"/> containing the displays the user can access along with their permissions.</returns>
/// <exception cref="ForbbidenException">Thrown when permissions for a unit-scoped display cannot be obtained for the user.</exception>
public async Task<List<DisplayWithPermissionsDto>> GetAllByUser(string? userName)
{
var start = DateTime.Now;
@@ -119,7 +153,7 @@ public class DisplayService(
var dis = await displayRepository.GetByUnitId(dId);
foreach (var display in dis)
{
var toAdd = await GetInfo(display.Id, userName, user.Authorization,null, false, false, false, false);
var toAdd = await GetInfo(display.Id, userName, user.Authorization, null, false, false, false, false);
if (toAdd != null)
{
@@ -160,11 +194,24 @@ public class DisplayService(
return listToReturn;
}
/// <summary>
/// Determines whether a display with the specified identifier exists in the provided collection of display permissions.
/// The check safely skips entries whose <c>Display</c> reference is null before comparing the display identifier.
/// </summary>
/// <param name="displayId">The identifier of the display to look up, compared as a string.</param>
/// <param name="perms">The collection of display-with-permissions entries to search through.</param>
/// <returns><c>true</c> if a non-null display with a matching identifier is found; otherwise, <c>false</c>.</returns>
private static bool FindDisplayInPerms(string displayId, List<DisplayWithPermissionsDto> perms)
{
return perms.Any(p => p.Display != null && p.Display.Id.ToString() == displayId);
}
/// <summary>
/// Retrieves all displays associated with the specified display type by resolving the matching display configurations and loading their corresponding displays.
/// Each returned display is enriched with its parent configuration, and configurations without associated displays are skipped.
/// </summary>
/// <param name="type">The display type used to filter the display configurations.</param>
/// <returns>A list of displays matching the specified type, each with its related configuration assigned; an empty list is returned when no displays are found.</returns>
public async Task<List<Display>> GetByType(DisplayConfigEnums.DisplayType type)
{
var configs = await displayConfigService.GetByType(type);
@@ -179,32 +226,65 @@ public class DisplayService(
return listToReturn;
}
/// <summary>
/// Retrieves a list of displays associated with the specified point of care.
/// </summary>
/// <param name="pointOfCare">The point of care used to filter the displays.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the list of displays matching the specified point of care.</returns>
public async Task<List<Display>> GetByPointOfCare(PointOfCare pointOfCare)
{
return await displayRepository.GetByPointOfCare(pointOfCare);
}
/// <summary>
/// Retrieves the list of displays associated with the specified configuration identifier by delegating to the display repository.
/// </summary>
/// <param name="configId">The configuration identifier used to look up the associated displays.</param>
/// <returns>A task that returns the list of <see cref="Display"/> objects matching the given configuration identifier.</returns>
public Task<List<Display>> GetByConfigId(ObjectId configId)
{
return displayRepository.GetByConfigId(configId);
}
/// <summary>
/// Retrieves a list of displays associated with the specified card configuration identifier by delegating to the underlying repository.
/// </summary>
/// <param name="configId">The unique identifier of the card configuration used to look up the associated displays.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Display"/> objects matching the provided card configuration identifier.</returns>
public Task<List<Display>> GetByCardConfigId(ObjectId configId)
{
return displayRepository.GetByCardConfigId(configId);
}
/// <summary>
/// Retrieves a <see cref="Display"/> by its name. Throws a <see cref="NotFoundException"/> if no matching display is found.
/// </summary>
/// <param name="name">The name of the display to look up.</param>
/// <returns>The <see cref="Display"/> that matches the specified name.</returns>
/// <exception cref="NotFoundException">Thrown when no display is found for the given name.</exception>
public async Task<Display?> GetByName(string name)
{
return await displayRepository.GetByName(name) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
/// <summary>
/// Retrieves a <see cref="Display"/> by its unique identifier from the repository.
/// </summary>
/// <param name="id">The unique identifier of the display to retrieve.</param>
/// <returns>The matching <see cref="Display"/> if found; otherwise, <c>null</c>.</returns>
public async Task<Display?> GetById(ObjectId id)
{
return await displayRepository.GetById(id);
}
/// <summary>
/// Retrieves a display by its identifier, enriches it with localized point-of-care information, its display configuration, and the permissions available to the current user.
/// </summary>
/// <param name="id">The unique identifier of the display to retrieve.</param>
/// <param name="localeEnum">The locale used to localize the related point-of-care information.</param>
/// <returns>A <see cref="DisplayWithPermissionsDto"/> containing the display and its associated permissions.</returns>
/// <exception cref="NotFoundException">Thrown when the current user cannot be identified from the JWT or when no display is found for the specified <paramref name="id"/>.</exception>
public async Task<DisplayWithPermissionsDto> GetByIdWithPermissions(ObjectId id, LocaleEnum localeEnum)
{
var username = JwtHelper.GetUsernameFromPrincipal(httpContextAccessor.HttpContext?.User!) ??
@@ -241,18 +321,38 @@ public class DisplayService(
};
}
/// <summary>
/// Asynchronously counts the number of displays associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose displays should be counted.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the total number of displays for the given unit.</returns>
public async Task<long> CountDisplaysByUnitId(ObjectId unitId)
{
return await displayRepository.CountByUnitId(unitId);
}
public async Task<Display?> GetInfo(ObjectId id,
string? userName,
/// <summary>
/// Retrieves display information by id, with optional enrichment of point-of-care, patient data, and section list based on the provided flags.
/// Uses cached data when display configuration is requested; otherwise fetches the base display and caches the result.
/// Fetches user authorizations from the user repository when not supplied, and logs an error if the display list cannot be populated due to a missing configuration.
/// </summary>
/// <param name="id">Identifier of the display to retrieve.</param>
/// <param name="userName">Optional user name used to look up authorizations when none are provided.</param>
/// <param name="authorizations">Optional pre-resolved authorizations used to filter the display section list.</param>
/// <param name="locale">Optional locale applied when loading point-of-care data.</param>
/// <param name="fillPointOfCare">If true, populates the point-of-care entries for the display.</param>
/// <param name="fillPatientData">If true, includes patient data when retrieving point-of-care information.</param>
/// <param name="fillDisplayList">If true, populates the display section list filtered by the resolved authorizations.</param>
/// <param name="fillDisplayConfig">If true, retrieves the full display including its configuration (cached); otherwise retrieves the base display.</param>
/// <param name="ct">Cancellation token to cancel the operation.</param>
/// <returns>The requested <see cref="Display"/>, or <c>null</c> if no display is found for the given id.</returns>
public async Task<Display?> GetInfo(ObjectId id,
string? userName,
List<Authorization>? authorizations,
LocaleEnum? locale,
LocaleEnum? locale,
bool fillPointOfCare = true,
bool fillPatientData = false,
bool fillDisplayList = true,
bool fillPatientData = false,
bool fillDisplayList = true,
bool fillDisplayConfig = true,
CancellationToken ct = default)
{
@@ -261,7 +361,7 @@ public class DisplayService(
Display? display;
if (fillDisplayConfig)
{
// Clave: display con configuración
var (key, ttl) = CacheKeys.DisplayWithConfigKeyWithTtl(_cacheSettings, id);
@@ -270,7 +370,7 @@ public class DisplayService(
async () => await BuildDisplayWithConfig(id, ct),
ttl,
ct);
}
else
{
@@ -285,7 +385,7 @@ public class DisplayService(
}
if (display == null) return null;
// PointOfCare (cacheado en su propio servicio)
if (fillPointOfCare)
foreach (var poc in display.PointOfCareIdList)
@@ -315,7 +415,13 @@ public class DisplayService(
return display;
}
/// <summary>
/// Builds a <see cref="Display"/> enriched with its display configuration. Returns <c>null</c> when the display is not found, and for <see cref="SmartDisplay"/> instances that have a <c>CardRotatingLayout</c>, resolves and assigns each card's configuration, falling back to an empty <see cref="CardConfig"/> when a card configuration cannot be retrieved.
/// </summary>
/// <param name="id">The identifier of the display to load.</param>
/// <param name="ct">The cancellation token used to cancel the asynchronous operation.</param>
/// <returns>The <see cref="Display"/> with its configuration populated, or <c>null</c> if no display exists for the given <paramref name="id"/>.</returns>
private async Task<Display?> BuildDisplayWithConfig(ObjectId id, CancellationToken ct)
{
var display = await displayRepository.GetById(id);
@@ -328,10 +434,10 @@ public class DisplayService(
await displayConfigService.GetById(display.DisplayConfigId, display.UnitId, type);
if (type != DisplayConfigEnums.DisplayType.SmartDisplay ||
display.DisplayConfig is not SmartDisplay { CardRotatingLayout: not null } smart)
display.DisplayConfig is not SmartDisplay { CardRotatingLayout: not null } smart)
return display;
if(smart.CardRotatingLayout== null)
if (smart.CardRotatingLayout == null)
return display;
foreach (var card in smart.CardRotatingLayout)
@@ -339,15 +445,24 @@ public class DisplayService(
?? new CardConfig();
return display;
}
/// <summary>
/// Retrieves the display sections accessible to a specific user, filtered by display type, based on the user's authorities (either provided or fetched from the authority service).
/// Supports both direct display references and unit-based references, marks the currently selected display, and returns an empty list if the user is not found or no matching sections exist.
/// </summary>
/// <param name="type">The display type used to filter the returned sections.</param>
/// <param name="currentDisplay">The identifier of the currently selected display, which will be flagged as selected in the result; may be null.</param>
/// <param name="userName">The username used to look up the user and their authorities; if null, an empty list is returned.</param>
/// <param name="authorizations">Optional pre-fetched list of user authorities; when null, authorities are retrieved from the authority service.</param>
/// <returns>A task that resolves to a list of <see cref="MinimalDisplaySection"/> items accessible to the user and matching the specified display type.</returns>
public async Task<List<MinimalDisplaySection>> GetDisplaySectionByUser(
DisplayConfigEnums.DisplayType type,
ObjectId? currentDisplay,
string? userName,
List<Authorization>? authorizations)
DisplayConfigEnums.DisplayType type,
ObjectId? currentDisplay,
string? userName,
List<Authorization>? authorizations)
{
try
{
@@ -413,6 +528,10 @@ public class DisplayService(
}
}
/// <summary>
/// Asynchronously retrieves all display configurations of type DisplayNurse and SmartDisplay, mapping them into minimal display sections and grouping them within a display list DTO.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a MinimalDisplayListDto with the populated DisplayNurse and SmartDisplay collections.</returns>
public async Task<MinimalDisplayListDto> GetAllDisplaySection()
{
var minimalDisplayListDto = new MinimalDisplayListDto();
@@ -441,11 +560,23 @@ public class DisplayService(
return minimalDisplayListDto;
}
/// <summary>
/// Retrieves the list of displays associated with the specified unit identifier by delegating to the display repository.
/// </summary>
/// <param name="unitId">The identifier of the unit whose displays should be returned.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of <see cref="Display"/> objects for the given unit.</returns>
public async Task<List<Display>> GetByUnitId(ObjectId unitId)
{
return await displayRepository.GetByUnitId(unitId);
}
/// <summary>
/// Retrieves all available points of care (POC) and their associated unit information for the specified display identifiers. Invalid display ID strings are silently skipped, virtual points of care can optionally be excluded, and a <see cref="NotFoundException"/> is thrown when a resolved unit cannot be found; any unexpected error is logged and an empty result is returned.
/// </summary>
/// <param name="displayIds">A list of display identifier strings used to resolve the related units and their available points of care.</param>
/// <param name="excludeVirtual">When set to <c>true</c>, virtual points of care are excluded from the result; otherwise, they are included.</param>
/// <returns>A <see cref="PocAndUnitDto"/> containing the available points of care and their associated unit details.</returns>
/// <exception cref="NotFoundException">Thrown when a unit associated with one of the resolved display identifiers cannot be found.</exception>
public async Task<PocAndUnitDto> GetAllAvailablePoc(List<string> displayIds, bool excludeVirtual = false)
{
try
@@ -492,6 +623,11 @@ public class DisplayService(
}
}
/// <summary>
/// Retrieves all points of care associated with the specified display. Returns an empty list when the display is not found, when no associated points of care exist, or when an error occurs during retrieval.
/// </summary>
/// <param name="id">The ObjectId of the display whose points of care should be retrieved.</param>
/// <returns>A list of points of care linked to the display, or an empty list if the display cannot be found or if an error is encountered.</returns>
public async Task<List<PointOfCare>> GetAllPocsByDisplayId(ObjectId id)
{
try
@@ -516,6 +652,11 @@ public class DisplayService(
}
}
/// <summary>
/// Retrieves the display configuration locations associated with the specified display configuration ID, mapping each display to its corresponding unit name. If a unit cannot be found for a display, the resulting location's unit name will be null.
/// </summary>
/// <param name="displayConfigId">The identifier of the display configuration whose locations are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="DisplayConfigLocationDto"/> objects with display and unit information.</returns>
public async Task<List<DisplayConfigLocationDto>> GetDisplayConfigLocations(ObjectId displayConfigId)
{
var displays = await GetByConfigId(displayConfigId);
@@ -534,6 +675,11 @@ public class DisplayService(
return locations;
}
/// <summary>
/// Determines whether the specified display configuration is currently in use by checking if it is referenced by any related entity.
/// </summary>
/// <param name="displayConfigId">The unique identifier of the display configuration to check.</param>
/// <returns><c>true</c> if the display configuration is referenced by at least one entity; otherwise, <c>false</c>.</returns>
public async Task<bool> IsDisplayConfigInUse(ObjectId displayConfigId)
{
return await displayRepository.IsDisplayConfigInUse(displayConfigId) > 0;
@@ -547,21 +693,35 @@ public class DisplayService(
* En esta actualización se espera una resubscipción al id del display ya que actualizar los PoC conlleva actualizar
* subscrioptor y locations para las observaciones
*/
/// <summary>
/// Updates the point of care list associated with the specified display, invalidating the related cache entries and broadcasting the change to subscribers.
/// </summary>
/// <param name="objectId">The identifier of the display whose point of care list is being updated.</param>
/// <param name="listPocObId">The list of point of care object identifiers to assign to the display.</param>
/// <returns>The updated <see cref="Display"/> instance after the point of care list change.</returns>
/// <exception cref="NotFoundException">Thrown when no display exists for the specified <paramref name="objectId"/>.</exception>
/// <exception cref="ConflictException">Thrown when the point of care list update cannot be persisted.</exception>
public async Task<Display?> UpdatePointOfCareList(ObjectId objectId, List<ObjectId> listPocObId)
{
var oldDisplay = await displayRepository.GetById(objectId) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var displayToReturn = await displayRepository.UpdatePointOfCareList(objectId, listPocObId) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(objectId));
SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayPoC);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay, displayToReturn);
return displayToReturn;
}
/// <summary>
/// Updates the configuration of an existing display by casting the new configuration to its specific type based on <see cref="DisplayConfigEnums.DisplayType"/>, supporting <c>DisplayNurse</c> and <c>SmartDisplay</c>. On a successful update, broadcasts the change, creates an audit log entry, and invalidates the related cache entry. Returns <c>null</c> if the provided configuration type is not supported or the cast results in <c>null</c>.
/// </summary>
/// <param name="oldDisplay">The existing display whose configuration will be updated.</param>
/// <param name="newDisplayConfig">The new configuration to apply, or <c>null</c> if no update is provided.</param>
/// <returns>The updated <see cref="Display"/> if the configuration was successfully applied; otherwise, <c>null</c>.</returns>
public async Task<Display?> UpdateConfig(Display oldDisplay, DisplayConfig? newDisplayConfig)
{
var newDisplayConfigCast = new DisplayConfig();
@@ -584,15 +744,21 @@ public class DisplayService(
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay,
displayToReturn);
}
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(oldDisplay.Id));
return displayToReturn;
}
return null;
}
/// <summary>
/// Updates the configuration ID associated with the specified display. On a successful update, the related cache entries are invalidated, a display update broadcast is sent, and an audit log entry is created.
/// </summary>
/// <param name="oldDisplay">The display whose configuration ID is being updated.</param>
/// <param name="configId">The new configuration ID to assign to the display.</param>
/// <returns>The updated display, or <c>null</c> if the display was not found.</returns>
public async Task<Display?> UpdateConfigId(Display oldDisplay, ObjectId configId)
{
var displayToReturn = await displayRepository.UpdateConfigId(oldDisplay.Id, configId);
@@ -606,13 +772,21 @@ public class DisplayService(
return displayToReturn;
}
/// <summary>
/// Updates the configuration preset associated with the specified display, invalidates the display cache, records an audit log entry, and broadcasts a notification to subscribers based on the resolved display type (DisplayNurse, SmartDisplay, or Unknown). Throws a not-found exception when the update result or configuration cannot be resolved, and an invalid-format exception when the configuration type is not one of the handled types.
/// </summary>
/// <param name="objectIdDisplay">The identifier of the display whose configuration preset is being updated.</param>
/// <param name="objectIdConfigDisplay">The identifier of the new configuration preset to apply to the display.</param>
/// <returns>The updated <see cref="Display"/> entity, or <c>null</c> if the update could not be completed.</returns>
/// <exception cref="NotFoundException">Thrown when the update result or the resolved configuration is <c>null</c>.</exception>
/// <exception cref="InvalidFormatException">Thrown when the configuration type is not one of the handled display types.</exception>
public async Task<Display?> UpdateConfigPreset(ObjectId objectIdDisplay, ObjectId objectIdConfigDisplay)
{
var oldConfig = await displayConfigService.GetById(objectIdConfigDisplay);
var result = await displayRepository.UpdateConfigPreset(objectIdDisplay, objectIdConfigDisplay);
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(objectIdDisplay));
var config = await displayConfigService.GetById(objectIdConfigDisplay);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldConfig, config);
if (result == null || config == null)
@@ -636,14 +810,21 @@ public class DisplayService(
return result;
}
/// <summary>
/// Updates the name of an existing display identified by the given identifier, invalidates the related cache entries, and records an audit log entry for the change.
/// </summary>
/// <param name="id">The unique identifier of the display to update.</param>
/// <param name="name">The new name to assign to the display.</param>
/// <returns>The updated <see cref="Display"/> instance, or <c>null</c> if the update could not be performed.</returns>
/// <exception cref="NotFoundException">Thrown when no display is found for the specified <paramref name="id"/>.</exception>
public async Task<Display?> UpdateName(ObjectId id, string name)
{
var display = await displayRepository.GetById(id) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var newDisplay = await displayRepository.UpdateName(display, name);
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(id));
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, display, newDisplay);
return newDisplay;
}
@@ -652,27 +833,37 @@ public class DisplayService(
#region Delete
/// <summary>
/// Deletes a display by its identifier, removing the record, invalidating the related cache, clearing associated authorities, and recording an audit log entry.
/// </summary>
/// <param name="id">The unique identifier of the display to delete.</param>
/// <returns>A task that resolves to <c>true</c> when the display has been successfully deleted.</returns>
/// <exception cref="NotFoundException">Thrown when no display is found for the specified <paramref name="id"/>.</exception>
public async Task<bool> DeleteDisplay(ObjectId id)
{
var display = await displayRepository.GetById(id) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
await displayRepository.DeleteAsync(id);
// Invalidar CACHE (colección completa)
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(id));
await authorityService.DeleteByDisplayId(id);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, display, null);
return true;
}
/// <summary>
/// Deletes all displays associated with the specified unit identifier, invalidates the displays cache, and removes related authority data for the unit.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose displays and related authority data will be removed.</param>
public async Task DeleteDisplaysByUnitId(ObjectId unitId)
{
await displayRepository.DeleteManyByUnitId(unitId);
// Invalidar CACHE (colección completa)
await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.Displays));
await authorityService.DeleteByUnitId(unitId);
}
@@ -680,18 +871,34 @@ public class DisplayService(
#region Send Notification
/// <summary>
/// Sends a smart display configuration update broadcast asynchronously to all specified WebSocket subscribers.
/// </summary>
/// <param name="subscribers">The list of WebSocket subscribers that will receive the smart display configuration update.</param>
/// <param name="config">The smart display configuration to broadcast. May be <c>null</c> if no configuration is provided.</param>
private void SendSmartDisplayBroadcast(List<WsSubscriber> subscribers, SmartDisplay? config)
{
foreach (var subscriber in subscribers)
_ = clientMessageService.SendAsync(subscriber.Id, OperationType.UpdateSmartDisplayConfig, config);
}
/// <summary>
/// Broadcasts a nurse display configuration update to all specified WebSocket subscribers by sending an asynchronous update message to each one.
/// </summary>
/// <param name="subscribers">The list of WebSocket subscribers that will receive the nurse display configuration update.</param>
/// <param name="config">The nurse display configuration to broadcast, which may be <c>null</c>.</param>
private void SendNurseDisplayBroadcast(List<WsSubscriber> subscribers, DisplayNurse? config)
{
foreach (var subscriber in subscribers)
_ = clientMessageService.SendAsync(subscriber.Id, OperationType.UpdateNurseDisplayConfig, config);
}
/// <summary>
/// Sends a broadcast message to all subscribers associated with the specified display.
/// Currently only handles the <see cref="OperationType.UpdateDisplayPoC"/> operation, dispatching an asynchronous notification to each subscriber; other operation types are ignored.
/// </summary>
/// <param name="display">The display whose subscribers will receive the broadcast; used to filter the subscriber list by its identifier.</param>
/// <param name="operation">The type of operation being broadcast, which determines the action taken on matching subscribers.</param>
private void SendDisplayBroadcast(Display display, OperationType operation)
{
var subscribers = subscribersService.GetSubscribers().Where(s =>
@@ -11,6 +11,9 @@ using Serilog;
namespace adas_core.Application.Services;
/// <summary>
/// Provides a concrete implementation of the <see cref="IFileService"/> contract for performing file-related operations.
/// </summary>
public class FileService : IFileService
{
private readonly string? _assetsDirectory;
@@ -32,6 +35,12 @@ public class FileService : IFileService
}
/// <summary>
/// Copies the provided uploaded files into the configured update directory, creating each file on disk.
/// Returns false if the update directory is not configured (null, empty, or whitespace); otherwise returns true after all files have been copied.
/// </summary>
/// <param name="files">The collection of uploaded form files to be written to the update directory.</param>
/// <returns>A task that resolves to true when every file is successfully copied, or false when the update directory is not configured.</returns>
public async Task<bool> CopyUpdateFiles(ICollection<IFormFile> files)
{
if (string.IsNullOrWhiteSpace(_updateDirectory)) return false;
@@ -45,6 +54,12 @@ public class FileService : IFileService
return true;
}
/// <summary>
/// Asynchronously uploads a collection of asset files into a directory organized by the specified theme. If the assets directory is not configured, the method returns false; otherwise, it ensures the target directory exists, writes each file to disk, and returns true.
/// </summary>
/// <param name="files">The collection of uploaded form files to persist to the assets directory.</param>
/// <param name="themeParse">The asset theme used to determine the subdirectory in which the files will be stored.</param>
/// <returns>A task that resolves to <c>true</c> when the files are successfully written, or <c>false</c> when the assets directory path is not configured.</returns>
public async Task<bool> UploadAssetFiles(ICollection<IFormFile> files, FileEnum.AssetTheme themeParse)
{
if (string.IsNullOrWhiteSpace(_assetsDirectory)) return false;
@@ -60,6 +75,12 @@ public class FileService : IFileService
return true;
}
/// <summary>
/// Retrieves all asset files from the subdirectory that matches the specified theme, creating the subdirectory if it does not exist.
/// Returns an empty list when the assets directory is not configured or when an error occurs while reading the files.
/// </summary>
/// <param name="themeParse">The theme used to locate the corresponding subdirectory within the assets directory.</param>
/// <returns>A list of <see cref="AssetDto"/> objects containing the name, extension, and full path of each file found; an empty list if the assets directory is not configured or an error occurs.</returns>
public List<AssetDto> GetAllAssetFile(FileEnum.AssetTheme themeParse)
{
try
@@ -91,6 +112,13 @@ public class FileService : IFileService
}
}
/// <summary>
/// Retrieves the list of file paths contained in the specified directory.
/// If the directory does not exist, a warning is logged and an empty list is returned;
/// if an error occurs during retrieval, it is logged and an empty list is returned.
/// </summary>
/// <param name="directoryPath">The path of the directory to search for files.</param>
/// <returns>A list of file paths found in the directory, or an empty list if the directory does not exist or an error occurs.</returns>
public List<string> GetFilesInDirectory(string directoryPath)
{
List<string> fileList = [];
File diff suppressed because it is too large Load Diff
@@ -19,6 +19,10 @@ public class HistoricalConfigChangesService(
{
private readonly ILogger<HistoricalConfigChangesService> _logger = logger;
/// <summary>
/// Deletes a historical configuration change record by its identifier and creates an audit log entry recording the deletion.
/// </summary>
/// <param name="id">The unique identifier of the historical configuration change to delete.</param>
public async Task DeleteHistoricalConfigChange(ObjectId id)
{
var filter = Builders<HistoricalConfigChanges>.Filter.Eq("_id", id);
@@ -28,6 +32,11 @@ public class HistoricalConfigChangesService(
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, result, null);
}
/// <summary>
/// Retrieves the most recent historical configuration change entry for the specified configuration type.
/// </summary>
/// <param name="configType">The configuration type used to look up the last historical change.</param>
/// <returns>The most recent <see cref="HistoricalConfigChanges"/> entry, or <c>null</c> if no changes exist for the given type.</returns>
public async Task<HistoricalConfigChanges?> FindLastConfigChanges(DisplayConfigEnums.ConfigTypes configType)
{
var result = await historicalConfigChangesRepository.FindLastHistoricalConfigChangesByType(configType);
@@ -35,28 +44,58 @@ public class HistoricalConfigChangesService(
return result.FirstOrDefault();
}
/// <summary>
/// Retrieves a historical configuration change record by its unique identifier.
/// Returns null when no matching record is found in the repository.
/// </summary>
/// <param name="id">The unique identifier of the historical configuration change to retrieve.</param>
/// <returns>The matching <see cref="HistoricalConfigChanges"/> record, or null if no record is found.</returns>
public async Task<HistoricalConfigChanges?> Get(ObjectId id)
{
return await historicalConfigChangesRepository.FindById(id);
}
/// <summary>
/// Retrieves all historical configuration changes from the repository.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a collection of all <see cref="HistoricalConfigChanges"/> records.</returns>
public async Task<ICollection<HistoricalConfigChanges>> GetAll()
{
return await historicalConfigChangesRepository.FindAll();
}
/// <summary>
/// Retrieves the most recent historical configuration changes for the specified configuration type, limited to a given number of entries.
/// </summary>
/// <param name="type">The configuration type used to filter the historical changes.</param>
/// <param name="num">The maximum number of recent changes to return. Defaults to 10.</param>
/// <returns>A collection of the latest <see cref="HistoricalConfigChanges"/> entries matching the specified type.</returns>
public async Task<ICollection<HistoricalConfigChanges>> GetByType(DisplayConfigEnums.ConfigTypes type, int num = 10)
{
return await historicalConfigChangesRepository.FindLastHistoricalConfigChangesByType(type, num);
}
/// <summary>
/// Retrieves the most recent historical configuration changes for a given user, optionally filtered by configuration type and limited to a specified maximum number of entries.
/// </summary>
/// <param name="user">The identifier of the user whose historical configuration changes are being retrieved.</param>
/// <param name="configTypes">Optional filter for the configuration type; when null, all configuration types are included.</param>
/// <param name="num">The maximum number of historical change entries to return. Defaults to 10.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of the user's historical configuration changes.</returns>
public async Task<ICollection<HistoricalConfigChanges>> GetByUser(string user,
DisplayConfigEnums.ConfigTypes? configTypes = null, int num = 10)
DisplayConfigEnums.ConfigTypes? configTypes = null, int num = 10)
{
return await historicalConfigChangesRepository.FindLastHistoricalConfigChangesByUser(user, configTypes, num);
}
/// <summary>
/// Inserts a new historical configuration change record into the repository and creates a corresponding audit log entry.
/// If the repository returns null, a conflict exception is thrown; on failure, the error is logged and null is returned.
/// </summary>
/// <param name="historicalConfigChanges">The historical configuration change entity to insert.</param>
/// <returns>The inserted <see cref="HistoricalConfigChanges"/> entity, or null if the operation fails.</returns>
/// <exception cref="ConflictException">Thrown when the repository returns null after the insert operation.</exception>
public async Task<HistoricalConfigChanges?> InsertOne(HistoricalConfigChanges historicalConfigChanges)
{
try
@@ -74,8 +113,15 @@ public class HistoricalConfigChangesService(
}
}
/// <summary>
/// Updates an existing historical configuration change record, creating an audit log entry for the change.
/// If the record is not found, a <see cref="ConflictException"/> is thrown; any other exception is logged and the method returns <c>null</c>.
/// </summary>
/// <param name="historicalConfigChanges">The historical configuration change entity containing the updated values to persist.</param>
/// <returns>The updated <see cref="HistoricalConfigChanges"/> entity on success, or <c>null</c> if an error occurs during the operation.</returns>
/// <exception cref="ConflictException">Thrown when no existing historical configuration change is found with the specified <c>Id</c>.</exception>
public async Task<HistoricalConfigChanges?> UpdateHistoricalConfigChange(
HistoricalConfigChanges historicalConfigChanges)
HistoricalConfigChanges historicalConfigChanges)
{
try
{
@@ -93,8 +139,17 @@ public class HistoricalConfigChangesService(
}
}
/// <summary>
/// Asynchronously logs a change to a display configuration, recording the user who made the change,
/// the configuration type, the previous value, and the new value. Logs an error if the insertion fails,
/// or a debug message if it succeeds.
/// </summary>
/// <param name="user">The username of the user who made the configuration change.</param>
/// <param name="configType">The type of configuration that was changed.</param>
/// <param name="newConfig">The new configuration value after the change.</param>
/// <param name="oldConfig">The previous configuration value before the change.</param>
public async Task LogConfigChanged(string user, DisplayConfigEnums.ConfigTypes configType, string newConfig,
string oldConfig)
string oldConfig)
{
HistoricalConfigChanges historicalConfigChanges = new()
{
@@ -4,6 +4,14 @@ namespace adas_core.Application.Services.Interfaces;
public interface IApiRequestService
{
/// <summary>
/// Asynchronously persists the specified <see cref="ApiRequest"/>.
/// </summary>
/// <param name="apiRequest">The API request to be saved.</param>
Task SaveRequestAsync(ApiRequest apiRequest);
/// <summary>
/// Asynchronously saves the specified API request.
/// </summary>
/// <param name="apiRequest">The API request to persist.</param>
Task SaveRequest(ApiRequest apiRequest);
}
@@ -7,39 +7,135 @@ namespace adas_core.Application.Services.Interfaces;
public interface IAdminPanelService
{
/// <summary>
/// Asynchronously deletes a unit identified by its unique identifier from the data store.
/// Returns a result indicating whether the deletion was successful (e.g., true if the unit was found and removed, false otherwise).
/// </summary>
/// <param name="unitId">The unique identifier of the unit to delete.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the unit was successfully deleted; otherwise, <c>false</c> if the unit was not found.</returns>
Task<bool> DeleteUnitById(ObjectId unitId);
/// <summary>
/// Inserts a new <see cref="Unit"/> into the data store and returns the inserted entity.
/// </summary>
/// <param name="unit">The <see cref="Unit"/> to insert.</param>
/// <returns>A <see cref="Task{T}"/> that resolves to the inserted <see cref="Unit"/>, or <see langword="null"/> if the insert could not be completed.</returns>
Task<Unit?> InsertUnit(Unit unit);
#region Patient
/// <summary>
/// Creates a new patient based on the provided admin panel request.
/// </summary>
/// <param name="apiRequest">The admin panel request containing the data needed to create the patient.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the created <see cref="Patient"/>, or <c>null</c> if no patient was created.</returns>
Task<Patient?> CreatePatient(AdmPanelRequest apiRequest);
/// <summary>
/// Asynchronously retrieves the patient associated with the specified location.
/// </summary>
/// <param name="location">The location used to look up the associated patient.</param>
/// <returns>A task that resolves to the <see cref="Patient"/> found at the given location, or <c>null</c> if no patient is associated with that location.</returns>
Task<Patient?> FindPatientByLocation(PatientLocation location);
/// <summary>
/// Asynchronously retrieves a <see cref="Patient"/> by their unique patient number.
/// </summary>
/// <param name="patientNumber">The unique identifier of the patient to look up.</param>
/// <returns>A <see cref="Task{TResult}"/> that yields the matching <see cref="Patient"/>, or <c>null</c> if no patient is found with the specified number.</returns>
Task<Patient?> FindPatientByPatientNumber(string patientNumber);
/// <summary>
/// Asynchronously retrieves a patient from the data store using the specified unique identifier.
/// Returns null when no patient matches the provided identifier.
/// </summary>
/// <param name="id">The unique identifier of the patient to retrieve.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the patient if found; otherwise, null.</returns>
Task<Patient?> FindPatientById(ObjectId id);
//List<person> FindAllPatient();
/// <summary>
/// Asynchronously finds and returns a <see cref="Patient"/> based on the criteria provided in the admission panel request.
/// Returns <c>null</c> when no matching patient is found.
/// </summary>
/// <param name="request">The admission panel request containing the search criteria used to locate the patient.</param>
/// <returns>A <see cref="Task{TResult}"/> that yields the matching <see cref="Patient"/> if found, or <c>null</c> otherwise.</returns>
Task<Patient?> FindPatient(AdmPanelRequest request);
/// <summary>
/// Asynchronously retrieves the dependency information DTO for the specified <paramref name="unit"/>.
/// Returns <c>null</c> when no dependency information is available for the unit.
/// </summary>
/// <param name="unit">The unit for which to retrieve dependency information.</param>
/// <returns>A <see cref="Task{UnitInfoDto}"/> that yields the unit dependency DTO, or <c>null</c> if none is found.</returns>
Task<UnitInfoDto?> GetUnitDependencyDto(Unit unit);
/// <summary>
/// Updates the patient location based on the provided admission panel request.
/// </summary>
/// <param name="request">The admission panel request containing the patient location details to update.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the update was successful; otherwise, <c>false</c>.</returns>
Task<bool> UpdatePatientLocation(AdmPanelRequest request);
/// <summary>
/// Updates the patient data based on the provided admission panel request, using the existing patient record as a reference.
/// </summary>
/// <param name="request">The admission panel request containing the updated patient data.</param>
/// <param name="oldPatient">The existing patient record to be updated.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the patient data was successfully updated; otherwise, <c>false</c>.</returns>
Task<bool> UpdatePatientData(AdmPanelRequest request, Patient oldPatient);
/// <summary>
/// Archives the specified patient, marking them as inactive while preserving their record.
/// </summary>
/// <param name="patient">The patient to be archived.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains <c>true</c> if the patient was successfully archived; otherwise, <c>false</c>.</returns>
Task<bool> ArchivePatient(Patient patient);
#endregion
#region ConfigObservations
/// <summary>
/// Asynchronously creates a configuration based on the specified observation.
/// </summary>
/// <param name="configObservation">The observation data used to create the configuration.</param>
/// <returns>A task that represents the asynchronous create operation, containing a value indicating whether the configuration was created successfully.</returns>
Task<bool> CreateConfig(ConfigObservation configObservation);
/// <summary>
/// Asynchronously updates the configuration based on the provided <see cref="ConfigObservation"/>, applying any required changes derived from the observation data.
/// </summary>
/// <param name="configObservation">The observation data used to determine and apply configuration updates.</param>
/// <returns>A <see cref="Task{Boolean}"/> that represents the asynchronous update operation, containing a value indicating whether the configuration was successfully updated.</returns>
Task<bool> UpdateConfig(ConfigObservation configObservation);
/// <summary>
/// Deletes the configuration observation item identified by the specified identifier.
/// </summary>
/// <param name="id">The unique identifier of the configuration observation item to delete.</param>
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the item was successfully deleted; otherwise, <c>false</c>.</returns>
Task<bool> DeleteConfigObservationItem(ObjectId id);
#endregion
#region Medicienes
/// <summary>
/// Retrieves a <see cref="Medicine"/> entity by its unique identifier from the data store.
/// Returns <c>null</c> when no medicine matches the provided identifier.
/// </summary>
/// <param name="medicineId">The unique <see cref="ObjectId"/> of the medicine to look up.</param>
/// <returns>A <see cref="Task{Medicine}"/> that resolves to the matching <see cref="Medicine"/>, or <c>null</c> if not found.</returns>
Task<Medicine?> GetMedicineById(ObjectId medicineId);
/// <summary>
/// Asynchronously creates and persists a new medicine record.
/// </summary>
/// <param name="medicine">The medicine entity to be created and posted.</param>
/// <returns>A task that represents the asynchronous operation, containing the newly created <see cref="Medicine"/>, or <c>null</c> if the operation fails.</returns>
Task<Medicine?> PostMedicine(Medicine medicine);
/// <summary>
/// Updates an existing medicine record in the system.
/// </summary>
/// <param name="medicine">The medicine entity containing the updated information to be persisted.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the updated <see cref="Medicine"/> if found, or <c>null</c> if the medicine does not exist.</returns>
Task<Medicine?> UpdateMedicine(Medicine medicine);
/// <summary>
/// Deletes a medicine identified by its unique identifier.
/// </summary>
/// <param name="medicineId">The unique identifier of the medicine to delete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains <c>true</c> if the medicine was successfully deleted; otherwise, <c>false</c>.</returns>
Task<bool> DeleteMedicineById(string medicineId);
#endregion
@@ -9,26 +9,121 @@ namespace adas_core.Application.Services.Interfaces;
public interface IAdmissionService : IApiRequestService
{
/// <summary>
/// Asynchronously retrieves an admission by its unique identifier, returning <c>null</c> when no matching admission is found.
/// </summary>
/// <param name="admissionId">The unique identifier of the admission to retrieve.</param>
/// <returns>A task that represents the asynchronous operation, containing the <see cref="Admission"/> if found, or <c>null</c> if no admission matches the specified identifier.</returns>
Task<Admission?> GetAdmissionByIdAsync(ObjectId admissionId);
/// <summary>
/// Asynchronously deletes an admission record identified by the specified admission ID.
/// </summary>
/// <param name="admissionId">The unique identifier of the admission to delete.</param>
Task DeleteAdmissionByIdAsync(ObjectId admissionId);
/// <summary>
/// Asynchronously deletes the specified admission record.
/// </summary>
/// <param name="admission">The admission entity to remove.</param>
Task DeleteAdmissionAsync(Admission admission);
/// <summary>
/// Deletes all admissions associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The identifier of the unit whose admissions will be deleted.</param>
Task DeleteAdmissionsByUnitId(ObjectId unitId);
/// <summary>
/// Asynchronously updates an existing admission record with the provided information.
/// </summary>
/// <param name="admission">The admission entity containing the updated data to be persisted.</param>
/// <returns>A task that represents the asynchronous update operation.</returns>
Task UpdateAdmissionAsync(Admission admission);
/// <summary>
/// Asynchronously retrieves a collection of admissions.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains an enumerable collection of <see cref="Admission"/> objects.</returns>
Task<IEnumerable<Admission>> GetAdmissionsAsync();
/// <summary>
/// Asynchronously inserts a new admission record and returns the created entry, or <see langword="null"/> if the insertion was not performed.
/// </summary>
/// <param name="admission">The admission entity to be inserted into the data store.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the inserted <see cref="Admission"/>, or <see langword="null"/> when no record is produced.</returns>
Task<Admission?> InsertAdmission(Admission admission);
/// <summary>
/// Admits a patient based on the provided admission details, optionally registering the patient as new.
/// </summary>
/// <param name="admission">The admission information used to process the patient admission.</param>
/// <param name="isNew">Indicates whether the patient is being admitted for the first time. Defaults to <c>false</c>.</param>
Task AdmitPatient(Admission admission, bool isNew = false);
/// <summary>
/// Processes the return of a patient to the admissions workflow, typically used when a patient needs to be re-queued or reinstated for admission processing.
/// </summary>
/// <param name="patientId">The unique identifier of the patient to be returned to admissions.</param>
Task ReturnPatientToAdmissions(ObjectId patientId);
/// <summary>
/// Asynchronously returns a patient to the admissions workflow using the specified admission record.
/// </summary>
/// <param name="patientId">The unique identifier of the patient being returned to admissions.</param>
/// <param name="adm">The admission record associated with the patient being returned.</param>
Task ReturnPatientToAdmissions(ObjectId patientId, Admission adm);
/// <summary>
/// Retrieves a list of admissions associated with the specified patient location.
/// </summary>
/// <param name="location">The patient location used to filter the admissions.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of admissions for the given location.</returns>
Task<List<Admission>> GetAdmissionByLocation(PatientLocation location);
/// <summary>
/// Asynchronously retrieves the list of admissions associated with the specified point of care identifier.
/// </summary>
/// <param name="id">The <see cref="ObjectId"/> of the point of care used to filter the admissions.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="Admission"/> objects matching the specified point of care id, or an empty list if no admissions are found.</returns>
Task<List<Admission>> GetAdmissionByPointOfCareId(ObjectId id);
/// <summary>
/// Retrieves the list of admissions associated with the specified point of care, localized for the given locale.
/// </summary>
/// <param name="pocId">The identifier of the point of care whose admissions are being queried.</param>
/// <param name="locale">The locale used to localize the returned admission data.</param>
/// <returns>A task that resolves to the list of admissions matching the point of care and locale; an empty list when no matches are found.</returns>
Task<List<Admission>> GetAdmissionByPointOfCareIdAndLocale(ObjectId pocId, LocaleEnum locale);
/// <summary>
/// Retrieves a list of admissions associated with the specified unit identifier, excluding any Point of Care (PoC) related admissions.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose admissions should be retrieved.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of admissions for the specified unit, excluding PoC admissions.</returns>
Task<List<Admission>> GetAdmissionByUnitIdWithOutPoC(ObjectId unitId);
/// <summary>
/// Asynchronously counts the number of admissions associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The MongoDB ObjectId of the unit whose admissions should be counted.</param>
/// <returns>A task that represents the asynchronous operation, containing the total count of admissions for the given unit.</returns>
Task<long> CountAdmissionsByUnitId(ObjectId unitId);
/// <summary>
/// Searches for a patient by their patient number within the specified unit, returning the matching patient search result or null if no match is found.
/// </summary>
/// <param name="patientNumber">The patient number used to identify the patient.</param>
/// <param name="unitId">The identifier of the unit in which the patient is being searched.</param>
/// <returns>A task that returns the matching <see cref="PatientSearch"/> if found; otherwise, null.</returns>
Task<PatientSearch?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId);
/// <summary>
/// Retrieves the admission record associated with the specified patient number, returning <c>null</c> when no matching admission is found.
/// </summary>
/// <param name="patientNumber">The unique patient number used to look up the admission.</param>
/// <returns>A task that resolves to the matching <see cref="Admission"/>, or <c>null</c> if no admission exists for the given patient number.</returns>
Task<Admission?> GetAdmissionByPatientNumber(string patientNumber);
/// <summary>
/// Updates a patient master list item based on the provided option change and related unit and type information.
/// </summary>
/// <param name="opt">The update option master list data transfer object containing the change details.</param>
/// <param name="unitList">The collection of units associated with the master list item change.</param>
/// <param name="typeName">The name of the type used to identify the master list item category.</param>
/// <returns>A task that represents the asynchronous update operation.</returns>
Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable<Unit> unitList, string typeName);
/// <summary>
/// Deletes a patient master list item identified by the specified options, units, and type name.
/// </summary>
/// <param name="opt">The option list used to identify the master list item to delete.</param>
/// <param name="unitList">The collection of units associated with the master list item.</param>
/// <param name="typeName">The name of the type associated with the master list item.</param>
Task DeletePatientMasterListItem(OptionList opt, IEnumerable<Unit> unitList, string typeName);
}

Some files were not shown because too many files have changed in this diff Show More