using System.Reflection;
using System.Text;
using adas_core.Application.Providers;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Providers;
using adas_core.Domain.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Quartz;
using Quartz.Impl;
using Serilog;
using ILogger = Serilog.ILogger;
using Options = Microsoft.Extensions.Options.Options;
namespace adas_core.Application.Services;
///
/// Provides scheduling functionality as a service to manage and coordinate timed or periodic operations.
///
public class SchedulerService
{
private readonly bool _activateCheckExpiredAlerts;
private readonly bool _activateCheckExpiredObservations;
private readonly bool _activateCheckInactivePatients;
private readonly bool _activateSchedulerCheckActiveTreatmentsJob;
private readonly bool _activateSchedulerCheckActiveAppointmentsJob;
private readonly bool _activateSchedulerCheckHydricBalance;
private readonly bool _activateSchedulerCheckNewsJob;
private readonly bool _activateSchedulerCheckOpiateBoluses;
private readonly bool _activateSchedulerGetProvidersObservations;
private readonly ArchiveNurseData _archiveNurseData;
private readonly int _archivePatientsWithoutObservationsSinceHours;
private readonly Lazy _calculatedObservationsService;
private readonly int _checkActiveTreatmentsSchedulerIntervalMinutes;
private readonly int _checkActiveAppointmentsSchedulerIntervalMinutes;
private readonly int _checkExpiredAlertsIntervalSeconds;
private readonly int _checkExpiredObservationsIntervalMinutes;
private readonly int _checkInactivePatientSchedulerIntervalHours;
private readonly int _checkNewsJobIntervalMinutes;
private readonly Lazy _configObservationService;
private readonly int _getProviderObservationsIntervalMinutes;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
private readonly Lazy _medicineService;
private readonly Lazy _observationService;
private readonly Lazy _patientProcedureService;
private readonly Lazy _patientService;
private readonly IOptions> _providersSettings;
private readonly int _sinceDischargeTimeToArchive;
private readonly Lazy _treatmentService;
private readonly Lazy _appointmentService;
private readonly Lazy _diagnosisService;
///
/// Initializes a new instance of the class, assigning its dependencies and loading scheduler intervals and activation flags from the supplied application settings.
///
/// Application configuration providing scheduler intervals, activation flags, and archival thresholds.
/// Configuration describing the external providers whose observations are retrieved.
/// Lazy provider of patient data operations.
/// Lazy provider of treatment data operations.
/// Lazy provider of appointment data operations.
/// Lazy provider of diagnosis data operations.
/// Lazy provider of medicine data operations.
/// Lazy provider of observation data operations.
/// Lazy provider of observation configuration operations.
/// Logger used to record scheduler activity and diagnostics.
/// Factory used to create HTTP clients for provider integrations.
/// Lazy provider of calculated observations operations.
/// Lazy provider of patient care plan operations.
public SchedulerService(IOptions apiSettings,
IOptions> providersSettings,
Lazy patientService,
Lazy treatmentService,
Lazy appointmentService,
Lazy diagnosisService,
Lazy medicineService,
Lazy observationService,
Lazy configObservationService,
ILogger logger,
IHttpClientFactory httpClientFactory,
Lazy calculatedObservationsService,
Lazy patientProcedureService)
{
_patientService = patientService;
_treatmentService = treatmentService;
_appointmentService = appointmentService;
_diagnosisService = diagnosisService;
_medicineService = medicineService;
_observationService = observationService;
_configObservationService = configObservationService;
_providersSettings = providersSettings;
_httpClientFactory = httpClientFactory;
_calculatedObservationsService = calculatedObservationsService;
_patientProcedureService = patientProcedureService;
_logger = logger;
//LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true);
_archivePatientsWithoutObservationsSinceHours =
apiSettings.Value.ArchivePatientsWithoutObservationsSinceHours ?? 24;
var checkInactivePatientSchedulerIntervalHours =
apiSettings.Value.CheckInactivePatientSchedulerIntervalHours;
_checkInactivePatientSchedulerIntervalHours = checkInactivePatientSchedulerIntervalHours ?? 12;
var checkActiveTreatmentsSchedulerIntervalMinutes =
apiSettings.Value.CheckActiveTreatmentsSchedulerIntervalMinutes;
_checkActiveTreatmentsSchedulerIntervalMinutes = checkActiveTreatmentsSchedulerIntervalMinutes ?? 5;
var checkActiveAppointmentsSchedulerIntervalMinutes =
apiSettings.Value.CheckActiveAppointmentsSchedulerIntervalMinutes;
_checkActiveAppointmentsSchedulerIntervalMinutes = checkActiveAppointmentsSchedulerIntervalMinutes ?? 60;
var getProvidersObservationsIntervalMinutes =
apiSettings.Value.GetProvidersObservationsSchedulerIntervalMinutes;
_getProviderObservationsIntervalMinutes = getProvidersObservationsIntervalMinutes ?? 5;
var sinceDischargeTimeToArchive = apiSettings.Value.SinceDischargeTimeToArchive;
_sinceDischargeTimeToArchive = sinceDischargeTimeToArchive ?? 48;
var activateSchedulerCheckExpiredObservations = apiSettings.Value.ActivateSchedulerCheckExpiredObservation;
_activateCheckExpiredObservations = activateSchedulerCheckExpiredObservations;
_activateCheckExpiredAlerts = apiSettings.Value.ActivateCheckExpiredAlerts;
_activateCheckInactivePatients = apiSettings.Value.ActivateCheckInactivePatients;
_activateSchedulerCheckActiveTreatmentsJob = apiSettings.Value.ActivateSchedulerCheckActiveTreatmentsJob;
_activateSchedulerCheckActiveAppointmentsJob = apiSettings.Value.ActivateSchedulerCheckActiveAppointmentsJob;
_activateSchedulerCheckOpiateBoluses = apiSettings.Value.ActivateSchedulerCheckOpiateBoluses;
_activateSchedulerCheckHydricBalance = apiSettings.Value.ActivateSchedulerCheckHydricBalance;
_activateSchedulerGetProvidersObservations = apiSettings.Value.ActivateSchedulerGetProvidersObservations;
_archiveNurseData = apiSettings.Value.ArchiveNurseData;
var schedulerCheckExpiredObservationsIntervalMinutes =
apiSettings.Value.SchedulerCheckExpiredObservationsIntervalMinutes;
_checkExpiredObservationsIntervalMinutes = schedulerCheckExpiredObservationsIntervalMinutes ?? 3;
var schedulerCheckExpiredAlertsIntervalSeconds =
apiSettings.Value.SchedulerCheckExpiredAlertsIntervalSeconds;
_checkExpiredAlertsIntervalSeconds = schedulerCheckExpiredAlertsIntervalSeconds ?? 6;
_activateSchedulerCheckNewsJob = apiSettings.Value.ActivateSchedulerCheckNewsJob;
_checkNewsJobIntervalMinutes =
apiSettings.Value.CheckNewsJobIntervalMinutes;
Task.Run(InitScheduler);
}
///
/// Initializes the Quartz scheduler by wiring up service dependencies for background jobs, creating job and trigger definitions, and conditionally scheduling them based on feature flag configuration settings before starting the scheduler.
///
private async Task InitScheduler()
{
//TODO con el cambio a .net core y el repaso a la inyección de dependencias de estructure map
//probar a inyectar la dependencia así https://dev.to/bohdanstupak1/dependency-injection-for-quartz-net-in-net-core-3oh7
CheckInactivePatientsJob.PatientService = _patientService.Value;
CheckActiveTreatmentsJob.MedicineService = _medicineService.Value;
CheckActiveTreatmentsJob.PatientService = _patientService.Value;
CheckActiveTreatmentsJob.TreatmentService = _treatmentService.Value;
CheckActiveTreatmentsJob.CalculatedObservationsService = _calculatedObservationsService.Value;
CheckActiveAppointmentsJob.AppointmentService = _appointmentService.Value;
CheckActiveAppointmentsJob.DiagnosisService = _diagnosisService.Value;
CheckActiveAppointmentsJob.PatientService = _patientService.Value;
ArchiveNurseDataJob.PatientCarePlanService = _patientProcedureService.Value;
ArchiveNurseDataJob.PatientService = _patientService.Value;
ArchiveNurseDataJob.ArchiveNurseDataSettings = _archiveNurseData;
CheckOpiateBolusesJob.PatientService = _patientService.Value;
CheckOpiateBolusesJob.CalculatedObservationsService = _calculatedObservationsService.Value;
CheckHydricBalanceRyCJob.ObservationService = _observationService.Value;
CheckHydricBalanceRyCJob.PatientService = _patientService.Value;
CheckHydricBalanceRyCJob.CalculatedObservationsService = _calculatedObservationsService.Value;
CheckExpiredObservationsJob.ObservationService = _observationService.Value;
CheckExpiredAlertsJob.ObservationService = _observationService.Value;
GetProvidersObservationsJob.ObservationService = _observationService.Value;
GetProvidersObservationsJob.PatientService = _patientService.Value;
GetProvidersObservationsJob.Providers = _providersSettings.Value;
GetProvidersObservationsJob.HttpClientFactory = _httpClientFactory;
CalculateNewsJob.ObservationService = _observationService.Value;
CalculateNewsJob.ConfigObservationService = _configObservationService.Value;
CalculateNewsJob.PatientService = _patientService.Value;
CalculateNewsJob.ExpiresIn = _checkNewsJobIntervalMinutes;
var jobDataMap = new JobDataMap
{
{ "archivePatientsWithoutObservationsSinceHours", _archivePatientsWithoutObservationsSinceHours },
{ "sinceDischargeTime", _sinceDischargeTimeToArchive }
};
// Grab the Scheduler instance from the Factory
var factory = new StdSchedulerFactory();
var scheduler = await factory.GetScheduler();
// and start it off
await scheduler.Start();
var checkInactivePatientsJob = JobBuilder.Create()
.UsingJobData(jobDataMap)
.Build();
// Trigger the job to run now, and then repeat every 10 seconds
var checkInactivePatientsTrigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInHours(_checkInactivePatientSchedulerIntervalHours)
.RepeatForever())
.Build();
var treatmentsJob = JobBuilder.Create()
.Build();
// Trigger the job to run now, and then repeat every 10 seconds
var treatmentsTrigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(_checkActiveTreatmentsSchedulerIntervalMinutes)
.RepeatForever())
.Build();
var appointmentsJob = JobBuilder.Create()
.Build();
var appointmentsTrigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(_checkActiveAppointmentsSchedulerIntervalMinutes)
.RepeatForever())
.Build();
//Job only for RYC hydric balance.
//Check every hour if hydric balance of that hour exists and recalculate.
var hydricBalanceJob = JobBuilder.Create()
.UsingJobData(jobDataMap)
.Build();
var datetimePlusOneHour = DateTime.UtcNow.AddHours(1);
var hydricBalanceTrigger = TriggerBuilder.Create()
.StartAt(new DateTime(datetimePlusOneHour.Year, datetimePlusOneHour.Month, datetimePlusOneHour.Day,
datetimePlusOneHour.Hour, 0, 0))
.WithSimpleSchedule(x => x
.WithIntervalInHours(1)
.RepeatForever())
.Build();
//Check_error_queues
var opiateBolusesJob = JobBuilder.Create()
.Build();
var opiateBolusesTrigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(5)
.RepeatForever())
.Build();
//Job to expire expired observations
//Check every minute if observations are expired and recalculate.
var checkExpiredObservationsJob = JobBuilder.Create()
.Build();
var checkExpiredObservationsTrigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(_checkExpiredObservationsIntervalMinutes)
.RepeatForever())
.Build();
//Job to expire Alert
//Check every minute if Alerts are expired and power off.
var checkExpiredAlertsJob = JobBuilder.Create()
.Build();
var checkExpiredAlertsTrigger = TriggerBuilder.Create()
.StartAt(DateBuilder.FutureDate(30, IntervalUnit.Second)) // Comienza 30segundos después del inicio
.WithSimpleSchedule(x => x
//.WithIntervalInMinutes(_checkExpiredAlertsIntervalMinutes)
.WithIntervalInSeconds(_checkExpiredAlertsIntervalSeconds)
.RepeatForever())
.Build();
//Job to get Providers (ADAS) observations
//Check every minute if Alerts are expired and power off.
var getProvidersObservationsJob = JobBuilder.Create()
.Build();
var getProvidersObservationsTrigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(_getProviderObservationsIntervalMinutes)
.RepeatForever())
.Build();
//Job to get Providers (ADAS) observations
//Check every minute if Alerts are expired and power off.
var calculateNewsJob = JobBuilder.Create()
.Build();
var calculateNewsJobTrigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(_checkNewsJobIntervalMinutes)
.RepeatForever())
.StartNow()
.Build();
//Job to ArchiveNurseDataConfig
//Check every 15 minute if Patients has data to archive.
var archiveNurseDataJob = JobBuilder.Create()
.Build();
var archiveNurseDataJobTrigger = TriggerBuilder.Create()
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(_archiveNurseData.IntervalMinutes)
.RepeatForever())
.StartNow()
.Build();
var jobsDictionary = new Dictionary>();
if (_activateCheckExpiredObservations)
jobsDictionary.Add(checkExpiredObservationsJob, new List { checkExpiredObservationsTrigger });
if (_activateCheckExpiredAlerts)
jobsDictionary.Add(checkExpiredAlertsJob, new List { checkExpiredAlertsTrigger });
if (_activateCheckInactivePatients)
jobsDictionary.Add(checkInactivePatientsJob, new List { checkInactivePatientsTrigger });
if (_activateSchedulerCheckActiveTreatmentsJob)
jobsDictionary.Add(treatmentsJob, new List { treatmentsTrigger });
if (_activateSchedulerCheckOpiateBoluses)
jobsDictionary.Add(opiateBolusesJob, new List { opiateBolusesTrigger });
if (_activateSchedulerCheckHydricBalance)
jobsDictionary.Add(hydricBalanceJob, new List { hydricBalanceTrigger });
if (_activateSchedulerGetProvidersObservations && _providersSettings.Value.Any())
jobsDictionary.Add(getProvidersObservationsJob, new List { getProvidersObservationsTrigger });
if (_activateSchedulerCheckNewsJob)
jobsDictionary.Add(calculateNewsJob, new List { calculateNewsJobTrigger });
if (_archiveNurseData.Active)
jobsDictionary.Add(archiveNurseDataJob, new List { archiveNurseDataJobTrigger });
_logger.LogDebug("Tell to schedule the job using our trigger. {jobsDictionary}", jobsDictionary);
// Tell to schedule the job using our trigger
await scheduler.ScheduleJobs(jobsDictionary, true);
// and last shut down the scheduler when you are ready to close your program
//await scheduler.Shutdown();
}
}
///
/// Represents a scheduled job that checks the hydric balance of the RyC component, implemented as a Quartz.NET job.
///
///
/// The DisallowConcurrentExecution attribute prevents overlapping executions of this job.
///
[DisallowConcurrentExecution]
public class CheckHydricBalanceRyCJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
public static IObservationService ObservationService { get; set; } = null!;
public static IPatientService PatientService { get; set; } = null!;
public static ICalculatedObservationsService CalculatedObservationsService { get; set; } = null!;
///
/// Executes the scheduled job that checks hydric balance for all patients and maps the most recent observation recorded within the current hour window.
/// Iterates over every patient, retrieves the latest "Hydric_Balance" observation before a cutoff time, and forwards it to the calculated observations service when it falls within the current hour.
/// All exceptions raised during processing are caught and logged, allowing the job to finish without interrupting the scheduler.
///
/// The Quartz job execution context provided by the scheduler when the job trigger fires.
public async Task Execute(IJobExecutionContext context)
{
var start = DateTime.Now;
//LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true);
_logger.Debug("Start check hydric balance job");
try
{
var patients = await PatientService.FindAll();
foreach (var patient in patients)
{
var lastHydricBalance = await ObservationService.FindLastBeforeDate(patient.Id,
DateTime.UtcNow.AddMinutes(50), "Hydric_Balance");
if (lastHydricBalance != null &&
DateTime.Compare(
new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, DateTime.UtcNow.Day,
DateTime.UtcNow.Hour, 0, 0),
new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month,
lastHydricBalance.Time.Day, lastHydricBalance.Time.Hour, 0, 0))
== 0
)
await CalculatedObservationsService.Map(lastHydricBalance);
}
}
catch (Exception ex)
{
_logger.Error("ERROR executing check hydric balance job {exMessage}", ex.Message);
}
var end = DateTime.Now;
_logger.Debug("Finished check hydric balance job in {TotalMilliseconds:F1} ms",
(end - start).TotalMilliseconds);
}
}
///
/// Represents a scheduled job that checks the status of active treatments.
///
///
/// The DisallowConcurrentExecution attribute ensures that only one instance of this job can execute at any given time.
///
[DisallowConcurrentExecution]
public class CheckActiveTreatmentsJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
public static ITreatmentService TreatmentService { get; set; } = null!;
public static IMedicineService MedicineService { get; set; } = null!;
public static IPatientService PatientService { get; set; } = null!;
public static ICalculatedObservationsService CalculatedObservationsService { get; set; } = null!;
///
/// Executes the scheduled job that iterates through all patients, retrieves their active treatments and the associated medicines (excluding nutrition-type medicines), and calculates medicine observations for each patient. Exceptions are caught and logged without rethrowing, and the total execution duration is logged upon completion.
///
/// The job execution context provided by the scheduler.
public async Task Execute(IJobExecutionContext context)
{
var start = DateTime.Now;
//LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true);
_logger.Debug("Executing check active treatments job");
try
{
var patients = await PatientService.FindAll();
foreach (var patient in patients)
{
var activeMedicines = new List();
//var treatments = treatmentService.GetActiveTreatmentsByPatient(patient.id);
var treatments =
await CalculatedObservationsService.GetActiveTreatmentsByPatient(patient.Id);
var medicines = await MedicineService.GetMedicinesOfTreatments(treatments);
var medicinesList = medicines.ToList();
var medicinesWithoutNutrition = medicinesList.Where(m => !m.Type.Contains("Nutrition"))
.ToList();
activeMedicines.AddRange(medicinesWithoutNutrition);
await CalculatedObservationsService.CalculateMedicineObservation(activeMedicines,
patient.Id);
}
}
catch (Exception ex)
{
_logger.Error("ERROR executing check active treatments job {exMessage}", ex.Message);
}
var end = DateTime.Now;
_logger.Debug("Finished check active treatments job in {TotalMilliseconds:F1} ms",
(end - start).TotalMilliseconds);
}
}
///
/// Represents a scheduled job that checks for active appointments, implemented as an .
///
///
/// The attribute prevents multiple instances of this job from running at the same time.
///
[DisallowConcurrentExecution]
public class CheckActiveAppointmentsJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
public static IAppointmentService AppointmentService { get; set; } = null!;
public static IDiagnosisService DiagnosisService { get; set; } = null!;
public static IPatientService PatientService { get; set; } = null!;
///
/// Executes the scheduled check active appointments job, logging the start and completion times along with the total elapsed time. Any exception thrown during execution is caught and logged without being rethrown.
///
/// The Quartz scheduler context that provides runtime information for the job execution.
public async Task Execute(IJobExecutionContext context)
{
var start = DateTime.Now;
_logger.Debug("Executing check active appointments job");
try
{
//TODO falta definir
}
catch (Exception ex)
{
_logger.Error("ERROR executing check active treatments job {exMessage}", ex.Message);
}
var end = DateTime.Now;
_logger.Debug("Finished check active treatments job in {TotalMilliseconds:F1} ms",
(end - start).TotalMilliseconds);
}
}
///
/// Represents a scheduled job that checks for inactive patients.
///
///
/// The DisallowConcurrentExecution attribute prevents multiple instances of this job from running simultaneously.
///
[DisallowConcurrentExecution]
public class CheckInactivePatientsJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
public static IPatientService PatientService { get; set; } = null!;
///
/// Executes the scheduled job that discharges patients who have been inactive (without observations) for a configured period of hours.
/// Skips processing when another instance is already running, as indicated by the global isCheckingInactivePatients flag, and logs the elapsed execution time.
///
/// The Quartz job execution context whose data map supplies the inactivity and discharge thresholds.
public async Task Execute(IJobExecutionContext context)
{
//LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true);
var start = DateTime.Now;
var dataMap = context.JobDetail.JobDataMap;
var archivePatientsWithoutObservationsSinceHours =
dataMap.GetInt("archivePatientsWithoutObservationsSinceHours");
var sinceDischargeTime = dataMap.GetInt("sinceDischargeTime");
var sinceDate = DateTime.Now.AddHours(-archivePatientsWithoutObservationsSinceHours);
_logger.Debug(
"Executing Check inactive patients job archivePatientsWithoutObservationsSinceHours: {archivePatientsWithoutObservationsSinceHours} sinceDate: {sinceDate} sinceDischargeTime: {sinceDischargeTime}",
archivePatientsWithoutObservationsSinceHours, sinceDate, sinceDischargeTime);
try
{
var exists = GlobalData.Data.TryGetValue("isCheckingInactivePatients", out var isChecking);
if (!exists || isChecking is false)
await PatientService.DischargeInactivePatients(sinceDate, sinceDischargeTime);
}
catch (Exception ex)
{
_logger.Error("ERROR executing check inactive patients job {ExMessage}", ex.Message);
}
var end = DateTime.Now;
_logger.Debug("Finished Check inactive patients job in {TotalMilliseconds:F1} ms",
(end - start).TotalMilliseconds);
}
}
///
/// Represents a scheduled job that checks opiate boluses, implementing the interface.
///
///
/// The DisallowConcurrentExecution attribute ensures that overlapping executions of this job are prevented, guaranteeing that only one instance runs at a time.
///
[DisallowConcurrentExecution]
public class CheckOpiateBolusesJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
public static IPatientService PatientService { get; set; } = null!;
public static ICalculatedObservationsService CalculatedObservationsService { get; set; } = null!;
///
/// Executes a scheduled job that calculates opiate bolus observations for all patients.
/// Retrieves every patient via the patient service and triggers the bolus opiates calculation for each one,
/// logging any errors that occur and reporting the total execution time upon completion.
///
/// The Quartz job execution context provided by the scheduler for this job run.
public async Task Execute(IJobExecutionContext context)
{
var start = DateTime.Now;
//LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true);
_logger.Debug("Executing check opiate boluses job");
try
{
var patients = await PatientService.FindAll();
foreach (var patient in patients) await CalculatedObservationsService.CalculateBolusOpiates(patient.Id);
}
catch (Exception ex)
{
_logger.Error("ERROR executing check opiate boluses job {ExMessage} trace: {ExStackTrace}", ex.Message,
ex.StackTrace);
}
var end = DateTime.Now;
_logger.Debug("Finished Check opiate boluses job in {TotalSeconds:F1} seconds", (end - start).TotalSeconds);
}
}
///
/// Represents a background job that checks for expired observations, scheduled to run using the Quartz.NET job scheduling system.
///
///
/// The attribute ensures that only one instance of this job can execute at a given time, preventing overlapping runs that could lead to duplicate processing of expired observations.
///
[DisallowConcurrentExecution]
public class CheckExpiredObservationsJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
public static IObservationService ObservationService { get; set; } = null!;
///
/// Executes the scheduled job that checks and expires observations, recalculating dependent data. Uses a global flag to prevent concurrent execution of the expiration logic, and logs the start, duration, and any errors encountered.
///
/// The job execution context provided by the scheduler.
public async Task Execute(IJobExecutionContext context)
{
var start = DateTime.Now;
//LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true);
_logger.Debug("Executing check expired observations job");
try
{
var exists = GlobalData.Data.TryGetValue("isCheckingExpiration", out var isChecking);
if (!exists)
{
GlobalData.AddData("isCheckingExpiration", false);
isChecking = false;
}
if (!exists || isChecking is false) await ObservationService.ExpireObservationsAndRecalculateAsync();
}
catch (Exception ex)
{
_logger.Error("ERROR executing check expired observations {ExMessage} trace: {ExStackTrace}",
ex.Message, ex.StackTrace);
}
var end = DateTime.Now;
_logger.Debug("Finished Check expired observations job in {TotalSeconds:F1} seconds",
(end - start).TotalSeconds);
}
}
///
/// Job for calculate NEWS based on FR, SPo2, Temperature, Tas and FC
///
[DisallowConcurrentExecution]
public class CalculateNewsJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
public static IObservationService ObservationService { get; set; } = null!;
public static IConfigObservationService ConfigObservationService { get; set; } = null!;
public static IPatientService PatientService { get; set; } = null!;
public static int ExpiresIn { get; set; } = 15;
///
/// Executes the scheduled job that calculates the National Early Warning Score (NEWS) for each patient
/// located in an active Point of Care, based on the most recent vital sign observations
/// (respiratory rate, SpO2, temperature, systolic blood pressure, and heart rate).
/// Observations flagged as expired or whose configured expiration time has elapsed are skipped,
/// and the resulting aggregate score is stored as a new NEWS observation for the patient when
/// the latest underlying observation is not expired.
///
/// The Quartz job execution context provided by the scheduler.
public async Task Execute(IJobExecutionContext context)
{
//Filter patients in real locations
//LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true);
var patients = await PatientService.FindInActivePoC();
var observationList = new List { "Resp_Rate_Calculated", "SpO2", "Temperature", "TAs", "FC" };
foreach (var patient in patients)
{
var observations =
await CheckExpiredAlertsJob.ObservationService.FindLastObservations(patient.Id, 1, observationList);
if (!observations.Any()) continue;
var newsScore = 0;
var msg = new StringBuilder();
_logger.Debug("CalculateNewsJob - patient ({patientPointOfCare} {patientBed})", patient.UnitString,
patient.Bed);
foreach (var obs in observations)
{
var conf = await ConfigObservationService.Get(obs);
var expires = conf?.Expires;
if (obs.Expired || (expires != null &&
obs.Time.AddSeconds(expires.Value).ToUniversalTime() < DateTime.UtcNow))
{
_logger.Verbose(
"CalculateNewsJob - patient ({patientPointOfCare} {patientBed}) obs ({obsName} value {obsValue} time {obsTime} expired {obsExpired}) confexpires: {expires} EXPIRED",
patient.UnitString, patient.Bed, obs.Name, obs.Value, obs.Time, obs.Expired, expires);
continue;
}
_logger.Verbose(
"CalculateNewsJob - patient ({patientPointOfCare} {patientBed}) obs ({obsName} value {obsValue} time {obsTime} expired {obsExpired}) confexpires: {expires} NOT EXPIRED",
patient.UnitString, patient.Bed, obs.Name, obs.Value, obs.Time, obs.Expired, expires);
var partialNewsScore = 0;
switch (obs.Name)
{
case "Resp_Rate_Calculated":
if (!double.TryParse(obs.Value.ToString(), out var frValue)) continue;
switch (Math.Round(frValue, 0))
{
case >= 12 and <= 20:
break;
case >= 25:
case <= 8:
partialNewsScore = 3;
break;
case >= 9 and <= 11:
partialNewsScore = 1;
break;
case >= 21 and <= 24:
partialNewsScore = 2;
break;
}
break;
case "SpO2":
if (!double.TryParse(obs.Value.ToString(), out var spo2Value)) continue;
switch (Math.Round(spo2Value, 0))
{
case <= 91:
partialNewsScore = 3;
break;
case >= 92 and <= 93:
partialNewsScore = 2;
break;
case >= 94 and <= 95:
partialNewsScore = 1;
break;
}
break;
case "Temperature":
if (!double.TryParse(obs.Value.ToString(), out var tempValue)) continue;
switch (Math.Round(tempValue, 1))
{
case <= 35:
partialNewsScore = 3;
break;
case >= 35.1 and <= 36.0:
case >= 38.1 and <= 39.0:
partialNewsScore = 1;
break;
case >= 39.1:
partialNewsScore = 2;
break;
}
break;
case "TAs":
if (!double.TryParse(obs.Value.ToString(), out var tasValue)) continue;
switch (Math.Round(tasValue, 0))
{
case <= 90:
case >= 220:
partialNewsScore = 3;
break;
case >= 91 and <= 100:
partialNewsScore = 2;
break;
case >= 101 and <= 110:
partialNewsScore = 1;
break;
}
break;
case "FC":
if (!double.TryParse(obs.Value.ToString(), out var fcValue)) continue;
switch (Math.Round(fcValue, 0))
{
case <= 40:
case >= 131:
partialNewsScore = 3;
break;
case >= 111 and <= 130:
partialNewsScore = 2;
break;
case >= 41 and <= 50:
case >= 91 and <= 110:
partialNewsScore = 1;
break;
}
break;
}
msg.Append($"[{obs.Name} {obs.Value} {partialNewsScore}] ");
newsScore += partialNewsScore;
}
var lastObs =
await CheckExpiredAlertsJob.ObservationService.FindLastObservations(patient.Id, 1, observationList);
if (lastObs.FirstOrDefault()?.Expired != true)
{
var newObs = new PatientObservation
{
Name = "NEWS",
CodingSystem = "ADAS",
Expires = ExpiresIn,
Value = newsScore,
PatientId = patient.Id,
Time = DateTime.Now
};
await CheckExpiredAlertsJob.ObservationService.InsertObservation(newObs);
_logger.Debug(
$"CalculateNewsJob - patient ({patient.UnitString} {patient.Bed}) newsScore: {newsScore} from {msg}");
}
}
}
}
///
/// Represents a scheduled job that checks for expired alerts.
///
///
/// This job is decorated with the to prevent overlapping executions of the same job instance.
///
[DisallowConcurrentExecution]
public class CheckExpiredAlertsJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
public static IObservationService ObservationService { get; set; } = null!;
///
/// Executes the scheduled job that checks for expired alerts and triggers the power-off routine, while preventing concurrent executions and logging execution duration.
/// Runs the expiration logic only when the global "isCheckingAlertsExpiration" flag is absent or set to false; any exception is caught and logged without being rethrown.
///
/// The Quartz job execution context provided by the scheduler when the job trigger fires.
public async Task Execute(IJobExecutionContext context)
{
var start = DateTime.Now;
//LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true);
_logger.Debug("Executing check expired alerts and power off job");
try
{
var exists = GlobalData.Data.TryGetValue("isCheckingAlertsExpiration", out var isChecking);
if (!exists || isChecking is false) await ObservationService.ExpireAlertsAndPowerOffAsync();
}
catch (Exception ex)
{
_logger.Error("ERROR executing check expired alerts and power off {ExMessage} trace: {ExStackTrace}",
ex.Message,
ex.StackTrace);
}
var end = DateTime.Now;
_logger.Debug("Finished Check check expired alerts and power off job in {TotalSeconds:F1} seconds",
(end - start).TotalSeconds);
}
}
///
/// Represents a scheduled job that archives nurse data, preventing concurrent executions of the same job instance.
///
///
/// The attribute ensures that the job will not be triggered while a previous execution is still running.
///
[DisallowConcurrentExecution]
public class ArchiveNurseDataJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
//private readonly ArchiveNurseData _archiveNurseData;
public static ArchiveNurseData ArchiveNurseDataSettings { get; set; } = new();
public static IPatientService PatientService { get; set; } = null!;
public static IPatientCarePlanService PatientCarePlanService { get; set; } = null!;
///
/// Executes the ArchiveNurseDataJob which archives finished nurse procedures, tests, and treatments for patients whose associated end dates exceed the configured thresholds defined in . Each archive category is only processed when its corresponding setting flag is active, and any exception is caught and logged without interrupting the remaining work.
///
/// The Quartz.NET job execution context that provides runtime information for the scheduled job execution.
public async Task Execute(IJobExecutionContext context)
{
var start = DateTime.Now;
_logger.Debug("Executing ArchiveNurseDataJob job");
try
{
if (ArchiveNurseDataSettings.ArchiveProcedure.Active)
{
var patientWithFinishedProcedures =
await PatientService.FindAllPatientWithFinishedProcedures(ArchiveNurseDataSettings.ArchiveProcedure
.EndDateAfterMinutes);
foreach (var patientWithFinishedProcedure in patientWithFinishedProcedures)
{
var itemsToArchive = patientWithFinishedProcedure.Procedures?
.Where(c =>
c is
{
//OptionType: "procedure",
EndDate: not null
} &&
c.EndDate.Value.AddMinutes(ArchiveNurseDataSettings.ArchiveProcedure.EndDateAfterMinutes) <
DateTime.UtcNow)
.ToList();
if (itemsToArchive != null)
{
_ = PatientCarePlanService.ArchiveCarePlanFromJob(patientWithFinishedProcedure,
itemsToArchive);
patientWithFinishedProcedure.Procedures = patientWithFinishedProcedure.Procedures?
.Where(c => !itemsToArchive.Contains(c)).ToList();
await PatientService.Update(patientWithFinishedProcedure);
}
}
}
if (ArchiveNurseDataSettings.ArchiveTest.Active)
{
var patientWithFinishedTests =
await PatientService.FindAllPatientWithFinishedTest(ArchiveNurseDataSettings.ArchiveTest
.EndDateAfterMinutes);
foreach (var patientWithFinishedTest in patientWithFinishedTests)
{
var itemsToArchive = patientWithFinishedTest.Tests?
.Where(c =>
c is
{
//OptionType: "test",
EndDate: not null
} &&
c.EndDate.Value.AddMinutes(ArchiveNurseDataSettings.ArchiveTest.EndDateAfterMinutes) <
DateTime.UtcNow)
.ToList();
if (itemsToArchive != null)
{
_ = PatientCarePlanService.ArchiveCarePlanFromJob(patientWithFinishedTest,
itemsToArchive);
patientWithFinishedTest.Tests = patientWithFinishedTest.Tests?
.Where(c => !itemsToArchive.Contains(c)).ToList();
await PatientService.Update(patientWithFinishedTest);
}
}
}
if (ArchiveNurseDataSettings.ArchiveTreatment.Active)
{
var patientWithFinishedTreatments =
await PatientService.FindAllPatientWithFinishedTreatment(ArchiveNurseDataSettings.ArchiveTreatment
.EndDateAfterMinutes);
foreach (var patientWithFinishedTreatment in patientWithFinishedTreatments)
{
var itemsToArchive = patientWithFinishedTreatment.Treatment?
.Where(c =>
c.EndDate.HasValue &&
c.EndDate.Value.AddMinutes(ArchiveNurseDataSettings.ArchiveTest.EndDateAfterMinutes) <
DateTime.UtcNow)
.ToList();
if (itemsToArchive != null)
{
_ = PatientCarePlanService.ArchiveCarePlanFromJob(patientWithFinishedTreatment,
itemsToArchive);
patientWithFinishedTreatment.Treatment = patientWithFinishedTreatment.Treatment?
.Where(c => !itemsToArchive.Contains(c))
.ToList();
await PatientService.Update(patientWithFinishedTreatment);
}
}
}
}
catch (Exception ex)
{
_logger.Error("ERROR executing ArchiveNurseDataJob {ExMessage} trace: {ExStackTrace}",
ex.Message,
ex.StackTrace);
}
var end = DateTime.Now;
_logger.Debug("Finished ArchiveNurseDataJob {TotalSeconds:F1} seconds",
(end - start).TotalSeconds);
}
}
///
/// Represents a scheduled job that retrieves observations associated with providers.
///
/// The DisallowConcurrentExecution attribute prevents overlapping executions of this job instance.
[DisallowConcurrentExecution]
public class GetProvidersObservationsJob : IJob
{
private readonly ILogger _logger = Log.ForContext();
public static IObservationService ObservationService { get; set; } = null!;
public static IPatientService PatientService { get; set; } = null!;
public static List Providers { get; set; } = null!;
public static IHttpClientFactory HttpClientFactory { get; set; } = null!;
///
/// Executes the scheduled job that retrieves observations from all configured providers for every patient.
/// Uses reflection to resolve and instantiate each provider type (mapping the assembly name by replacing '-' with '_'),
/// skips providers whose type or required constructor cannot be found, and logs and recovers from any exception raised during processing.
///
/// The Quartz job execution context provided by the scheduler when the job is triggered.
public async Task Execute(IJobExecutionContext context)
{
var start = DateTime.Now;
_logger.Debug("Executing get providers job");
try
{
if (!Providers.Any()) return;
foreach (var provider in Providers)
{
//replace - to _ to fit name
var providerType =
Type.GetType(
$"{Assembly.GetExecutingAssembly().GetName().Name?.Replace("-", "_")}.Providers.{provider.Name}Provider");
if (providerType == null)
{
Log.Error("Provider {ProviderName} not able lo load by reflection", provider.Name);
continue;
}
var ctor = providerType.GetConstructor([
typeof(IOptions),
typeof(IHttpClientFactory)
]);
if (ctor == null)
{
Log.Error("not able to find constructor for provider: {ProviderName}", provider.Name);
continue;
}
var customProvider = (BaseProvider)ctor.Invoke([
Options.Create(provider),
HttpClientFactory
]);
var patients = await PatientService.FindAll();
foreach (var patient in patients)
{
var observations = await customProvider.GetObservations(patient);
if (observations.Any()) observations.ForEach(o => ObservationService.InsertObservation(o));
}
}
}
catch (Exception ex)
{
_logger.Error(
"ERROR executing GetByCodeSysAndCode providers observations job error: {ExMessage} trace: {ExStackTrace}",
ex.Message, ex.StackTrace);
}
var end = DateTime.Now;
_logger.Debug("Finished GetByCodeSysAndCode providers observations job in {TotalMilliseconds:F1} ms",
(end - start).TotalMilliseconds);
}
}