488 lines
21 KiB
C#
488 lines
21 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.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;
|
|
|
|
/// <summary>
|
|
/// Provides business logic for managing patient treatments, including insertion,
|
|
/// update, deletion, archiving, pagination, and broadcasting of treatment events
|
|
/// to subscribed clients.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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
|
|
/// (<c>OMP_O09</c>, <c>ORM_O01</c>, <c>RAS_O17</c>) and emits broadcasts to
|
|
/// subscribers based on the patient's point of care.
|
|
/// </remarks>
|
|
public class TreatmentService(
|
|
ITreatmentRepository treatmentRepository,
|
|
ITreatmentArchiveRepository treatmentArchiveRepository,
|
|
IPatientService patientService,
|
|
IConfigObservationService configObservationService,
|
|
ILogger<TreatmentService> logger,
|
|
IClientMessageService clientMessageService,
|
|
ISubscribersService subscribersService,
|
|
Lazy<ICalculatedObservationsService> calculatedObservationsService,
|
|
IUnitService unitService,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
ILocalAuditService auditService)
|
|
: ITreatmentService
|
|
{
|
|
/// <summary>
|
|
/// Retrieves all treatments associated with the specified patient identifier.
|
|
/// </summary>
|
|
/// <param name="id">The <see cref="ObjectId"/> of the patient.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result contains
|
|
/// an <see cref="IEnumerable{T}"/> of <see cref="PatientTreatment"/> for the patient.
|
|
/// </returns>
|
|
public async Task<IEnumerable<PatientTreatment>> GetTreatmentsByPatientId(ObjectId id)
|
|
{
|
|
return await treatmentRepository.GetByPatientId(id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a new patient treatment after applying mapping/calculation rules
|
|
/// and creates an audit log entry.
|
|
/// </summary>
|
|
/// <param name="treatment">The <see cref="PatientTreatment"/> to insert.</param>
|
|
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
|
/// <remarks>
|
|
/// If the treatment cannot be mapped, it is silently ignored. A broadcast
|
|
/// is dispatched asynchronously upon successful insertion.
|
|
/// </remarks>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all treatments belonging to the specified patient and creates
|
|
/// an audit log entry capturing the deleted state.
|
|
/// </summary>
|
|
/// <param name="id">The <see cref="ObjectId"/> of the patient whose treatments will be deleted.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result is
|
|
/// <see langword="true"/> if the deletion succeeds or there is nothing to delete;
|
|
/// otherwise the method throws.
|
|
/// </returns>
|
|
/// <exception cref="ConflictException">
|
|
/// Thrown when the patient has no treatments to delete (no record was found)
|
|
/// or when the underlying delete operation fails.
|
|
/// </exception>
|
|
public async Task<bool> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Archives all treatments associated with the supplied patient.
|
|
/// </summary>
|
|
/// <param name="patient">The <see cref="Patient"/> whose treatments will be archived.</param>
|
|
/// <returns>A task that represents the asynchronous archive operation.</returns>
|
|
public async Task Archive(Patient patient)
|
|
{
|
|
await ArchiveByPatientId(patient.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies all treatments of the specified patient into the treatment archive
|
|
/// repository and subsequently removes them from the primary repository.
|
|
/// </summary>
|
|
/// <param name="id">The <see cref="ObjectId"/> of the patient whose treatments will be archived.</param>
|
|
/// <returns>A task that represents the asynchronous archive operation.</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates an existing treatment and writes an audit log entry comparing the
|
|
/// previous and updated values.
|
|
/// </summary>
|
|
/// <param name="patientTreatment">The <see cref="PatientTreatment"/> containing the updated values.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result is
|
|
/// <see langword="true"/> if the update succeeds.
|
|
/// </returns>
|
|
/// <exception cref="ConflictException">
|
|
/// Thrown when the treatment does not exist or the update operation fails.
|
|
/// </exception>
|
|
public async Task<bool> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an asynchronous cursor over all treatments of the given patient.
|
|
/// </summary>
|
|
/// <param name="patientId">The <see cref="ObjectId"/> of the patient.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result contains
|
|
/// an <see cref="IAsyncCursor{T}"/> of <see cref="PatientTreatment"/>.
|
|
/// </returns>
|
|
public async Task<IAsyncCursor<PatientTreatment>> FindByPatientIdAsync(ObjectId patientId)
|
|
{
|
|
return await treatmentRepository.FindByPatientIdAsync(patientId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all treatments of the given patient as an enumerable.
|
|
/// </summary>
|
|
/// <param name="patientId">The <see cref="ObjectId"/> of the patient.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result contains
|
|
/// an <see cref="IEnumerable{T}"/> of <see cref="PatientTreatment"/>.
|
|
/// </returns>
|
|
public async Task<IEnumerable<PatientTreatment>> FindByPatientId(ObjectId patientId)
|
|
{
|
|
return await treatmentRepository.FindByPatientId(patientId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all bolus treatments of the specified patient.
|
|
/// </summary>
|
|
/// <param name="patientId">The <see cref="ObjectId"/> of the patient.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result contains
|
|
/// a <see cref="List{T}"/> of <see cref="PatientTreatment"/> representing the bolus treatments.
|
|
/// </returns>
|
|
public async Task<List<PatientTreatment>> GetBolusTreatments(ObjectId patientId)
|
|
{
|
|
return await treatmentRepository.FindBolusTreatments(patientId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronous entry point that delegates to <see cref="SaveRequest(ApiRequest)"/>.
|
|
/// </summary>
|
|
/// <param name="apiRequest">The inbound <see cref="ApiRequest"/> to process.</param>
|
|
/// <returns>A task that represents the asynchronous save operation.</returns>
|
|
public async Task SaveRequestAsync(ApiRequest apiRequest)
|
|
{
|
|
await SaveRequest(apiRequest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes an inbound API request and persists the corresponding treatments
|
|
/// for the referenced patient.
|
|
/// </summary>
|
|
/// <param name="apiRequest">The <see cref="ApiRequest"/> to process.</param>
|
|
/// <returns>A task that represents the asynchronous save operation.</returns>
|
|
/// <exception cref="ApiRequestException">
|
|
/// Thrown when the patient number is missing or the request type is not supported.
|
|
/// </exception>
|
|
/// <remarks>
|
|
/// Supported message types are <c>OMP_O09</c>, <c>ORM_O01</c> and <c>RAS_O17</c>.
|
|
/// The method also enforces the unit's <c>AutoAdt</c> configuration when the
|
|
/// request does not originate from a panel.
|
|
/// </remarks>
|
|
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
|
|
*/
|
|
/// <summary>
|
|
/// Returns the active (non-canceled, in-range) treatments of the specified patient,
|
|
/// keeping the most recent active order per placer-order identifier.
|
|
/// </summary>
|
|
/// <param name="id">The <see cref="ObjectId"/> of the patient.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result contains
|
|
/// an <see cref="IEnumerable{T}"/> of nullable <see cref="PatientTreatment"/>
|
|
/// representing the active treatments. Treatments marked as <c>Dc</c> or
|
|
/// outside their validity window are excluded.
|
|
/// </returns>
|
|
public async Task<IEnumerable<PatientTreatment?>> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates every occurrence of an identifier (e.g. a referenced <see cref="ObjectId"/>)
|
|
/// in stored treatments from <paramref name="oldId"/> to <paramref name="id"/>
|
|
/// and broadcasts the changes to subscribers.
|
|
/// </summary>
|
|
/// <param name="nameId">The name of the field that holds the identifier.</param>
|
|
/// <param name="id">The new <see cref="ObjectId"/> value.</param>
|
|
/// <param name="oldId">The previous <see cref="ObjectId"/> value being replaced.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a paginated list of treatments based on the supplied filter.
|
|
/// </summary>
|
|
/// <param name="filter">The <see cref="PaginationFilter"/> containing page number and page size.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result contains
|
|
/// a <see cref="PaginationResponse{T}"/> of <see cref="PatientTreatment"/>.
|
|
/// </returns>
|
|
public async Task<PaginationResponse<PatientTreatment>> 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<PatientTreatment>(dataList, filter.PageNumber, filter.PageSize, count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the active treatments of a patient that match the specified placer order.
|
|
/// </summary>
|
|
/// <param name="patientId">The <see cref="ObjectId"/> of the patient.</param>
|
|
/// <param name="order">The placer-order identifier to filter by.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result contains
|
|
/// a <see cref="List{T}"/> of <see cref="PatientTreatment"/>.
|
|
/// </returns>
|
|
public async Task<List<PatientTreatment>> GetActiveTreatmentsByPatientIdAndOrder(ObjectId patientId, string order)
|
|
{
|
|
return await treatmentRepository.GetActiveTreatmentsByPatientIdAndOrder(patientId, order);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a treatment by its identifier and writes an audit log entry
|
|
/// capturing the deleted state.
|
|
/// </summary>
|
|
/// <param name="id">The <see cref="ObjectId"/> of the treatment to delete.</param>
|
|
/// <returns>A task that represents the asynchronous delete operation.</returns>
|
|
/// <exception cref="ConflictException">
|
|
/// Thrown when the treatment does not exist.
|
|
/// </exception>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the calculated-observations and configuration-observations
|
|
/// mapping pipeline to a treatment.
|
|
/// </summary>
|
|
/// <param name="treatment">The <see cref="PatientTreatment"/> to map.</param>
|
|
/// <returns>
|
|
/// A task that represents the asynchronous operation. The task result contains
|
|
/// the mapped <see cref="PatientTreatment"/>, or <see langword="null"/> when
|
|
/// the treatment is ignored by the configuration mapping.
|
|
/// </returns>
|
|
/// <remarks>
|
|
/// If the configuration-observation service returns <see langword="null"/>,
|
|
/// the treatment is considered ignored and a debug entry is logged.
|
|
/// </remarks>
|
|
private async Task<PatientTreatment?> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a broadcast message to all subscribers whose location list includes
|
|
/// the patient's point of care.
|
|
/// </summary>
|
|
/// <param name="treatment">The <see cref="PatientTreatment"/> to broadcast.</param>
|
|
/// <param name="operationType">The <see cref="OperationType"/> describing the change.</param>
|
|
/// <returns>A task that represents the asynchronous broadcast operation.</returns>
|
|
/// <remarks>
|
|
/// If the patient cannot be located or has no <c>PointOfCareId</c>, the
|
|
/// broadcast is skipped.
|
|
/// </remarks>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether a treatment is currently active (not canceled and
|
|
/// within its start/end time window).
|
|
/// </summary>
|
|
/// <param name="treatment">The <see cref="PatientTreatment"/> to evaluate.</param>
|
|
/// <returns>
|
|
/// <see langword="true"/> if the treatment has a placer order, is not
|
|
/// discontinued (<see cref="OrderControlType.Dc"/>) and the current UTC
|
|
/// time lies within its validity window; otherwise, <see langword="false"/>.
|
|
/// </returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the most recent active treatment from a group of treatments
|
|
/// sharing the same placer-order identifier.
|
|
/// </summary>
|
|
/// <param name="group">
|
|
/// A group of <see cref="PatientTreatment"/> instances sharing the same
|
|
/// placer-order entity identifier.
|
|
/// </param>
|
|
/// <returns>
|
|
/// The most recent treatment whose <see cref="OrderControlType"/> is
|
|
/// <c>Nw</c> or <c>Xo</c>, or <see langword="null"/> if none qualifies.
|
|
/// </returns>
|
|
private static PatientTreatment? GetMostRecentActiveTreatment(IGrouping<string?, PatientTreatment> group)
|
|
{
|
|
return group
|
|
.Where(t => t.OrderControl is OrderControlType.Nw or OrderControlType.Xo)
|
|
.OrderByDescending(t => t.OrderTime)
|
|
.FirstOrDefault();
|
|
}
|
|
} |