feature: Explainable Alerts
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
[Collection("Integration")]
|
||||
public class ExplainableAlertsTests : IAsyncLifetime
|
||||
{
|
||||
private readonly ApiFixture _fixture;
|
||||
|
||||
public ExplainableAlertsTests(ApiFixture fixture) => _fixture = fixture;
|
||||
|
||||
public async Task InitializeAsync() =>
|
||||
await ExplainableAlertsTestHelper.ResetAsync(_fixture);
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task News2Alert_IncludesScoreContributors()
|
||||
{
|
||||
var (encounterId, _) = await ExplainableAlertsTestHelper.SeedEncounterWithVitalsAsync(
|
||||
_fixture, respRate: 28, spo2: 92, systolicBp: 95, heartRate: 110,
|
||||
tempC: 38.5m, supplementalO2: 1m);
|
||||
|
||||
var alert = await ExplainableAlertsTestHelper.WaitForAlertAsync(
|
||||
_fixture, encounterId, AlertType.News2Emergency);
|
||||
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, alert.Id);
|
||||
|
||||
body.Explanation.Should().NotBeNull();
|
||||
body.Explanation!.ScoreContributors.Should().HaveCount(7);
|
||||
body.Explanation.ScoreContributors.Should().Contain(c =>
|
||||
c.Parameter == "Respiratory Rate" && c.Points > 0);
|
||||
body.Explanation.NarrativeSummary.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SofaAlert_IncludesOrganContributors()
|
||||
{
|
||||
var (encounterId, patientId) = await ExplainableAlertsTestHelper.SeedSofaBaselineAsync(_fixture);
|
||||
await ExplainableAlertsTestHelper.AdvanceSofaDeltaAsync(_fixture, encounterId, patientId, delta: 2);
|
||||
|
||||
var alert = await ExplainableAlertsTestHelper.WaitForAlertAsync(
|
||||
_fixture, encounterId, AlertType.SofaSepsis);
|
||||
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, alert.Id);
|
||||
|
||||
body.Explanation.Should().NotBeNull();
|
||||
body.Explanation!.ScoreContributors.Should().HaveCount(6);
|
||||
body.Explanation.ScoreContributors.Should().Contain(c => c.Parameter == "Respiratory");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GcsAlert_IncludesEyeVerbalMotor()
|
||||
{
|
||||
var (encounterId, patientId) = await ExplainableAlertsTestHelper.SeedEncounterAsync(_fixture);
|
||||
await ExplainableAlertsTestHelper.RecordGcsAsync(
|
||||
_fixture, encounterId, patientId, eye: 2, verbal: 2, motor: 3);
|
||||
|
||||
var alert = await ExplainableAlertsTestHelper.WaitForAlertAsync(
|
||||
_fixture, encounterId, AlertType.GcsCritical);
|
||||
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, alert.Id);
|
||||
|
||||
body.Explanation.Should().NotBeNull();
|
||||
body.Explanation!.ScoreContributors.Should().HaveCount(3);
|
||||
body.Explanation.ScoreContributors.Should().Contain(c => c.Parameter == "Eye");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TrendAlert_IncludesTrendContext()
|
||||
{
|
||||
var (encounterId, patientId) = await ExplainableAlertsTestHelper.SeedEncounterAsync(_fixture);
|
||||
await ExplainableAlertsTestHelper.SimulateRapidRespRateRiseAsync(
|
||||
_fixture, encounterId, patientId);
|
||||
|
||||
var alert = await ExplainableAlertsTestHelper.WaitForAlertAsync(
|
||||
_fixture, encounterId, AlertType.RapidDeterioration);
|
||||
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, alert.Id);
|
||||
|
||||
body.Explanation.Should().NotBeNull();
|
||||
body.Explanation!.Trend.Should().NotBeNull();
|
||||
body.Explanation.Trend!.PercentChange.Should().BeGreaterThan(0);
|
||||
body.Explanation.Trend.Duration.Should().NotBe(default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MedicationAlert_IncludesDrugContext()
|
||||
{
|
||||
var (encounterId, patientId) = await ExplainableAlertsTestHelper.SeedEncounterAsync(_fixture);
|
||||
await ExplainableAlertsTestHelper.AdministerMedicationAsync(
|
||||
_fixture, encounterId, "acetaminophen", dose: 1000, unit: "mg");
|
||||
await ExplainableAlertsTestHelper.FeedNews2VitalsAsync(
|
||||
_fixture, encounterId, patientId,
|
||||
respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
|
||||
tempC: 39.2m, supplementalO2: 0m);
|
||||
|
||||
var alert = await ExplainableAlertsTestHelper.WaitForFirstAlertWithExplanationAsync(
|
||||
_fixture, encounterId);
|
||||
|
||||
alert.Explanation.Should().NotBeNull();
|
||||
alert.Explanation!.MedicationContext.Should().NotBeNull();
|
||||
alert.Explanation.MedicationContext!.DrugName.Should().BeEquivalentTo("acetaminophen");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NarrativeSummary_IsHumanReadable()
|
||||
{
|
||||
var alert = await ExplainableAlertsTestHelper.GetAnyAlertWithExplanationAsync(_fixture);
|
||||
|
||||
alert.Explanation.Should().NotBeNull();
|
||||
alert.Explanation!.NarrativeSummary.Should().MatchRegex("(NEWS2|SOFA|GCS|rapid deterioration)", "i");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyAlert_NullExplanation_Serializes()
|
||||
{
|
||||
var legacyId = await ExplainableAlertsTestHelper.SeedLegacyAlertWithoutExplanationAsync(_fixture);
|
||||
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, legacyId);
|
||||
|
||||
body.Explanation.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListByEncounter_ReturnsExplanation()
|
||||
{
|
||||
var (encounterId, _) = await ExplainableAlertsTestHelper.SeedEncounterWithVitalsAsync(
|
||||
_fixture, respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
|
||||
tempC: 37.0m, supplementalO2: 0m);
|
||||
|
||||
using var client = _fixture.CreateClient();
|
||||
var response = await client.GetAsync($"/api/v1/encounters/{encounterId}/alerts");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var body = await response.Content.ReadFromJsonAsync<ApiResponse<PagedResult<AlertResponse>>>(
|
||||
ExplainableAlertsTestHelper.JsonOptions);
|
||||
|
||||
body!.Data!.Items.Should().NotBeEmpty();
|
||||
body.Data.Items.Should().AllSatisfy(a =>
|
||||
{
|
||||
a.Explanation.Should().NotBeNull();
|
||||
a.Explanation!.NarrativeSummary.Should().NotBeNullOrWhiteSpace();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ScoringOutputs_Unchanged()
|
||||
{
|
||||
var before = await ExplainableAlertsTestHelper.RunNews2HighScoreReplayAsync(_fixture);
|
||||
var after = await ExplainableAlertsTestHelper.RunNews2HighScoreReplayAsync(_fixture);
|
||||
|
||||
after.TotalScore.Should().Be(before.TotalScore);
|
||||
after.AlertType.Should().Be(before.AlertType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExplainableAlert_OutboxPayload_IncludesExplanation()
|
||||
{
|
||||
var (encounterId, _) = await ExplainableAlertsTestHelper.SeedEncounterWithVitalsAsync(
|
||||
_fixture, respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
|
||||
tempC: 37.0m, supplementalO2: 0m);
|
||||
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var outbox = await db.OutboxEvents
|
||||
.Where(e => e.Topic == "alert.generated" && e.PartitionKey == encounterId.ToString())
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.FirstAsync();
|
||||
|
||||
outbox.Payload.Should().Contain("explanation");
|
||||
outbox.Payload.Should().Contain("narrativeSummary");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StackExchange.Redis;
|
||||
|
||||
public static class ExplainableAlertsTestHelper
|
||||
{
|
||||
public static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
private static readonly DateTimeOffset TrendBaseTime =
|
||||
new(2026, 6, 18, 10, 0, 0, TimeSpan.Zero);
|
||||
|
||||
public static async Task ResetAsync(ApiFixture fixture)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await DbResetHelper.ResetAsync(db);
|
||||
|
||||
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
|
||||
var server = redis.GetServer(redis.GetEndPoints().First());
|
||||
await server.FlushDatabaseAsync(1);
|
||||
}
|
||||
|
||||
public static async Task<(Guid EncounterId, Guid PatientId)> SeedEncounterAsync(ApiFixture fixture)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var patient = new Patient
|
||||
{
|
||||
Id = Guid.NewGuid(), Mrn = $"MRN-EXP-{Guid.NewGuid():N}"[..16],
|
||||
FirstName = "Explain", LastName = "Test",
|
||||
DateOfBirth = new DateOnly(1970, 1, 1), Gender = "M",
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
var encounter = new Encounter
|
||||
{
|
||||
Id = Guid.NewGuid(), PatientId = patient.Id, EncounterType = EncounterType.Inpatient,
|
||||
Status = EncounterStatus.Active, Department = Department.Icu,
|
||||
AttendingPhysician = "Dr. Explain", AdmittedAt = DateTimeOffset.UtcNow,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
db.Patients.Add(patient);
|
||||
db.Encounters.Add(encounter);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return (encounter.Id, patient.Id);
|
||||
}
|
||||
|
||||
public static async Task FeedNews2VitalsAsync(
|
||||
ApiFixture fixture, Guid encounterId, Guid patientId,
|
||||
int respRate, decimal spo2, decimal systolicBp, decimal heartRate,
|
||||
decimal tempC, decimal supplementalO2, decimal avpu = 0m)
|
||||
{
|
||||
await ClearNews2KeysAsync(fixture, encounterId);
|
||||
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var detector = scope.ServiceProvider.GetRequiredService<News2Detector>();
|
||||
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "RESP_RATE", respRate);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "SPO2", spo2);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "SYSTOLIC_BP", systolicBp);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "HEART_RATE", heartRate);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "AVPU", avpu);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "TEMP_C", tempC);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "SUPPLEMENTAL_O2", supplementalO2);
|
||||
}
|
||||
|
||||
public static async Task<(Guid EncounterId, Guid PatientId)> SeedEncounterWithVitalsAsync(
|
||||
ApiFixture fixture,
|
||||
int respRate, decimal spo2, decimal systolicBp, decimal heartRate,
|
||||
decimal tempC, decimal supplementalO2, decimal avpu = 0m)
|
||||
{
|
||||
var (encounterId, patientId) = await SeedEncounterAsync(fixture);
|
||||
await FeedNews2VitalsAsync(
|
||||
fixture, encounterId, patientId, respRate, spo2, systolicBp, heartRate,
|
||||
tempC, supplementalO2, avpu);
|
||||
return (encounterId, patientId);
|
||||
}
|
||||
|
||||
public static async Task<(Guid EncounterId, Guid PatientId)> SeedSofaBaselineAsync(ApiFixture fixture)
|
||||
{
|
||||
var (encounterId, patientId) = await SeedEncounterAsync(fixture);
|
||||
await SeedSofaBaselineInputsAsync(fixture, encounterId, patientId);
|
||||
return (encounterId, patientId);
|
||||
}
|
||||
|
||||
public static async Task AdvanceSofaDeltaAsync(
|
||||
ApiFixture fixture, Guid encounterId, Guid patientId, int delta = 2)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var detector = scope.ServiceProvider.GetRequiredService<SofaDetector>();
|
||||
|
||||
if (delta >= 2)
|
||||
{
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "PLATELET_K_UL", 20m, DateTimeOffset.UtcNow);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "CREATININE_MG_DL", 4.5m, DateTimeOffset.UtcNow);
|
||||
}
|
||||
else
|
||||
{
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "PLATELET_K_UL", 120m, DateTimeOffset.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task RecordGcsAsync(
|
||||
ApiFixture fixture, Guid encounterId, Guid patientId, int eye, int verbal, int motor)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var detector = scope.ServiceProvider.GetRequiredService<GcsDetector>();
|
||||
await ClearGcsKeysAsync(fixture, encounterId);
|
||||
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "GCS_EYE", eye);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "GCS_VERBAL", verbal);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "GCS_MOTOR", motor);
|
||||
}
|
||||
|
||||
public static async Task SimulateRapidRespRateRiseAsync(
|
||||
ApiFixture fixture, Guid encounterId, Guid patientId)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var detector = scope.ServiceProvider.GetRequiredService<TrendDetector>();
|
||||
await ClearTrendKeysAsync(fixture, encounterId);
|
||||
|
||||
await detector.ProcessObservationAsync(
|
||||
encounterId, patientId, "RESP_RATE", 14m, TrendBaseTime);
|
||||
await detector.ProcessObservationAsync(
|
||||
encounterId, patientId, "RESP_RATE", 28m, TrendBaseTime.AddMinutes(10));
|
||||
}
|
||||
|
||||
public static async Task AdministerMedicationAsync(
|
||||
ApiFixture fixture, Guid encounterId, string drugName, decimal dose, string unit)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var medService = scope.ServiceProvider.GetRequiredService<IMedicationService>();
|
||||
await medService.CreateAsync(encounterId,
|
||||
new CreateMedicationAdministrationRequest(
|
||||
drugName, dose, unit, "PO",
|
||||
DateTimeOffset.UtcNow.AddMinutes(-30), "nurse-test"));
|
||||
}
|
||||
|
||||
public static async Task RecordVitalAsync(
|
||||
ApiFixture fixture, Guid encounterId, Guid patientId, string code, decimal value)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var detector = scope.ServiceProvider.GetRequiredService<News2Detector>();
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, code, value);
|
||||
}
|
||||
|
||||
public static async Task<ClinicalAlert> WaitForAlertAsync(
|
||||
ApiFixture fixture, Guid encounterId, AlertType alertType)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var alert = await db.ClinicalAlerts
|
||||
.AsNoTracking()
|
||||
.Where(a => a.EncounterId == encounterId && a.AlertType == alertType)
|
||||
.OrderByDescending(a => a.TriggeredAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (alert is null)
|
||||
throw new InvalidOperationException(
|
||||
$"Expected {alertType} alert on encounter {encounterId} but none was found.");
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
public static async Task<T> GetAlertAsync<T>(ApiFixture fixture, Guid alertId)
|
||||
{
|
||||
using var client = fixture.CreateClient();
|
||||
var response = await client.GetAsync($"/api/v1/alerts/{alertId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var body = await response.Content.ReadFromJsonAsync<ApiResponse<T>>(JsonOptions);
|
||||
return body!.Data!;
|
||||
}
|
||||
|
||||
public static async Task<AlertResponse> WaitForFirstAlertWithExplanationAsync(
|
||||
ApiFixture fixture, Guid encounterId)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var alert = await db.ClinicalAlerts
|
||||
.AsNoTracking()
|
||||
.Where(a => a.EncounterId == encounterId && a.Explanation != null)
|
||||
.OrderByDescending(a => a.TriggeredAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (alert is null)
|
||||
throw new InvalidOperationException(
|
||||
$"Expected an alert with explanation on encounter {encounterId}.");
|
||||
|
||||
return await GetAlertAsync<AlertResponse>(fixture, alert.Id);
|
||||
}
|
||||
|
||||
public static async Task<AlertResponse> GetAnyAlertWithExplanationAsync(ApiFixture fixture)
|
||||
{
|
||||
await SeedEncounterWithVitalsAsync(
|
||||
fixture, respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
|
||||
tempC: 37.0m, supplementalO2: 0m);
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var alert = await db.ClinicalAlerts
|
||||
.AsNoTracking()
|
||||
.Where(a => a.Explanation != null)
|
||||
.OrderByDescending(a => a.TriggeredAt)
|
||||
.FirstAsync();
|
||||
|
||||
return await GetAlertAsync<AlertResponse>(fixture, alert.Id);
|
||||
}
|
||||
|
||||
public static async Task<Guid> SeedLegacyAlertWithoutExplanationAsync(ApiFixture fixture)
|
||||
{
|
||||
var (encounterId, patientId) = await SeedEncounterAsync(fixture);
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var alertId = Guid.NewGuid();
|
||||
db.ClinicalAlerts.Add(new ClinicalAlert
|
||||
{
|
||||
Id = alertId,
|
||||
EncounterId = encounterId,
|
||||
PatientId = patientId,
|
||||
AlertType = AlertType.CriticalPotassiumMeqL,
|
||||
Severity = AlertSeverity.Critical,
|
||||
Details = "Potassium 2.1 mEq/L is below critical low.",
|
||||
Status = AlertStatus.Open,
|
||||
TriggeredAt = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Explanation = null
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
return alertId;
|
||||
}
|
||||
|
||||
public static async Task<News2ReplayResult> RunNews2HighScoreReplayAsync(ApiFixture fixture)
|
||||
{
|
||||
var (encounterId, patientId) = await SeedEncounterWithVitalsAsync(
|
||||
fixture, respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
|
||||
tempC: 37.0m, supplementalO2: 0m);
|
||||
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var score = await db.News2Scores
|
||||
.Where(s => s.EncounterId == encounterId)
|
||||
.OrderByDescending(s => s.CalculatedAt)
|
||||
.FirstAsync();
|
||||
var alert = await db.ClinicalAlerts
|
||||
.Where(a => a.EncounterId == encounterId)
|
||||
.OrderByDescending(a => a.TriggeredAt)
|
||||
.FirstAsync();
|
||||
|
||||
return new News2ReplayResult(score.TotalScore, alert.AlertType);
|
||||
}
|
||||
|
||||
private static async Task SeedSofaBaselineInputsAsync(
|
||||
ApiFixture fixture, Guid encounterId, Guid patientId)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var detector = scope.ServiceProvider.GetRequiredService<SofaDetector>();
|
||||
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "PAO2_MMHG", 100m, DateTimeOffset.UtcNow);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "FIO2_PCT", 40m, DateTimeOffset.UtcNow);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "PLATELET_K_UL", 180m, DateTimeOffset.UtcNow);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "BILIRUBIN_MG_DL", 1.0m, DateTimeOffset.UtcNow);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "SYSTOLIC_BP", 120m, DateTimeOffset.UtcNow);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "DIASTOLIC_BP", 80m, DateTimeOffset.UtcNow);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "CREATININE_MG_DL", 1.0m, DateTimeOffset.UtcNow);
|
||||
await detector.ProcessObservationAsync(encounterId, patientId, "URINE_OUTPUT_ML_H", 50m, DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
private static async Task ClearNews2KeysAsync(ApiFixture fixture, Guid encounterId)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var cache = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>().GetDatabase();
|
||||
foreach (var key in News2Calculator.AllParameterKeys(encounterId))
|
||||
await cache.KeyDeleteAsync(key);
|
||||
}
|
||||
|
||||
private static async Task ClearGcsKeysAsync(ApiFixture fixture, Guid encounterId)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var cache = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>().GetDatabase();
|
||||
foreach (var key in GcsCalculator.AllComponentKeys(encounterId))
|
||||
await cache.KeyDeleteAsync(key);
|
||||
}
|
||||
|
||||
private static async Task ClearTrendKeysAsync(ApiFixture fixture, Guid encounterId)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var cache = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>().GetDatabase();
|
||||
foreach (var key in TrendCalculator.AllHistoryKeys(encounterId))
|
||||
await cache.KeyDeleteAsync(key);
|
||||
}
|
||||
|
||||
public record News2ReplayResult(int TotalScore, AlertType AlertType);
|
||||
}
|
||||
Reference in New Issue
Block a user