155 lines
5.8 KiB
C#
155 lines
5.8 KiB
C#
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
public static class ScenarioReplayHelper
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
};
|
|
|
|
public static string ScenariosDirectory =>
|
|
Path.GetFullPath(Path.Combine(
|
|
AppContext.BaseDirectory,
|
|
"..", "..", "..", "..",
|
|
"VigilCare.Simulator", "Scenarios", "List"));
|
|
|
|
public static ScenarioFile Load(string fileName)
|
|
{
|
|
var path = Path.Combine(ScenariosDirectory, fileName);
|
|
var json = File.ReadAllText(path);
|
|
return JsonSerializer.Deserialize<ScenarioFile>(json, JsonOptions)
|
|
?? throw new InvalidOperationException($"Failed to deserialize {path}");
|
|
}
|
|
|
|
public static async Task<(Guid PatientId, Guid EncounterId)> ReplayObservationsAsync(
|
|
HttpClient client,
|
|
ScenarioFile scenario,
|
|
CancellationToken ct = default)
|
|
{
|
|
var patientResp = await client.PostAsJsonAsync("/api/v1/patients", new
|
|
{
|
|
firstName = scenario.Patient.FirstName,
|
|
lastName = scenario.Patient.LastName,
|
|
dateOfBirth = scenario.Patient.DateOfBirth,
|
|
gender = scenario.Patient.Gender,
|
|
}, ct);
|
|
patientResp.EnsureSuccessStatusCode();
|
|
var patient = (await patientResp.Content.ReadFromJsonAsync<ApiEnvelope<PatientDto>>(ct))!.Data;
|
|
|
|
var encounterResp = await client.PostAsJsonAsync(
|
|
$"/api/v1/patients/{patient.Id}/encounters",
|
|
new
|
|
{
|
|
encounterType = scenario.Encounter.EncounterType,
|
|
department = scenario.Encounter.Department,
|
|
attendingPhysician = scenario.Encounter.AttendingPhysician,
|
|
roomBed = scenario.Encounter.RoomBed,
|
|
admissionReason = scenario.Encounter.AdmissionReason,
|
|
}, ct);
|
|
encounterResp.EnsureSuccessStatusCode();
|
|
var encounter = (await encounterResp.Content.ReadFromJsonAsync<ApiEnvelope<EncounterDto>>(ct))!.Data;
|
|
|
|
var start = DateTimeOffset.UtcNow;
|
|
foreach (var cluster in scenario.Events
|
|
.Where(e => e.Type == "observation")
|
|
.GroupBy(e => e.OffsetMinutes)
|
|
.OrderBy(g => g.Key))
|
|
{
|
|
var recordedAt = start.AddMinutes(cluster.Key);
|
|
var batch = cluster.Select(evt =>
|
|
{
|
|
var code = evt.Data.GetProperty("code").GetString()!;
|
|
var value = evt.Data.GetProperty("value").GetDecimal();
|
|
var unit = evt.Data.GetProperty("unit").GetString()!;
|
|
var source = evt.Data.TryGetProperty("source", out var s) ? s.GetString()! : "Manual";
|
|
return new
|
|
{
|
|
observationCode = code,
|
|
value,
|
|
unit,
|
|
source = MapSource(source),
|
|
recordedAt,
|
|
};
|
|
}).ToList();
|
|
|
|
foreach (var chunk in batch.Chunk(10))
|
|
{
|
|
var resp = await client.PostAsJsonAsync(
|
|
$"/api/v1/encounters/{encounter.Id}/observations",
|
|
new { observations = chunk }, ct);
|
|
resp.EnsureSuccessStatusCode();
|
|
}
|
|
}
|
|
|
|
return (patient.Id, encounter.Id);
|
|
}
|
|
|
|
public static async Task<SofaScore> WaitForSofaScoreAsync(
|
|
IServiceProvider services,
|
|
Guid encounterId,
|
|
TimeSpan timeout)
|
|
{
|
|
var deadline = DateTime.UtcNow + timeout;
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
using var scope = services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var sofa = await db.SofaScores
|
|
.Where(s => s.EncounterId == encounterId)
|
|
.OrderByDescending(s => s.CalculatedAt)
|
|
.FirstOrDefaultAsync();
|
|
if (sofa is not null)
|
|
return sofa;
|
|
await Task.Delay(500);
|
|
}
|
|
throw new TimeoutException($"Timed out waiting for SOFA score on encounter {encounterId}");
|
|
}
|
|
|
|
public static async Task WaitForAlertTypeAsync(
|
|
IServiceProvider services,
|
|
Guid encounterId,
|
|
AlertType alertType,
|
|
TimeSpan timeout)
|
|
{
|
|
var deadline = DateTime.UtcNow + timeout;
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
using var scope = services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
if (await db.ClinicalAlerts.AnyAsync(a =>
|
|
a.EncounterId == encounterId && a.AlertType == alertType))
|
|
return;
|
|
await Task.Delay(500);
|
|
}
|
|
throw new TimeoutException($"Timed out waiting for {alertType} on encounter {encounterId}");
|
|
}
|
|
|
|
public static async Task AssertNoAlertTypeAsync(
|
|
IServiceProvider services,
|
|
Guid encounterId,
|
|
AlertType alertType,
|
|
TimeSpan settleDelay)
|
|
{
|
|
await Task.Delay(settleDelay);
|
|
using var scope = services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var found = await db.ClinicalAlerts.AnyAsync(a =>
|
|
a.EncounterId == encounterId && a.AlertType == alertType);
|
|
if (found)
|
|
throw new InvalidOperationException($"Unexpected {alertType} alert on encounter {encounterId}");
|
|
}
|
|
|
|
private static string MapSource(string source) => source.ToLowerInvariant() switch
|
|
{
|
|
"device" or "monitor" => "Device",
|
|
"lab" => "Lab",
|
|
_ => "Manual",
|
|
};
|
|
|
|
private record ApiEnvelope<T>(T Data);
|
|
private record PatientDto(Guid Id);
|
|
private record EncounterDto(Guid Id);
|
|
} |