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 patientService, Lazy observationService, IDiagnosisService diagnosisService, IUnitService unitService, IOptions apiSettings, IOptions cacheSettings, ILogger 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 ; 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); } } 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); } } public Task SaveRequestAsync(ApiRequest apiRequest) { return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); } public async Task Archive(Patient patient) { await ArchiveByPatientId(patient.Id); } 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); } public async Task> GetByPatient(ObjectId patientId) { return await appointmentRepository.GetByPatient(patientId); } public async Task> 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), 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; } public async Task> 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), 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; } public Task> FindByPatientIdAsync(ObjectId patientId) { return appointmentRepository.FindByPatientIdAsync(patientId); } public async Task> FindByLocation(PatientLocation location) { return await appointmentRepository.FindByLocation(location); } 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); } public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) { await appointmentRepository.UpdateManyObjectId(nameId, id, oldId); } 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); } } }