429 lines
21 KiB
C#
429 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.AppSettings;
|
|
using adas_core.Domain.Utils;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using MongoDB.Bson;
|
|
using MongoDB.Driver;
|
|
using Patient = adas_core.Domain.Models.MongoModels.Patient;
|
|
|
|
namespace adas_core.Application.Services;
|
|
|
|
public class AppointmentService(
|
|
IAppointmentRepository appointmentRepository,
|
|
IAppointmentArchiveRepository appointmentArchiveRepository,
|
|
Lazy<IPatientService> patientService,
|
|
Lazy<IObservationService> observationService,
|
|
IDiagnosisService diagnosisService,
|
|
IUnitService unitService,
|
|
IOptions<ApiSettings> apiSettings,
|
|
IOptions<CacheSettings> cacheSettings,
|
|
ILogger<AppointmentService> logger,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
ILocalAuditService auditService,
|
|
IPointOfCareService pointOfCareService,
|
|
ISubscribersService subscribersService,
|
|
IClientMessageService clientMessageService,
|
|
ICacheService cacheService)
|
|
: IAppointmentService
|
|
{
|
|
private readonly bool _createPatientWithSiu = apiSettings.Value.CreatePatientWithSiu;
|
|
private readonly CacheSettings? _cacheSettings = cacheSettings.Value;
|
|
|
|
/// <summary>
|
|
/// Saves an API request by validating the patient, resolving or creating the patient record, processing the request, and handling associated observations and diagnoses.
|
|
/// </summary>
|
|
/// <param name="apiRequest">The API request containing the patient number, location, type, observations, diagnosis, and related data to be processed.</param>
|
|
/// <exception cref="ApiRequestException">Thrown when the <paramref name="apiRequest"/> has a null or empty patient number.</exception>
|
|
public async Task SaveRequest(ApiRequest apiRequest)
|
|
{
|
|
if (string.IsNullOrEmpty(apiRequest.PatientNumber))
|
|
{
|
|
logger.LogDebug("Patient is null");
|
|
throw new ApiRequestException("Patient number is null");
|
|
}
|
|
|
|
logger.LogDebug("patientNumber: {apiRequestPatientNumber} location: {apiRequestLocation}",
|
|
apiRequest.PatientNumber, apiRequest.Location);
|
|
logger.LogDebug("RequestType: {apiRequestType}", apiRequest.Type);
|
|
|
|
var patient = await patientService.Value.FindByPatientNumber(apiRequest.PatientNumber);
|
|
if (patient == null)
|
|
{
|
|
// PATIENT NOT FOUND
|
|
logger.LogWarning("Patient not Found. {patientNumber}", apiRequest.PatientNumber);
|
|
if (!_createPatientWithSiu) return; // IGNORE
|
|
patient = await patientService.Value.CreatePatientFromRequest(apiRequest, true);
|
|
}
|
|
|
|
await ProcessApiRequest(apiRequest, patient);
|
|
|
|
if (patient != null)
|
|
{
|
|
if (apiRequest is { Observations: not null, ObservationData: not null })
|
|
observationService.Value.ProcessObservations(apiRequest.Observations, patient,
|
|
apiRequest.MessageTime, apiRequest.ObservationData);
|
|
else
|
|
logger.LogWarning("Observations not Found. {patientNumber} ", patient.PatientNumber);
|
|
|
|
if (apiRequest.Diagnosis != null)
|
|
_ = diagnosisService.ProcessDiagnosis(apiRequest.Diagnosis, patient, apiRequest.MessageTime);
|
|
else
|
|
logger.LogWarning("Diagnosis not Found. {patientNumber}", patient.PatientNumber);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes an incoming API request for a patient, handling HL7 SIU message types to create, update, or cancel appointments.
|
|
/// Validates the patient and auto-ADT configuration, then routes the request to the appropriate handler based on message type: SIU_S12-S14 and SIU_S18-S22 (booking/rescheduling/modification), SIU_S15-S17 (cancellation), and other types (blocked slots / no-show), persisting changes, refreshing cache, creating audit logs, and emitting broadcasts.
|
|
/// </summary>
|
|
/// <param name="apiRequest">The API request containing the HL7 message type, timestamp, and appointment payload to process.</param>
|
|
/// <param name="patient">The patient associated with the request; if null, the method returns without processing.</param>
|
|
public async Task ProcessApiRequest(ApiRequest apiRequest, Patient? patient)
|
|
{
|
|
if (patient == null)
|
|
return;
|
|
|
|
var unitConfig = await unitService.FindById(patient.UnitId);
|
|
if (!Hl7Utils.ManageAutoAdt(unitConfig, null, logger, "ORU")) return;
|
|
|
|
apiRequest.Appointments ??= [];
|
|
if (apiRequest.Appointment != null) apiRequest.Appointments.Add(apiRequest.Appointment);
|
|
//apiRequest.appointments.ForEach(ap =>
|
|
foreach (var ap in apiRequest.Appointments)
|
|
{
|
|
if (ap.VisitNumber == null || patient.Person == null) return;
|
|
|
|
ap.PatientId = patient.Id;
|
|
ap.Patient = patient.Person;
|
|
var apdb = ap.VisitNumber != null
|
|
? await appointmentRepository.FindByPatientAndVisitNumber(ap.PatientId, ap.VisitNumber)
|
|
: null;
|
|
apdb ??= await appointmentRepository.FindByPatientAndReason(ap.PatientId, ap.AppointmentReason);
|
|
|
|
if (apdb != null && DateTime.Compare(apdb.UpdateTime, apiRequest.MessageTime) > 0) return;
|
|
// TODO: Aux
|
|
var oldAp = await auditService.DeepCopyAsync(ap);
|
|
switch (apiRequest.Type)
|
|
{
|
|
//* SIU_S12 - Notification of new appointment booking
|
|
//* SIU_S13 - Notification of Appointment Rescheduling
|
|
//* SIU_S14 - Notification of Appointment Modification
|
|
//* SIU_S18 - Notification of Addition of Service/Resource on Appointment
|
|
//* SIU_S19 - Notification of Modification of Service/Resource on Appointment
|
|
//* SIU_S20 - Notification of Cancellation of Service/Resource on Appointment
|
|
//* SIU_S21 - Notification of Discontinuation of Service/Resource on Appointment
|
|
//* SIU_S22 - Notification of Deletion of Service/Resource on Appointment
|
|
|
|
case "SIU_S12":
|
|
case "SIU_S13":
|
|
case "SIU_S14":
|
|
case "SIU_S18":
|
|
case "SIU_S19":
|
|
case "SIU_S20":
|
|
case "SIU_S21":
|
|
case "SIU_S22":
|
|
ap.UpdateTime = apiRequest.MessageTime;
|
|
if (apdb != null)
|
|
{
|
|
ap.Id = apdb.Id;
|
|
ap.CreateTime = apdb.CreateTime;
|
|
ap.AppointmentOperationType = OperationType.UpdatedAppointment;
|
|
ap.ApplyResourceGroups(apdb);
|
|
await appointmentRepository.Update(ap);
|
|
// Invalidar CACHE (colección completa)
|
|
await cacheService.DeleteObjectAsync(CacheKeys.PatientAppointmentsToday(ap.PatientId));
|
|
//SendBroadcast(ap, OperationType.updatedAppointment);
|
|
}
|
|
else
|
|
{
|
|
ap.CreateTime = apiRequest.MessageTime;
|
|
ap.AppointmentOperationType = OperationType.NewAppointment;
|
|
ap.ApplyResourceGroups();
|
|
await appointmentRepository.InsertOneAsync(ap);
|
|
//SendBroadcast(ap, OperationType.newAppointment);
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
//* SIU_S15 - Notification of Appointment Cancellation
|
|
//* SIU_S16 - Notification of Appointment Discontinuation
|
|
//* SIU_S17 - Notification of Appointment Deletion
|
|
|
|
case "SIU_S15":
|
|
case "SIU_S16":
|
|
case "SIU_S17":
|
|
ap.UpdateTime = apiRequest.MessageTime;
|
|
if (apdb != null)
|
|
{
|
|
ap.Id = apdb.Id;
|
|
ap.CreateTime = apdb.CreateTime;
|
|
ap.AppointmentOperationType = OperationType.CanceledAppointment;
|
|
ap.ApplyResourceGroups(apdb);
|
|
await appointmentRepository.Update(ap);
|
|
await cacheService.DeleteObjectAsync(CacheKeys.PatientAppointmentsToday(ap.PatientId));
|
|
}
|
|
else
|
|
{
|
|
ap.CreateTime = apiRequest.MessageTime;
|
|
ap.AppointmentOperationType = OperationType.CanceledAppointment;
|
|
ap.ApplyResourceGroups();
|
|
await appointmentRepository.InsertOneAsync(ap);
|
|
}
|
|
|
|
//SendBroadcast(ap, OperationType.canceledAppointment);
|
|
break;
|
|
|
|
|
|
//* SIU_S23 - Notification of Blocked Schedule Time Slot(S)
|
|
//* SIU_S24 - Notification of Opened (un-blocked) Schedule Time Slot(s)
|
|
//* SIU_S26 - Notification That Patient Did Not Show Up for Scheduled Appointment
|
|
|
|
default:
|
|
|
|
ap.UpdateTime = apiRequest.MessageTime;
|
|
if (apdb != null)
|
|
{
|
|
ap.Id = apdb.Id;
|
|
ap.CreateTime = apdb.CreateTime;
|
|
//ap.appointmentOperationType = OperationType.updatedAppointment;
|
|
ap.ApplyResourceGroups(apdb);
|
|
await appointmentRepository.Update(ap);
|
|
await cacheService.DeleteObjectAsync(CacheKeys.PatientAppointmentsToday(ap.PatientId));
|
|
//SendBroadcast(ap, ap.appointmentOperationType);
|
|
}
|
|
else
|
|
{
|
|
ap.CreateTime = apiRequest.MessageTime;
|
|
//ap.appointmentOperationType = OperationType.newAppointment;
|
|
ap.ApplyResourceGroups();
|
|
await appointmentRepository.InsertOneAsync(ap);
|
|
//SendBroadcast(ap, ap.appointmentOperationType);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (apdb != null)
|
|
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, apdb, ap);
|
|
|
|
else await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, ap);
|
|
|
|
if (ap.AppointmentOperationType.HasValue)
|
|
await SendBroadcast(ap, ap.AppointmentOperationType);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously saves the provided API request by running the save operation on a background task.
|
|
/// </summary>
|
|
/// <param name="apiRequest">The API request to be saved.</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>
|
|
/// Archives the specified patient by delegating the operation to <see cref="ArchiveByPatientId"/> using the patient's identifier.
|
|
/// </summary>
|
|
/// <param name="patient">The patient to be archived.</param>
|
|
public async Task Archive(Patient patient)
|
|
{
|
|
await ArchiveByPatientId(patient.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Archives all appointments associated with the specified patient by copying them to the appointment archive repository and then deleting them from the source collection.
|
|
/// </summary>
|
|
/// <param name="id">The unique identifier of the patient whose appointments should be archived.</param>
|
|
public async Task ArchiveByPatientId(ObjectId id)
|
|
{
|
|
logger.LogDebug("Archive Appointments by patientId {id}", id);
|
|
using (var cursor = await FindByPatientIdAsync(id))
|
|
{
|
|
while (await cursor.MoveNextAsync())
|
|
foreach (var current in cursor.Current)
|
|
await appointmentArchiveRepository.InsertOneAsync(current);
|
|
}
|
|
|
|
await DeleteByPatientId(id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a list of patient appointments associated with the specified patient identifier by delegating to the appointment repository.
|
|
/// </summary>
|
|
/// <param name="patientId">The unique identifier of the patient whose appointments are being requested.</param>
|
|
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientAppointment"/> records for the given patient.</returns>
|
|
public async Task<List<PatientAppointment>> GetByPatient(ObjectId patientId)
|
|
{
|
|
return await appointmentRepository.GetByPatient(patientId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the list of appointments scheduled for today (UTC) for the specified patient, using a cache-aside pattern to avoid repeated database queries.
|
|
/// The full list of patient appointments is fetched from cache (or loaded from the repository on a cache miss) and then filtered locally to include only those whose start time falls on the current UTC date.
|
|
/// </summary>
|
|
/// <param name="patientId">The unique identifier of the patient whose appointments are being queried.</param>
|
|
/// <param name="ct">Cancellation token used to cancel the asynchronous operation.</param>
|
|
/// <returns>A task that resolves to a list of <see cref="PatientAppointment"/> instances scheduled for today; an empty list is returned when no appointments match.</returns>
|
|
public async Task<List<PatientAppointment>> GetTodayByPatient(
|
|
ObjectId patientId,
|
|
CancellationToken ct = default)
|
|
{
|
|
// Obtener clave + TTL según CacheSettings
|
|
var (key, ttl) = CacheKeys.PatientAppointmentsTodayKeyWithTtl(_cacheSettings, patientId);
|
|
|
|
// Cachear la lista RAW de citas del paciente (sin filtrar)
|
|
var allAppointments = await cacheService.GetOrSetObjectAsync(
|
|
key,
|
|
async () =>
|
|
{
|
|
// 1 - Consultar todas las citas del paciente
|
|
var list = await appointmentRepository.GetByPatient(patientId);
|
|
|
|
// 2 - Devuelve RAW (List<PatientAppointment>), nada filtrado
|
|
return list;
|
|
},
|
|
ttl,
|
|
ct);
|
|
|
|
// 3 - Ahora filtramos solo las de "hoy"
|
|
var today = DateTime.UtcNow.Date;
|
|
|
|
var todayAppointments = allAppointments
|
|
.Where(a => a.Timings.Any(t =>
|
|
t.StartTime.HasValue &&
|
|
t.StartTime.Value.Date == today))
|
|
.ToList();
|
|
|
|
return todayAppointments;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Retrieves the patient appointments scheduled for today at the specified point of care. Uses a cache to store the full appointment list for the point of care and filters it by today's date; returns an empty list if the point of care is not found.
|
|
/// </summary>
|
|
/// <param name="pocId">The identifier of the point of care whose appointments should be retrieved.</param>
|
|
/// <param name="ct">A cancellation token to cancel the asynchronous operation.</param>
|
|
/// <returns>A task that represents the asynchronous operation, containing the list of patient appointments scheduled for today at the specified point of care.</returns>
|
|
public async Task<List<PatientAppointment>> GetTodayByPoc(
|
|
ObjectId pocId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var poc = await pointOfCareService.FindById(pocId);
|
|
if (poc == null) return [];
|
|
|
|
// Obtener clave + TTL según CacheSettings
|
|
var (key, ttl) = CacheKeys.PocAppointmentsTodayKeyWithTtl(_cacheSettings, pocId);
|
|
|
|
// Cachear la lista RAW de citas del pointOfCare (sin filtrar)
|
|
var allAppointments = await cacheService.GetOrSetObjectAsync(
|
|
key,
|
|
async () =>
|
|
{
|
|
// 1 - Consultar todas las citas del pointOfCare
|
|
var list = await appointmentRepository.FindByPoC(poc);
|
|
|
|
// 2 - Devuelve RAW (List<PatientAppointment>), nada filtrado
|
|
return list;
|
|
},
|
|
ttl,
|
|
ct);
|
|
|
|
// 3 - Ahora filtramos solo las de "hoy"
|
|
var today = DateTime.UtcNow.Date;
|
|
|
|
var todayAppointments = allAppointments
|
|
.Where(a => a.Timings.Any(t =>
|
|
t.StartTime.HasValue &&
|
|
t.StartTime.Value.Date == today))
|
|
.ToList();
|
|
|
|
return todayAppointments;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously retrieves all patient appointments associated with the specified patient identifier by delegating to the appointment repository.
|
|
/// </summary>
|
|
/// <param name="patientId">The unique identifier of the patient whose appointments are to be retrieved.</param>
|
|
/// <returns>A task that represents the asynchronous operation, containing an async cursor over the matching <see cref="PatientAppointment"/> documents.</returns>
|
|
public Task<IAsyncCursor<PatientAppointment>> FindByPatientIdAsync(ObjectId patientId)
|
|
{
|
|
return appointmentRepository.FindByPatientIdAsync(patientId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all patient appointments associated with the specified location.
|
|
/// </summary>
|
|
/// <param name="location">The location used to filter the patient appointments.</param>
|
|
/// <returns>A task that represents the asynchronous operation, containing a list of patient appointments for the specified location.</returns>
|
|
public async Task<List<PatientAppointment>> FindByLocation(PatientLocation location)
|
|
{
|
|
return await appointmentRepository.FindByLocation(location);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Deletes all appointments associated with the specified patient identifier, invalidates the appointments cache, and records the action in the audit log.
|
|
/// </summary>
|
|
/// <param name="id">The unique identifier of the patient whose appointments will be deleted.</param>
|
|
public async Task DeleteByPatientId(ObjectId id)
|
|
{
|
|
var patientApp = await FindByPatientIdAsync(id);
|
|
logger.LogDebug("Delete Appointments by Patient Id {id}", id);
|
|
await appointmentRepository.DeleteByPatientId(id);
|
|
|
|
// Invalidar CACHE (colección completa)
|
|
await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.Appointments));
|
|
|
|
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, patientApp, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates multiple appointment records by replacing the specified <paramref name="oldId"/> with the new <paramref name="id"/>, scoped by the given <paramref name="nameId"/>. Delegates the operation to the underlying appointment repository.
|
|
/// </summary>
|
|
/// <param name="nameId">The identifier used to scope which appointment records are affected by the update.</param>
|
|
/// <param name="id">The new ObjectId that will replace the existing one in the matching records.</param>
|
|
/// <param name="oldId">The current ObjectId to be replaced in the matching records.</param>
|
|
public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
|
|
{
|
|
await appointmentRepository.UpdateManyObjectId(nameId, id, oldId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Broadcasts a patient appointment operation to all subscribers associated with the appointment's locations. Iterates through each resource group and location, resolving the unit and point of care, and dispatches a fire-and-forget message to every matching subscriber. Skips locations with missing unit/bed data and silently ignores unresolved unit or point-of-care lookups.
|
|
/// </summary>
|
|
/// <param name="appointment">The patient appointment whose resource groups and locations will be broadcast to subscribers.</param>
|
|
/// <param name="operationType">The optional operation type describing the change performed on the appointment; passed along to the subscriber message.</param>
|
|
private async Task SendBroadcast(PatientAppointment appointment, OperationType? operationType)
|
|
{
|
|
//RECORRE LOS DIFERENTES LOCATIONS DE LA CITA
|
|
foreach (var resourceGroup in appointment.ResourceGroups)
|
|
if (resourceGroup.Locations != null)
|
|
foreach (var location in resourceGroup.Locations)
|
|
// TODO
|
|
if (!string.IsNullOrEmpty(location.UnitName) && !string.IsNullOrEmpty(location.Bed))
|
|
{
|
|
var unit = await unitService.FindByName(location.UnitName);
|
|
if (unit == null) continue;
|
|
|
|
var poc = await pointOfCareService.FindByBedAndUnitId(location.Bed, unit.Id);
|
|
if (poc == null) continue;
|
|
|
|
var subscribers = subscribersService.GetSubscribers().Where(s =>
|
|
!s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c =>
|
|
c == poc.Id
|
|
)).ToList();
|
|
|
|
foreach (var subscriber in subscribers)
|
|
_ = clientMessageService.SendAsync(subscriber.Id, operationType, appointment);
|
|
}
|
|
}
|
|
} |