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.Filter; using adas_core.Domain.Models.MongoModels; using adas_core.Domain.Models.Responses; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using MongoDB.Bson; using MongoDB.Driver; namespace adas_core.Application.Services; /// /// Provides business logic for managing patient treatments, including insertion, /// update, deletion, archiving, pagination, and broadcasting of treatment events /// to subscribed clients. /// /// /// This service orchestrates interactions between the treatment repository, the /// patient service, configuration/calculation observation services, and audit /// services. It also handles inbound HL7-like pharmacy/treatment messages /// (OMP_O09, ORM_O01, RAS_O17) and emits broadcasts to /// subscribers based on the patient's point of care. /// public class TreatmentService( ITreatmentRepository treatmentRepository, ITreatmentArchiveRepository treatmentArchiveRepository, IPatientService patientService, IConfigObservationService configObservationService, ILogger logger, IClientMessageService clientMessageService, ISubscribersService subscribersService, Lazy calculatedObservationsService, IUnitService unitService, IHttpContextAccessor httpContextAccessor, ILocalAuditService auditService) : ITreatmentService { /// /// Retrieves all treatments associated with the specified patient identifier. /// /// The of the patient. /// /// A task that represents the asynchronous operation. The task result contains /// an of for the patient. /// public async Task> GetTreatmentsByPatientId(ObjectId id) { return await treatmentRepository.GetByPatientId(id); } /// /// Inserts a new patient treatment after applying mapping/calculation rules /// and creates an audit log entry. /// /// The to insert. /// A task that represents the asynchronous insert operation. /// /// If the treatment cannot be mapped, it is silently ignored. A broadcast /// is dispatched asynchronously upon successful insertion. /// public async Task Insert(PatientTreatment treatment) { logger.LogDebug("Insert {treatment}", treatment); var mappedTreatment = await MapTreatment(treatment); if (mappedTreatment != null) { await treatmentRepository.InsertOneAsync(mappedTreatment); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, mappedTreatment); _ = SendBroadcast(mappedTreatment, OperationType.Treatment); } } /// /// Deletes all treatments belonging to the specified patient and creates /// an audit log entry capturing the deleted state. /// /// The of the patient whose treatments will be deleted. /// /// A task that represents the asynchronous operation. The task result is /// if the deletion succeeds or there is nothing to delete; /// otherwise the method throws. /// /// /// Thrown when the patient has no treatments to delete (no record was found) /// or when the underlying delete operation fails. /// public async Task DeleteByPatientId(ObjectId id) { var treatmentToDelete = await FindByPatientId(id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); if (!treatmentToDelete.Any()) return true; logger.LogDebug("Delete Treatments by Patient Id {id}", id); var result = await treatmentRepository.DeleteByPatientId(id); if (!result) throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, treatmentToDelete, null); return result; } /// /// Archives all treatments associated with the supplied patient. /// /// The whose treatments will be archived. /// A task that represents the asynchronous archive operation. public async Task Archive(Patient patient) { await ArchiveByPatientId(patient.Id); } /// /// Copies all treatments of the specified patient into the treatment archive /// repository and subsequently removes them from the primary repository. /// /// The of the patient whose treatments will be archived. /// A task that represents the asynchronous archive operation. public async Task ArchiveByPatientId(ObjectId id) { logger.LogDebug("ArchiveB Treatments by Patient Id PatientId {id}", id); using (var cursor = await FindByPatientIdAsync(id)) { while (await cursor.MoveNextAsync()) foreach (var current in cursor.Current) await treatmentArchiveRepository.InsertOneAsync(current); } await DeleteByPatientId(id); } /// /// Updates an existing treatment and writes an audit log entry comparing the /// previous and updated values. /// /// The containing the updated values. /// /// A task that represents the asynchronous operation. The task result is /// if the update succeeds. /// /// /// Thrown when the treatment does not exist or the update operation fails. /// public async Task UpdateTreatment(PatientTreatment patientTreatment) { var oldTreatment = await treatmentRepository.GetById(patientTreatment.Id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); logger.LogDebug("Updating Treatment {treatment}", patientTreatment.ToJson()); var result = await treatmentRepository.Update(patientTreatment); if (!result) throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldTreatment, patientTreatment); _ = SendBroadcast(patientTreatment, OperationType.UpdateTreatment); return result; } /// /// Returns an asynchronous cursor over all treatments of the given patient. /// /// The of the patient. /// /// A task that represents the asynchronous operation. The task result contains /// an of . /// public async Task> FindByPatientIdAsync(ObjectId patientId) { return await treatmentRepository.FindByPatientIdAsync(patientId); } /// /// Returns all treatments of the given patient as an enumerable. /// /// The of the patient. /// /// A task that represents the asynchronous operation. The task result contains /// an of . /// public async Task> FindByPatientId(ObjectId patientId) { return await treatmentRepository.FindByPatientId(patientId); } /// /// Returns all bolus treatments of the specified patient. /// /// The of the patient. /// /// A task that represents the asynchronous operation. The task result contains /// a of representing the bolus treatments. /// public async Task> GetBolusTreatments(ObjectId patientId) { return await treatmentRepository.FindBolusTreatments(patientId); } /// /// Asynchronous entry point that delegates to . /// /// The inbound to process. /// A task that represents the asynchronous save operation. public async Task SaveRequestAsync(ApiRequest apiRequest) { await SaveRequest(apiRequest); } /// /// Processes an inbound API request and persists the corresponding treatments /// for the referenced patient. /// /// The to process. /// A task that represents the asynchronous save operation. /// /// Thrown when the patient number is missing or the request type is not supported. /// /// /// Supported message types are OMP_O09, ORM_O01 and RAS_O17. /// The method also enforces the unit's AutoAdt configuration when the /// request does not originate from a panel. /// public async Task SaveRequest(ApiRequest apiRequest) { if (string.IsNullOrEmpty(apiRequest.PatientNumber)) { logger.LogDebug("Patient is null"); throw new ApiRequestException("Patient is null"); } logger.LogDebug("patientNumber: {apiRequestpatientNumber}", apiRequest.PatientNumber); var unitConfig = await unitService.FindByName(apiRequest.Location?.UnitName); if (unitConfig != null) { if (!unitConfig.Configuration.AutoAdt && !apiRequest.RequestFromPanel) { logger.LogWarning("Unit: {UnitName} with auto adt set to false can't manage auto ADT", apiRequest.Location?.UnitName); return; } } else { logger.LogWarning("Unit not found in Mapping list: {UnitName} can't manage auto ADT", apiRequest.Location?.UnitName); return; } try { switch (apiRequest.Type) { /* * OMP EVENT * OMP_O09 - Pharmacy/treatment order * ORM_O01 - Order message * RAS_O17 - Pharmacy/treatment administration */ case "OMP_O09": case "ORM_O01": case "RAS_O17": var patient = await patientService.FindByPatientNumber(apiRequest.PatientNumber); if (patient == null) { logger.LogDebug( "Ignore treatment: {apiRequest.treatments} for patient: {apiRequest.patientNumber}, patient not found", apiRequest.Treatment, apiRequest.PatientNumber); return; } apiRequest.Treatments ??= []; if (apiRequest.Treatment != null) apiRequest.Treatments.Add(apiRequest.Treatment); foreach (var treatment in apiRequest.Treatments) { treatment.PatientId = patient.Id; treatment.MessageTime = apiRequest.MessageTime; await Insert(treatment); } break; default: throw new ApiRequestException("ApiRequest type " + apiRequest.Type + " is not valid for Treatments"); } } catch (Exception ex) { logger.LogError("ERROR SAVING REQUEST: {exMessage} trace: {exStackTrace}", ex.Message, ex.StackTrace); throw; } } /* * Return active treatments of patient PatientType active = NW, canceled= DC */ /// /// Returns the active (non-canceled, in-range) treatments of the specified patient, /// keeping the most recent active order per placer-order identifier. /// /// The of the patient. /// /// A task that represents the asynchronous operation. The task result contains /// an of nullable /// representing the active treatments. Treatments marked as Dc or /// outside their validity window are excluded. /// public async Task> GetActiveTreatmentsByPatient(ObjectId id) { var treatments = await treatmentRepository.GetByPatientId(id); var activeTreatments = treatments .Where(IsValidTreatment) .GroupBy(t => t.PlacerOrder?.EntityIdentifier) .Select(GetMostRecentActiveTreatment) .Where(t => t != null) .AsEnumerable(); return activeTreatments; } /// /// Updates every occurrence of an identifier (e.g. a referenced ) /// in stored treatments from to /// and broadcasts the changes to subscribers. /// /// The name of the field that holds the identifier. /// The new value. /// The previous value being replaced. /// A task that represents the asynchronous operation. public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) { await treatmentRepository.UpdateManyObjectId(nameId, id, oldId); var treatments = await treatmentRepository.FindByPatientId(id); foreach (var treat in treatments) _ = SendBroadcast(treat, OperationType.UpdateTreatment); } /// /// Returns a paginated list of treatments based on the supplied filter. /// /// The containing page number and page size. /// /// A task that represents the asynchronous operation. The task result contains /// a of . /// public async Task> GetPaginatedTreatments(PaginationFilter filter) { var result = treatmentRepository.GetPaginatedTreatments(filter); var count = await result.CountDocumentsAsync(); var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize) .Limit(filter.PageSize) .ToCursorAsync(); var dataList = await data.ToListAsync(); return new PaginationResponse(dataList, filter.PageNumber, filter.PageSize, count); } /// /// Returns the active treatments of a patient that match the specified placer order. /// /// The of the patient. /// The placer-order identifier to filter by. /// /// A task that represents the asynchronous operation. The task result contains /// a of . /// public async Task> GetActiveTreatmentsByPatientIdAndOrder(ObjectId patientId, string order) { return await treatmentRepository.GetActiveTreatmentsByPatientIdAndOrder(patientId, order); } /// /// Deletes a treatment by its identifier and writes an audit log entry /// capturing the deleted state. /// /// The of the treatment to delete. /// A task that represents the asynchronous delete operation. /// /// Thrown when the treatment does not exist. /// public async Task DeleteById(ObjectId id) { var oldTreatment = await treatmentRepository.GetById(id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldTreatment, null); await treatmentRepository.DeleteAsync(id); } /// /// Applies the calculated-observations and configuration-observations /// mapping pipeline to a treatment. /// /// The to map. /// /// A task that represents the asynchronous operation. The task result contains /// the mapped , or when /// the treatment is ignored by the configuration mapping. /// /// /// If the configuration-observation service returns , /// the treatment is considered ignored and a debug entry is logged. /// private async Task MapTreatment(PatientTreatment treatment) { var treatment2 = await calculatedObservationsService.Value.Map(treatment); if (treatment2 == null) return null; var treatment3 = await configObservationService.Map(treatment2); if (treatment3 == null) logger.LogDebug("Mapping {treatment}: Ignored", treatment); return treatment; } /// /// Sends a broadcast message to all subscribers whose location list includes /// the patient's point of care. /// /// The to broadcast. /// The describing the change. /// A task that represents the asynchronous broadcast operation. /// /// If the patient cannot be located or has no PointOfCareId, the /// broadcast is skipped. /// private async Task SendBroadcast(PatientTreatment treatment, OperationType operationType) { var patient = await patientService.FindById(treatment.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, operationType, treatment); } /// /// Determines whether a treatment is currently active (not canceled and /// within its start/end time window). /// /// The to evaluate. /// /// if the treatment has a placer order, is not /// discontinued () and the current UTC /// time lies within its validity window; otherwise, . /// private static bool IsValidTreatment(PatientTreatment treatment) { if (treatment.PlacerOrder == null) return false; var currentTime = DateTime.UtcNow; if (treatment.EndTime != null && currentTime.CompareTo(treatment.EndTime) > 0) return false; if (treatment.StartTime != null && currentTime.CompareTo(treatment.StartTime) < 0) return false; return treatment.OrderControl != OrderControlType.Dc; } /// /// Returns the most recent active treatment from a group of treatments /// sharing the same placer-order identifier. /// /// /// A group of instances sharing the same /// placer-order entity identifier. /// /// /// The most recent treatment whose is /// Nw or Xo, or if none qualifies. /// private static PatientTreatment? GetMostRecentActiveTreatment(IGrouping group) { return group .Where(t => t.OrderControl is OrderControlType.Nw or OrderControlType.Xo) .OrderByDescending(t => t.OrderTime) .FirstOrDefault(); } }