Files
2026-06-26 10:29:23 +02:00

222 lines
11 KiB
C#

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<IPatientService> patientService,
IConfigObservationService configObservationService,
IRecordingAlertRepository recordingAlertRepository,
IRecordingAlertArchiveRepository recordingAlertArchiveRepository,
ILogger<RecordingAlertService> logger,
IClientMessageService clientMessageService,
ISubscribersService subscribersService,
IHttpContextAccessor httpContextAccessor,
ILocalAuditService auditService)
: IRecordingAlertService
{
/// <summary>
/// Archives the specified patient by delegating to the patient identifier-based archive method.
/// </summary>
/// <param name="patient">The patient to be archived.</param>
/// <returns>A task that represents the asynchronous archive operation.</returns>
public async Task Archive(Patient patient)
{
await ArchiveByPatientId(patient.Id);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="id">The unique identifier of the patient whose recording alerts should be archived.</param>
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);
}
/// <summary>
/// Deletes all recording alerts associated with the specified patient identifier by delegating to the recording alert repository.
/// </summary>
/// <param name="id">The unique identifier of the patient whose recording alerts should be removed.</param>
public async Task DeleteByPatientId(ObjectId id)
{
logger.LogDebug("Delete Recording Alerts by Patient Id {id}", id);
await recordingAlertRepository.DeleteByPatientId(id);
}
/// <summary>
/// Retrieves the most recent patient recording alerts by aggregating the last observations for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose recording alerts are being queried.</param>
/// <param name="num">The maximum number of recent alerts to return. Defaults to 2.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of the patient's most recent <see cref="PatientRecordingAlert"/> entries.</returns>
public async Task<List<PatientRecordingAlert>> FindLastRecordingAlert(ObjectId patientId, int num = 2)
{
return await recordingAlertRepository.AggregatedPatientLastObservations(patientId, num);
}
/// <summary>
/// Asynchronously saves 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 represents the asynchronous save operation.</returns>
public Task SaveRequestAsync(ApiRequest apiRequest)
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="apiRequest">The API request containing patient or location identifiers and the recording alerts to persist.</param>
/// <exception cref="ApiRequestException">Thrown when <paramref name="apiRequest"/>.Type is not a valid type for Observations.</exception>
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");
}
}
/// <summary>
/// Updates multiple recording alert records, replacing the <paramref name="oldId"/> with the new <paramref name="id"/> filtered by the specified <paramref name="nameId"/>.
/// </summary>
/// <param name="nameId">The name identifier used to filter the records to update.</param>
/// <param name="id">The new <see cref="ObjectId"/> value to assign to the matching records.</param>
/// <param name="oldId">The existing <see cref="ObjectId"/> value to be replaced in the matching records.</param>
public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
{
await recordingAlertRepository.UpdateManyObjectId(nameId, id, oldId);
}
/// <summary>
/// Inserts a new patient recording alert by persisting it, creating an audit log entry, broadcasting the alert, and executing any associated retention actions.
/// </summary>
/// <param name="recAlert">The patient recording alert to be inserted and processed.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="recAlert">The patient observation to broadcast to matching subscribers.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="recAlert">The patient recording alert whose retention actions will be evaluated and applied.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the resolved <see cref="RetentionPolicy"/> value is not handled by the switch statement.</exception>
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();
}
}
}