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();
}
}