Files
vigilcare-clinical/VigilCareClinicalAPI/BackgroundServices/Reconciliation/DisconnectedMonitorsCheck.cs
T

96 lines
3.5 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
public sealed class DisconnectedMonitorsCheck
{
public static readonly ReconciliationCheckType CheckType = ReconciliationCheckType.ActiveInpatientNoObservation;
private readonly AppDbContext _db;
private readonly ReconciliationPublisher _publisher;
private readonly ReconciliationJobOptions _opts;
private readonly ILogger<DisconnectedMonitorsCheck> _logger;
public DisconnectedMonitorsCheck(
AppDbContext db,
ReconciliationPublisher publisher,
IOptions<ReconciliationJobOptions> opts,
ILogger<DisconnectedMonitorsCheck> logger)
{
_db = db;
_publisher = publisher;
_opts = opts.Value;
_logger = logger;
}
public async Task<int> RunAsync(CancellationToken ct)
{
var cutoff = DateTimeOffset.UtcNow
.AddHours(-_opts.NoObservationThresholdHours);
// Limitation: this check detects absence of recorded observations, not absence
// of actual clinical monitoring. A nurse who took manual vitals but did not
// enter them into the system would still trigger a reconciliation alert.
// This is a known gap; the check errs on the side of false positives.
var candidates = await _db.Encounters
.Where(e => e.Status == EncounterStatus.Active
&& e.EncounterType == EncounterType.Inpatient)
.Select(e => new
{
EncounterId = e.Id,
e.PatientId,
LastObservationAt = e.Observations
.OrderByDescending(o => o.RecordedAt)
.Select(o => (DateTimeOffset?)o.RecordedAt)
.FirstOrDefault(),
})
.ToListAsync(ct);
var stale = candidates
.Where(e => e.LastObservationAt == null || e.LastObservationAt < cutoff)
.ToList();
if (!stale.Any())
{
_logger.LogDebug("[RECONCILIATION] Check3 — all active inpatients have recent observations");
return 0;
}
var inserted = 0;
foreach (var enc in stale)
{
var alreadyOpen = await _db.ReconciliationAlerts
.AnyAsync(r => r.CheckType == CheckType
&& r.EncounterId == enc.EncounterId
&& r.ResolvedAt == null, ct);
if (alreadyOpen) continue;
var lastObs = enc.LastObservationAt.HasValue
? $"last observation {enc.LastObservationAt:u}"
: "no observations ever recorded";
var details = $"Active inpatient encounter {enc.EncounterId} has no recent observations "
+ $"({lastObs}). Monitor may be disconnected.";
var rec = new ReconciliationAlert
{
Id = Guid.NewGuid(),
CheckType = CheckType, // ReconciliationCheckType enum
EncounterId = enc.EncounterId,
PatientId = enc.PatientId,
Details = details,
CreatedAt = DateTimeOffset.UtcNow,
};
_db.ReconciliationAlerts.Add(rec);
await _db.SaveChangesAsync(ct);
await _publisher.PublishAsync(rec, ct);
_logger.LogWarning(
"[RECONCILIATION] {CheckType} — encounter {EncounterId}: {LastObs}",
CheckType, enc.EncounterId, lastObs);
inserted++;
}
return inserted;
}
}