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 _logger; public UnacknowledgedAlertsCheck( AppDbContext db, ReconciliationPublisher publisher, IOptions opts, ILogger logger) { _db = db; _publisher = publisher; _opts = opts.Value; _logger = logger; } public async Task 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; } }