using System.Net.Http.Json; using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; [Collection("Integration")] public class ReconciliationTests : IAsyncLifetime { private readonly ApiFixture _fixture; private readonly HttpClient _http; public ReconciliationTests(ApiFixture fixture) { _fixture = fixture; _http = fixture.CreateClient(); _http.AsNurse(); } public Task InitializeAsync() => Task.CompletedTask; public Task DisposeAsync() => Task.CompletedTask; // ------------------------------------------------------------------------- // Check 1 — Unacknowledged critical alerts // ------------------------------------------------------------------------- [Fact] public async Task Check1_StaleCriticalAlert_CreatesReconciliationAlert() { await PrepareAsync(); var patientId = await CreatePatientAsync(); var encounterId = await CreateActiveEncounterAsync(patientId); var alertId = await IngestCriticalPotassiumAsync(encounterId); using (var scope = _fixture.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.FindAsync(alertId); alert!.TriggeredAt = DateTimeOffset.UtcNow.AddMinutes(-31); await db.SaveChangesAsync(); } await using var checkScope = _fixture.Services.CreateAsyncScope(); var check = checkScope.ServiceProvider.GetRequiredService(); var count = await check.RunAsync(CancellationToken.None); Assert.Equal(1, count); using var verifyScope = _fixture.Services.CreateScope(); var verifyDb = verifyScope.ServiceProvider.GetRequiredService(); var rec = await verifyDb.ReconciliationAlerts .FirstOrDefaultAsync(r => r.CheckType == UnacknowledgedAlertsCheck.CheckType && r.EncounterId == encounterId && r.ResolvedAt == null); Assert.NotNull(rec); Assert.Contains(alertId.ToString(), rec!.Details); } [Fact] public async Task Check1_AcknowledgedAlert_DoesNotCreateReconciliationAlert() { await PrepareAsync(); var patientId = await CreatePatientAsync(); var encounterId = await CreateActiveEncounterAsync(patientId); var alertId = await IngestCriticalPotassiumAsync(encounterId); var ackResp = await _http.PostAsJsonAsync( $"/api/v1/alerts/{alertId}/acknowledge", new AcknowledgeAlertRequest("Reviewed.")); ackResp.EnsureSuccessStatusCode(); using (var scope = _fixture.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.FindAsync(alertId); alert!.TriggeredAt = DateTimeOffset.UtcNow.AddMinutes(-31); await db.SaveChangesAsync(); } await using var checkScope = _fixture.Services.CreateAsyncScope(); var check = checkScope.ServiceProvider.GetRequiredService(); var count = await check.RunAsync(CancellationToken.None); Assert.Equal(0, count); } [Fact] public async Task Check1_SecondRun_DoesNotInsertDuplicate() { await PrepareAsync(); var patientId = await CreatePatientAsync(); var encounterId = await CreateActiveEncounterAsync(patientId); await IngestCriticalPotassiumAsync(encounterId); using (var scope = _fixture.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.FirstAsync(a => a.EncounterId == encounterId); alert.TriggeredAt = DateTimeOffset.UtcNow.AddMinutes(-31); await db.SaveChangesAsync(); } await using (var s = _fixture.Services.CreateAsyncScope()) { var c = s.ServiceProvider.GetRequiredService(); await c.RunAsync(CancellationToken.None); } int secondCount; await using (var s = _fixture.Services.CreateAsyncScope()) { var c = s.ServiceProvider.GetRequiredService(); secondCount = await c.RunAsync(CancellationToken.None); } Assert.Equal(0, secondCount); } // ------------------------------------------------------------------------- // Check 2 — Pending orders without results // ------------------------------------------------------------------------- [Fact] public async Task Check2_StalePendingOrder_CreatesReconciliationAlert() { await PrepareAsync(); var patientId = await CreatePatientAsync(); var encounterId = await CreateActiveEncounterAsync(patientId); using (var scope = _fixture.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.Orders.Add(new Order { Id = Guid.NewGuid(), EncounterId = encounterId, OrderType = OrderType.Lab, Description = "Comprehensive metabolic panel", OrderedBy = "Dr. Osei", Status = OrderStatus.Pending, OrderedAt = DateTimeOffset.UtcNow.AddHours(-5), ResultedAt = null, }); await db.SaveChangesAsync(); } await using var checkScope = _fixture.Services.CreateAsyncScope(); var check = checkScope.ServiceProvider.GetRequiredService(); var count = await check.RunAsync(CancellationToken.None); Assert.True(count >= 1); using var verifyScope = _fixture.Services.CreateScope(); var db2 = verifyScope.ServiceProvider.GetRequiredService(); var rec = await db2.ReconciliationAlerts .FirstOrDefaultAsync(r => r.CheckType == PendingOrdersCheck.CheckType && r.EncounterId == encounterId && r.ResolvedAt == null); Assert.NotNull(rec); Assert.Contains("Comprehensive metabolic panel", rec!.Details); } [Fact] public async Task Check2_ResultedOrder_DoesNotCreateReconciliationAlert() { await PrepareAsync(); var patientId = await CreatePatientAsync(); var encounterId = await CreateActiveEncounterAsync(patientId); using (var scope = _fixture.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.Orders.Add(new Order { Id = Guid.NewGuid(), EncounterId = encounterId, OrderType = OrderType.Lab, Description = "CBC", OrderedBy = "Dr. Osei", Status = OrderStatus.Resulted, OrderedAt = DateTimeOffset.UtcNow.AddHours(-5), ResultedAt = DateTimeOffset.UtcNow.AddHours(-4), }); await db.SaveChangesAsync(); } await using var checkScope = _fixture.Services.CreateAsyncScope(); var check = checkScope.ServiceProvider.GetRequiredService(); var count = await check.RunAsync(CancellationToken.None); Assert.Equal(0, count); } // ------------------------------------------------------------------------- // Check 3 — Active inpatient without recent observations // ------------------------------------------------------------------------- [Fact] public async Task Check3_ActiveInpatientNoObservations_CreatesReconciliationAlert() { await PrepareAsync(); var patientId = await CreatePatientAsync(); var encounterId = await CreateActiveEncounterAsync(patientId); await using var checkScope = _fixture.Services.CreateAsyncScope(); var check = checkScope.ServiceProvider.GetRequiredService(); var count = await check.RunAsync(CancellationToken.None); Assert.True(count >= 1); using var verifyScope = _fixture.Services.CreateScope(); var db = verifyScope.ServiceProvider.GetRequiredService(); var rec = await db.ReconciliationAlerts .FirstOrDefaultAsync(r => r.CheckType == DisconnectedMonitorsCheck.CheckType && r.EncounterId == encounterId && r.ResolvedAt == null); Assert.NotNull(rec); Assert.Contains("no observations ever recorded", rec!.Details); } [Fact] public async Task Check3_ActiveInpatientStaleObservation_CreatesReconciliationAlert() { await PrepareAsync(); var patientId = await CreatePatientAsync(); var encounterId = await CreateActiveEncounterAsync(patientId); var ingestResp = await _http.PostAsJsonAsync( $"/api/v1/encounters/{encounterId}/observations", new BatchIngestRequest(new List { new("HEART_RATE", 72, "bpm", ObservationSource.Device, DateTimeOffset.UtcNow.AddHours(-3), null), })); ingestResp.EnsureSuccessStatusCode(); await using var checkScope = _fixture.Services.CreateAsyncScope(); var check = checkScope.ServiceProvider.GetRequiredService(); var count = await check.RunAsync(CancellationToken.None); Assert.True(count >= 1); using var verifyScope = _fixture.Services.CreateScope(); var db = verifyScope.ServiceProvider.GetRequiredService(); var rec = await db.ReconciliationAlerts .FirstOrDefaultAsync(r => r.CheckType == DisconnectedMonitorsCheck.CheckType && r.EncounterId == encounterId && r.ResolvedAt == null); Assert.NotNull(rec); } [Fact] public async Task Check3_RecentObservation_DoesNotCreateReconciliationAlert() { await PrepareAsync(); var patientId = await CreatePatientAsync(); var encounterId = await CreateActiveEncounterAsync(patientId); var ingestResp = await _http.PostAsJsonAsync( $"/api/v1/encounters/{encounterId}/observations", new BatchIngestRequest(new List { new("HEART_RATE", 72, "bpm", ObservationSource.Device, DateTimeOffset.UtcNow.AddMinutes(-30), null), })); ingestResp.EnsureSuccessStatusCode(); await using var checkScope = _fixture.Services.CreateAsyncScope(); var check = checkScope.ServiceProvider.GetRequiredService(); var count = await check.RunAsync(CancellationToken.None); Assert.Equal(0, count); } [Fact] public async Task Check3_OutpatientEncounter_IsIgnored() { await PrepareAsync(); var patientId = await CreatePatientAsync(); var resp = await _http.PostAsJsonAsync( $"/api/v1/patients/{patientId}/encounters", new OpenEncounterRequest(EncounterType.Outpatient, Department.GeneralMedicine, "Dr. Osei")); resp.EnsureSuccessStatusCode(); var body = await resp.Content.ReadFromJsonAsync(); var encounterId = body!.RootElement.GetProperty("data").GetProperty("id").GetGuid(); await using var checkScope = _fixture.Services.CreateAsyncScope(); var check = checkScope.ServiceProvider.GetRequiredService(); await check.RunAsync(CancellationToken.None); using var verifyScope = _fixture.Services.CreateScope(); var db = verifyScope.ServiceProvider.GetRequiredService(); var rec = await db.ReconciliationAlerts .FirstOrDefaultAsync(r => r.CheckType == DisconnectedMonitorsCheck.CheckType && r.EncounterId == encounterId); Assert.Null(rec); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private async Task PrepareAsync() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await DbResetHelper.ResetAsync(db); db.AlertThresholds.AddRange( new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "POTASSIUM_MEQ_L", DisplayName = "Serum Potassium", Unit = "mEq/L", CriticalLow = 2.5m, WarningLow = 3.5m, WarningHigh = 5.0m, CriticalHigh = 6.5m, CreatedAt = DateTimeOffset.UtcNow, }, new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "HEART_RATE", DisplayName = "Heart Rate", Unit = "bpm", CriticalLow = 30, WarningLow = 50, WarningHigh = 100, CriticalHigh = 150, CreatedAt = DateTimeOffset.UtcNow, }); await db.SaveChangesAsync(); var redis = scope.ServiceProvider.GetRequiredService(); var cache = redis.GetDatabase(1); await cache.StringSetAsync( "threshold:POTASSIUM_MEQ_L", """{"ObservationCode":"POTASSIUM_MEQ_L","CriticalLow":2.5,"WarningLow":3.5,"WarningHigh":5.0,"CriticalHigh":6.5}"""); await cache.StringSetAsync( "threshold:HEART_RATE", """{"ObservationCode":"HEART_RATE","CriticalLow":30,"WarningLow":50,"WarningHigh":100,"CriticalHigh":150}"""); } private async Task CreatePatientAsync() { var resp = await _http.PostAsJsonAsync( "/api/v1/patients", new RegisterPatientRequest("Reconciliation", "Test", new DateOnly(1970, 6, 1), "Other")); resp.EnsureSuccessStatusCode(); var body = await resp.Content.ReadFromJsonAsync(); return body!.RootElement.GetProperty("data").GetProperty("id").GetGuid(); } private async Task CreateActiveEncounterAsync(Guid patientId) { var resp = await _http.PostAsJsonAsync( $"/api/v1/patients/{patientId}/encounters", new OpenEncounterRequest(EncounterType.Inpatient, Department.Icu, "Dr. Osei")); resp.EnsureSuccessStatusCode(); var body = await resp.Content.ReadFromJsonAsync(); return body!.RootElement.GetProperty("data").GetProperty("id").GetGuid(); } private async Task IngestCriticalPotassiumAsync(Guid encounterId) { var ingestResp = await _http.PostAsJsonAsync( $"/api/v1/encounters/{encounterId}/observations", new BatchIngestRequest(new List { new("POTASSIUM_MEQ_L", 2.1m, "mEq/L", ObservationSource.Device, DateTimeOffset.UtcNow, null), })); ingestResp.EnsureSuccessStatusCode(); var body = await ingestResp.Content.ReadFromJsonAsync(); return body!.RootElement.GetProperty("data").GetProperty("alertId").GetGuid(); } }