89 lines
3.4 KiB
C#
89 lines
3.4 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
public sealed class UnacknowledgedAlertsCheck
|
|
{
|
|
public static readonly ReconciliationCheckType CheckType = ReconciliationCheckType.UnacknowledgedCriticalAlert;
|
|
|
|
private readonly AppDbContext _db;
|
|
private readonly ReconciliationPublisher _publisher;
|
|
private readonly ReconciliationJobOptions _opts;
|
|
private readonly ILogger<UnacknowledgedAlertsCheck> _logger;
|
|
|
|
public UnacknowledgedAlertsCheck(
|
|
AppDbContext db,
|
|
ReconciliationPublisher publisher,
|
|
IOptions<ReconciliationJobOptions> opts,
|
|
ILogger<UnacknowledgedAlertsCheck> logger)
|
|
{
|
|
_db = db;
|
|
_publisher = publisher;
|
|
_opts = opts.Value;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<int> RunAsync(CancellationToken ct)
|
|
{
|
|
var cutoff = DateTimeOffset.UtcNow
|
|
.AddMinutes(-_opts.UnacknowledgedAlertThresholdMinutes);
|
|
|
|
// Load all stale critical alerts, grouped by encounter.
|
|
var staleAlerts = await _db.ClinicalAlerts
|
|
.Where(a => a.Severity == AlertSeverity.Critical
|
|
&& a.Status == AlertStatus.Open
|
|
&& a.TriggeredAt < cutoff)
|
|
.Select(a => new { a.Id, a.EncounterId, a.PatientId, a.TriggeredAt, a.Details })
|
|
.ToListAsync(ct);
|
|
|
|
if (!staleAlerts.Any())
|
|
{
|
|
_logger.LogDebug("[RECONCILIATION] Check1 — no stale critical alerts");
|
|
return 0;
|
|
}
|
|
|
|
// Group by encounter so one reconciliation_alert covers all alerts in that encounter.
|
|
var byEncounter = staleAlerts
|
|
.GroupBy(a => new { a.EncounterId, a.PatientId })
|
|
.ToList();
|
|
|
|
var inserted = 0;
|
|
foreach (var group in byEncounter)
|
|
{
|
|
// Deduplication: skip if an open reconciliation_alert already exists
|
|
// for this check type and encounter (partial index makes this fast).
|
|
var alreadyOpen = await _db.ReconciliationAlerts
|
|
.AnyAsync(r => r.CheckType == CheckType
|
|
&& r.EncounterId == group.Key.EncounterId
|
|
&& r.ResolvedAt == null, ct);
|
|
if (alreadyOpen) continue;
|
|
|
|
var alertIds = group.Select(a => a.Id).ToList();
|
|
var oldest = group.Min(a => a.TriggeredAt);
|
|
var details = $"{alertIds.Count} unacknowledged CRITICAL alert(s) for encounter "
|
|
+ $"{group.Key.EncounterId}. Oldest triggered at {oldest:u}. "
|
|
+ $"Alert IDs: {string.Join(", ", alertIds)}.";
|
|
|
|
var rec = new ReconciliationAlert
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
CheckType = CheckType, // ReconciliationCheckType enum
|
|
EncounterId = group.Key.EncounterId,
|
|
PatientId = group.Key.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}: {Details}",
|
|
CheckType, group.Key.EncounterId, details);
|
|
|
|
inserted++;
|
|
}
|
|
|
|
return inserted;
|
|
}
|
|
} |