feature: Reconciliation Jobs
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user