using adas_core.Application.Exceptions; using adas_core.Application.Repositories.Interfaces; using adas_core.Application.Services.Interfaces; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.MongoModels; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using MongoDB.Bson; namespace adas_core.Application.Services; public class RecordingAlertService( Lazy patientService, IConfigObservationService configObservationService, IRecordingAlertRepository recordingAlertRepository, IRecordingAlertArchiveRepository recordingAlertArchiveRepository, ILogger logger, IClientMessageService clientMessageService, ISubscribersService subscribersService, IHttpContextAccessor httpContextAccessor, ILocalAuditService auditService) : IRecordingAlertService { /// /// Archives the specified patient by delegating to the patient identifier-based archive method. /// /// The patient to be archived. /// A task that represents the asynchronous archive operation. public async Task Archive(Patient patient) { await ArchiveByPatientId(patient.Id); } /// /// Archives all recording alerts associated with the specified patient identifier by copying them /// to the archive repository and then deleting them from the source repository. /// /// The unique identifier of the patient whose recording alerts should be archived. public async Task ArchiveByPatientId(ObjectId id) { logger.LogDebug("Archive Recording Alerts by Patient Id {id}", id); using (var cursor = await recordingAlertRepository.FindByPatientIdAsync(id)) { while (await cursor.MoveNextAsync()) foreach (var current in cursor.Current) await recordingAlertArchiveRepository.InsertOneAsync(current); } await DeleteByPatientId(id); } /// /// Deletes all recording alerts associated with the specified patient identifier by delegating to the recording alert repository. /// /// The unique identifier of the patient whose recording alerts should be removed. public async Task DeleteByPatientId(ObjectId id) { logger.LogDebug("Delete Recording Alerts by Patient Id {id}", id); await recordingAlertRepository.DeleteByPatientId(id); } /// /// Retrieves the most recent patient recording alerts by aggregating the last observations for the specified patient. /// /// The unique identifier of the patient whose recording alerts are being queried. /// The maximum number of recent alerts to return. Defaults to 2. /// A task that represents the asynchronous operation, containing a list of the patient's most recent entries. public async Task> FindLastRecordingAlert(ObjectId patientId, int num = 2) { return await recordingAlertRepository.AggregatedPatientLastObservations(patientId, num); } /// /// Asynchronously saves the specified API request by executing the save operation on a background thread. /// /// The API request to save. /// A task that represents the asynchronous save operation. public Task SaveRequestAsync(ApiRequest apiRequest) { return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); } /// /// Saves observation data from an API request, processing ORU_R01, ORU_R40, and RecordingAlert types by resolving the associated patient and persisting each recording alert. /// If the request lacks both patient and location identifiers, or no matching patient is found, the request is ignored. /// /// The API request containing patient or location identifiers and the recording alerts to persist. /// Thrown when .Type is not a valid type for Observations. public async Task SaveRequest(ApiRequest apiRequest) { if (string.IsNullOrEmpty(apiRequest.PatientNumber) && string.IsNullOrEmpty(apiRequest.Location?.UnitName) && string.IsNullOrEmpty(apiRequest.Location?.Bed)) { logger.LogDebug("Patient and Location are nulls in apiRequest RecordingAlerts"); return; } logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}", apiRequest.PatientNumber, apiRequest.Location); logger.LogDebug("RequestType: {apiRequesttype}", apiRequest.Type); 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": case "RecordingAlert": var 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; } // Recording Alerts if (apiRequest.RecordingAlert != null && apiRequest.RecordingAlerts?.FirstOrDefault() == null) apiRequest.RecordingAlerts = [apiRequest.RecordingAlert]; if (apiRequest.RecordingAlerts != null) { logger.LogDebug("INSERT {apiRequestrecordingAlerts} OBSERVATIONS", apiRequest.RecordingAlerts.Count); foreach (var obs in apiRequest.RecordingAlerts) { obs.PatientId = patient.Id; obs.Id = ObjectId.GenerateNewId(); if (obs.Time == DateTime.MinValue) obs.Time = DateTime.UtcNow; await InsertRecordingAlert(obs); } } break; default: logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Observations", apiRequest.Type); throw new ApiRequestException("ApiRequest type " + apiRequest.Type + " is not valid for Observations"); } } /// /// Updates multiple recording alert records, replacing the with the new filtered by the specified . /// /// The name identifier used to filter the records to update. /// The new value to assign to the matching records. /// The existing value to be replaced in the matching records. public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) { await recordingAlertRepository.UpdateManyObjectId(nameId, id, oldId); } /// /// Inserts a new patient recording alert by persisting it, creating an audit log entry, broadcasting the alert, and executing any associated retention actions. /// /// The patient recording alert to be inserted and processed. private async Task InsertRecordingAlert(PatientRecordingAlert recAlert) { logger.LogDebug("Insert {recAlert}", recAlert); await recordingAlertRepository.InsertOneAsync(recAlert); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, recAlert); await SendObsBroadcast(recAlert); await DoRetentionActions(recAlert); } /// /// Asynchronously broadcasts a patient observation to subscribers whose registered locations include the patient's point of care, sending a recording alert when the observation is a recording alert and a generic observation otherwise. The method returns early without sending any message if the observation has no name, the patient cannot be resolved, or the patient has no point of care assigned. /// /// The patient observation to broadcast to matching subscribers. private async Task SendObsBroadcast(BasePatientObservation recAlert) { if (recAlert.Name == null) return; var type = recAlert is PatientRecordingAlert ? OperationType.RecordingAlert : OperationType.Observation; var patient = await patientService.Value.FindById(recAlert.PatientId); if (patient?.PointOfCareId == null) return; var subscribers = subscribersService.GetSubscribers() .Where(s => s.LocationIds.Contains(patient.PointOfCareId.Value)).ToList(); foreach (var subscriber in subscribers) await clientMessageService.SendAsync(subscriber.Id, type, recAlert); } /// /// Executes the configured retention policy for the given patient recording alert, deleting older recordings /// based on either a days-based or count-based retention value. The method performs no action and returns early /// if the retention configuration, its value, or the alert name is null. /// /// The patient recording alert whose retention actions will be evaluated and applied. /// Thrown when the resolved value is not handled by the switch statement. private async Task DoRetentionActions(PatientRecordingAlert recAlert) { var result = await configObservationService.RetentionActions(recAlert); if (result is not { RetentionPolicyValue: not null } || recAlert.Name == null) return; switch (result.RetentionPolicy) { case RetentionPolicy.DeleteOlderDays: await recordingAlertRepository.DeleteOlderDaysAsync(recAlert.Name, result.RetentionPolicyValue.Value); break; case RetentionPolicy.DeleteOlderNumber: await recordingAlertRepository.DeleteOlderNumberAsync(recAlert.Name, result.RetentionPolicyValue.Value); break; default: throw new ArgumentOutOfRangeException(); } } }