222 lines
9.4 KiB
C#
222 lines
9.4 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StackExchange.Redis;
|
|
|
|
[Collection("Integration")]
|
|
public class ClinicalDemographicsAndObservationTests : IAsyncLifetime
|
|
{
|
|
private readonly ApiFixture _fixture;
|
|
private readonly HttpClient _client;
|
|
private Guid _patientId;
|
|
private Guid _encounterId;
|
|
|
|
public ClinicalDemographicsAndObservationTests(ApiFixture fixture)
|
|
{
|
|
_fixture = fixture;
|
|
_client = fixture.CreateClient();
|
|
}
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
await DbResetHelper.ResetAsync(db);
|
|
|
|
var patient = new Patient
|
|
{
|
|
Id = Guid.NewGuid(), Mrn = "MRN-P10-001", FirstName = "Phase10", LastName = "Test",
|
|
DateOfBirth = new DateOnly(1965, 3, 15), Gender = "F",
|
|
BloodType = BloodType.BPositive, Allergies = "Sulfa",
|
|
EmergencyContactName = "Test Contact", EmergencyContactPhone = "555-9999",
|
|
CreatedAt = DateTimeOffset.UtcNow
|
|
};
|
|
var encounter = new Encounter
|
|
{
|
|
Id = Guid.NewGuid(), PatientId = patient.Id, EncounterType = EncounterType.Inpatient,
|
|
Status = EncounterStatus.Active, Department = Department.Icu,
|
|
AttendingPhysician = "Dr. Phase10",
|
|
RoomBed = "ICU-7A", AdmissionReason = "Sepsis workup",
|
|
AdmittedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow
|
|
};
|
|
db.Patients.Add(patient);
|
|
db.Encounters.Add(encounter);
|
|
|
|
db.AlertThresholds.AddRange(
|
|
new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "HEART_RATE",
|
|
DisplayName = "Heart Rate", Unit = "bpm",
|
|
CriticalLow = 30, WarningLow = 50, WarningHigh = 100, CriticalHigh = 150,
|
|
CreatedAt = DateTimeOffset.UtcNow },
|
|
new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "SYSTOLIC_BP",
|
|
DisplayName = "Systolic BP", Unit = "mmHg",
|
|
CriticalLow = 70, WarningLow = 90, WarningHigh = 160, CriticalHigh = 180,
|
|
CreatedAt = DateTimeOffset.UtcNow },
|
|
new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "DIASTOLIC_BP",
|
|
DisplayName = "Diastolic BP", Unit = "mmHg",
|
|
CriticalLow = 40, WarningLow = 60, WarningHigh = 90, CriticalHigh = 110,
|
|
CreatedAt = DateTimeOffset.UtcNow },
|
|
new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "LACTATE_MMOL_L",
|
|
DisplayName = "Serum Lactate", Unit = "mmol/L",
|
|
CriticalLow = null, WarningLow = null, WarningHigh = 2.0m, CriticalHigh = 4.0m,
|
|
CreatedAt = DateTimeOffset.UtcNow },
|
|
new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "AVPU",
|
|
DisplayName = "AVPU Consciousness", Unit = "score",
|
|
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = 2m,
|
|
CreatedAt = DateTimeOffset.UtcNow },
|
|
new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "SUPPLEMENTAL_O2",
|
|
DisplayName = "Supplemental O2", Unit = "flag",
|
|
CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null,
|
|
CreatedAt = DateTimeOffset.UtcNow },
|
|
new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "GLUCOSE_MG_DL",
|
|
DisplayName = "Blood Glucose", Unit = "mg/dL",
|
|
CriticalLow = 40, WarningLow = 70, WarningHigh = 180, CriticalHigh = 400,
|
|
CreatedAt = DateTimeOffset.UtcNow }
|
|
);
|
|
await db.SaveChangesAsync();
|
|
|
|
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
|
|
var cache = redis.GetDatabase(1);
|
|
foreach (var t in await db.AlertThresholds.ToListAsync())
|
|
{
|
|
var json = JsonSerializer.Serialize(new
|
|
{
|
|
t.ObservationCode, t.CriticalLow, t.WarningLow, t.WarningHigh, t.CriticalHigh
|
|
});
|
|
await cache.StringSetAsync($"threshold:{t.ObservationCode}", json);
|
|
}
|
|
|
|
_patientId = patient.Id;
|
|
_encounterId = encounter.Id;
|
|
}
|
|
|
|
public Task DisposeAsync() => Task.CompletedTask;
|
|
|
|
[Fact]
|
|
public async Task CriticalSystolicBp_AlertCreated()
|
|
{
|
|
var resp = await _client.PostAsJsonAsync(
|
|
$"/api/v1/encounters/{_encounterId}/observations",
|
|
new BatchIngestRequest(new List<IngestObservationRequest>
|
|
{
|
|
new("SYSTOLIC_BP", 65, "mmHg", ObservationSource.Device, DateTimeOffset.UtcNow, null)
|
|
}));
|
|
|
|
resp.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
var body = await resp.Content.ReadFromJsonAsync<JsonDocument>();
|
|
body!.RootElement.GetProperty("data").GetProperty("alertGenerated").GetBoolean()
|
|
.Should().BeTrue();
|
|
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var alert = await db.ClinicalAlerts.SingleAsync();
|
|
alert.AlertType.Should().Be(AlertType.CriticalSystolicBp);
|
|
alert.Severity.Should().Be(AlertSeverity.Critical);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AvpuUnresponsive_CriticalAlert()
|
|
{
|
|
var resp = await _client.PostAsJsonAsync(
|
|
$"/api/v1/encounters/{_encounterId}/observations",
|
|
new BatchIngestRequest(new List<IngestObservationRequest>
|
|
{
|
|
new("AVPU", 3, "score", ObservationSource.Manual, DateTimeOffset.UtcNow, null)
|
|
}));
|
|
|
|
resp.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var alert = await db.ClinicalAlerts.SingleAsync();
|
|
alert.AlertType.Should().Be(AlertType.CriticalAvpu);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SupplementalO2_NoAlert()
|
|
{
|
|
var resp = await _client.PostAsJsonAsync(
|
|
$"/api/v1/encounters/{_encounterId}/observations",
|
|
new BatchIngestRequest(new List<IngestObservationRequest>
|
|
{
|
|
new("SUPPLEMENTAL_O2", 1, "flag", ObservationSource.Manual, DateTimeOffset.UtcNow, null)
|
|
}));
|
|
|
|
resp.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
(await db.ClinicalAlerts.CountAsync()).Should().Be(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CriticalGlucose_AlertCreated()
|
|
{
|
|
var resp = await _client.PostAsJsonAsync(
|
|
$"/api/v1/encounters/{_encounterId}/observations",
|
|
new BatchIngestRequest(new List<IngestObservationRequest>
|
|
{
|
|
new("GLUCOSE_MG_DL", 30, "mg/dL", ObservationSource.Lab, DateTimeOffset.UtcNow, null)
|
|
}));
|
|
|
|
resp.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
|
|
using var scope = _fixture.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var alert = await db.ClinicalAlerts.SingleAsync();
|
|
alert.AlertType.Should().Be(AlertType.CriticalGlucoseMgDl);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PatientRegistration_ClinicalFields_RoundTrip()
|
|
{
|
|
var resp = await _client.PostAsJsonAsync("/api/v1/patients", new
|
|
{
|
|
firstName = "Demo",
|
|
lastName = "Patient",
|
|
dateOfBirth = "1990-06-15",
|
|
gender = "M",
|
|
bloodType = "AB-",
|
|
allergies = "Latex, Iodine",
|
|
emergencyContactName = "Demo Contact",
|
|
emergencyContactPhone = "555-1234"
|
|
});
|
|
|
|
resp.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
var body = await resp.Content.ReadFromJsonAsync<JsonDocument>();
|
|
var data = body!.RootElement.GetProperty("data");
|
|
data.GetProperty("bloodType").GetString().Should().Be("AB-");
|
|
data.GetProperty("allergies").GetString().Should().Be("Latex, Iodine");
|
|
data.GetProperty("emergencyContactName").GetString().Should().Be("Demo Contact");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenEncounter_WithRoomBed_Success()
|
|
{
|
|
var patientResp = await _client.PostAsJsonAsync("/api/v1/patients", new
|
|
{
|
|
firstName = "Room", lastName = "Test",
|
|
dateOfBirth = "1985-01-01", gender = "F"
|
|
});
|
|
var patientBody = await patientResp.Content.ReadFromJsonAsync<JsonDocument>();
|
|
var newPatientId = patientBody!.RootElement.GetProperty("data").GetProperty("id").GetGuid();
|
|
|
|
var resp = await _client.PostAsJsonAsync($"/api/v1/patients/{newPatientId}/encounters", new
|
|
{
|
|
encounterType = "INPATIENT",
|
|
department = "ICU",
|
|
attendingPhysician = "Dr. Room",
|
|
roomBed = "ICU-3C",
|
|
admissionReason = "Acute respiratory distress"
|
|
});
|
|
|
|
resp.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
var body = await resp.Content.ReadFromJsonAsync<JsonDocument>();
|
|
var data = body!.RootElement.GetProperty("data");
|
|
data.GetProperty("roomBed").GetString().Should().Be("ICU-3C");
|
|
data.GetProperty("admissionReason").GetString().Should().Be("Acute respiratory distress");
|
|
}
|
|
}
|