using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; public class VigilCareApiClient { private static readonly JsonSerializerOptions ApiJsonOptions = new(JsonSerializerDefaults.Web); private readonly HttpClient _http; public VigilCareApiClient(HttpClient http) { _http = http; } public async Task LoginAsync(string username, string password) { var response = await _http.PostAsJsonAsync("/api/v1/auth/login", new SimLoginRequest(username, password)); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(); throw new HttpRequestException( $"Login failed ({(int)response.StatusCode} {response.StatusCode}): {body}"); } var envelope = await response.Content.ReadFromJsonAsync>(); var token = envelope!.Data!.AccessToken; _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } public void SetBearerToken(string token) { _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } public async Task RegisterPatientAsync(RegisterPatientRequest req) { var response = await _http.PostAsJsonAsync("/api/v1/patients", req); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(); throw new HttpRequestException( $"Register patient failed ({(int)response.StatusCode} {response.StatusCode}): {body}"); } var envelope = await response.Content.ReadFromJsonAsync>(); return envelope!.Data!; } public async Task OpenEncounterAsync(Guid patientId, OpenEncounterRequest req) { var response = await _http.PostAsJsonAsync($"/api/v1/patients/{patientId}/encounters", req); response.EnsureSuccessStatusCode(); var envelope = await response.Content.ReadFromJsonAsync>(); return envelope!.Data!; } public async Task SendObservationBatchAsync( Guid encounterId, List observations, ReplayTarget target = ReplayTarget.Central) { if (target == ReplayTarget.Gateway) { foreach (var obs in observations) { var response = await _http.PostAsJsonAsync( $"/api/v1/encounters/{encounterId}/observations", new { observationCode = obs.ObservationCode, value = obs.Value, unit = obs.Unit, source = obs.Source, recordedAt = obs.RecordedAt, idempotencyKey = obs.IdempotencyKey ?? Guid.NewGuid().ToString() }); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(); throw new HttpRequestException( $"Observation ingest failed ({(int)response.StatusCode} {response.StatusCode}): {body}"); } } return; } foreach (var chunk in observations.Chunk(10)) { var batch = new BatchIngestRequest(chunk.ToList()); var response = await _http.PostAsJsonAsync( $"/api/v1/encounters/{encounterId}/observations", batch); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(); throw new HttpRequestException( $"Observation batch failed ({(int)response.StatusCode} {response.StatusCode}): {body}"); } } } public async Task TrySendMedicationAsync( Guid encounterId, CreateMedicationAdministrationRequest req) { var response = await _http.PostAsJsonAsync( $"/api/v1/encounters/{encounterId}/medications", req); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return false; response.EnsureSuccessStatusCode(); return true; } public async Task TryCreateOrderAsync( Guid encounterId, CreateOrderRequest req) { var response = await _http.PostAsJsonAsync( $"/api/v1/encounters/{encounterId}/orders", req); return response.IsSuccessStatusCode; } public async Task TryResultOrderAsync( Guid encounterId, string orderDescription, string? resultSummary) { var ordersResponse = await _http.GetAsync( $"/api/v1/encounters/{encounterId}/orders?status=PENDING"); if (!ordersResponse.IsSuccessStatusCode) return false; var envelope = await ordersResponse.Content .ReadFromJsonAsync>>(); var order = FindPendingOrder(envelope?.Data?.Items ?? [], orderDescription); if (order is null) return false; var resultResponse = await _http.PatchAsJsonAsync( $"/api/v1/orders/{order.Id}/result", new RecordOrderResultRequest(resultSummary)); return resultResponse.IsSuccessStatusCode; } private static OrderResponse? FindPendingOrder( List items, string description) { return items.FirstOrDefault(o => o.Description == description) ?? items.FirstOrDefault(o => description.StartsWith(o.Description, StringComparison.OrdinalIgnoreCase)) ?? items.FirstOrDefault(o => o.Description.StartsWith(description, StringComparison.OrdinalIgnoreCase)); } // --- Polling endpoints --- public async Task> GetAlertsAsync(Guid encounterId) { var response = await _http.GetAsync($"/api/v1/encounters/{encounterId}/alerts"); if (!response.IsSuccessStatusCode) return new(); var envelope = await response.Content .ReadFromJsonAsync>>(ApiJsonOptions); return envelope?.Data?.Items?.ToList() ?? new(); } public async Task GetCurrentNews2Async(Guid encounterId) { var response = await _http.GetAsync($"/api/v1/encounters/{encounterId}/news2/current"); if (!response.IsSuccessStatusCode) return null; var envelope = await response.Content.ReadFromJsonAsync>(); return envelope?.Data; } public async Task GetSepsisBundleAsync(Guid encounterId) { var response = await _http.GetAsync( $"/api/v1/encounters/{encounterId}/sepsis-bundle/current"); if (!response.IsSuccessStatusCode) return null; var envelope = await response.Content .ReadFromJsonAsync>(); return envelope?.Data; } public async Task GetCurrentGcsAsync(Guid encounterId) { var response = await _http.GetAsync($"/api/v1/encounters/{encounterId}/gcs"); if (!response.IsSuccessStatusCode) return null; var envelope = await response.Content.ReadFromJsonAsync>(); return envelope?.Data; } public async Task GetCurrentSofaAsync(Guid encounterId) { var response = await _http.GetAsync($"/api/v1/encounters/{encounterId}/sofa"); if (!response.IsSuccessStatusCode) return null; var envelope = await response.Content.ReadFromJsonAsync>(); return envelope?.Data; } public async Task GetCurrentQsofaAsync(Guid encounterId) { var response = await _http.GetAsync($"/api/v1/encounters/{encounterId}/qsofa/current"); if (!response.IsSuccessStatusCode) return null; var envelope = await response.Content.ReadFromJsonAsync>(); return envelope?.Data; } public async Task TryAcknowledgeAlertAsync( Guid encounterId, string alertType, string clinicianId, string? note, TimeSpan? waitForAlert = null, CancellationToken ct = default) { const int pollIntervalMs = 500; var deadline = waitForAlert.HasValue ? DateTimeOffset.UtcNow.Add(waitForAlert.Value) : (DateTimeOffset?)null; while (true) { ct.ThrowIfCancellationRequested(); var alerts = await GetAlertsAsync(encounterId); var alert = alerts.FirstOrDefault(a => IsMatchingAlertType(a.AlertType, alertType) && IsOpenAlertStatus(a.Status)); if (alert is not null) { var response = await _http.PostAsJsonAsync( $"/api/v1/alerts/{alert.Id}/acknowledge", new { note, clinicianId }, ct); return response.IsSuccessStatusCode; } if (!deadline.HasValue || DateTimeOffset.UtcNow >= deadline.Value) return false; await Task.Delay(pollIntervalMs, ct); } } private static bool IsOpenAlertStatus(string status) => string.Equals(status, "OPEN", StringComparison.OrdinalIgnoreCase); private static bool IsMatchingAlertType(string actual, string expected) { if (string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase)) return true; static string Normalize(string s) => s.Replace("_", "", StringComparison.Ordinal).ToUpperInvariant(); return Normalize(actual) == Normalize(expected); } }