From 457b801fb1f36561d2a735faafbd5abcfaa55b81 Mon Sep 17 00:00:00 2001 From: "himanshu.agrawal" Date: Sat, 29 Aug 2026 20:38:01 +0000 Subject: [PATCH] New upload New update --- .../LiveMedplumFhirService.cs | 665 ++++++++++++++++++ 1 file changed, 665 insertions(+) create mode 100644 .Net Capstone Project/LiveMedplumFhirService.cs diff --git a/.Net Capstone Project/LiveMedplumFhirService.cs b/.Net Capstone Project/LiveMedplumFhirService.cs new file mode 100644 index 0000000..e73e371 --- /dev/null +++ b/.Net Capstone Project/LiveMedplumFhirService.cs @@ -0,0 +1,665 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using ClinicalInsightsPro.API.DTOs; +using ClinicalInsightsPro.API.Models; +using ClinicalInsightsPro.API.Services.Interfaces; + +namespace ClinicalInsightsPro.API.Services; + +// Talks to a real Medplum FHIR R4 server over HTTP, using the OAuth2 client +// credentials grant (Fhir:MedplumClientId / MedplumClientSecret, both loaded +// from .env -- see Program.cs) against Fhir:MedplumTokenUrl, then +// reading FHIR resources at Fhir:MedplumBaseUrl. Activated when +// Fhir:UseLiveMedplum = true (MEDPLUM_USE_LIVE in .env); otherwise the app +// falls back to MedplumFhirService, which reads the local PostgreSQL store +// so the app still runs without live Medplum credentials. +// +// This service is READ-ONLY and never touches ApplicationDbContext -- the +// entities built below (Condition, Observation, AllergyIntolerance, +// MedicationRequest, Encounter, Patient) are transient objects used only to +// (a) run them through IValidationService, the same way the local store's +// rows are validated, and (b) shape the PatientChartDto response. So the +// [Required]/FK attributes on those model classes (e.g. Condition.RecorderId) +// are never enforced here -- nothing is ever SaveChanges'd. +public class LiveMedplumFhirService : IMedplumFhirService +{ + private readonly HttpClient _http; + private readonly IConfiguration _config; + private readonly IValidationService _validation; + private readonly ILogger _logger; + private readonly string _envFilePath; + + private string? _cachedToken; + private DateTime _tokenExpiresAt = DateTime.MinValue; + + public LiveMedplumFhirService(HttpClient http, IConfiguration config, IValidationService validation, + ILogger logger, IHostEnvironment hostEnvironment) + { + _http = http; + _config = config; + _validation = validation; + _logger = logger; + _envFilePath = Path.Combine(hostEnvironment.ContentRootPath, ".env"); + + // Reuse a still-valid token already written to .env (e.g. by a + // previous startup within the token's lifetime) instead of hitting + // Medplum again immediately. + var (savedToken, savedExpiry) = EnvTokenStore.TryLoadMedplumToken(_envFilePath); + if (!string.IsNullOrWhiteSpace(savedToken) && savedExpiry.HasValue && savedExpiry.Value > DateTime.UtcNow) + { + _cachedToken = savedToken; + _tokenExpiresAt = savedExpiry.Value; + } + } + + // Called once at application startup (see Program.cs) so a fresh Medplum + // access token is already generated and saved to .env by the time + // Swagger/the API is reachable -- no manual "call the token endpoint + // first" step required. + public async Task InitializeTokenAsync() => await EnsureAuthorizedAsync(); + + private async Task EnsureAuthorizedAsync() + { + if (_cachedToken != null && DateTime.UtcNow < _tokenExpiresAt) + { + _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _cachedToken); + return; + } + + var tokenUrl = _config["Fhir:MedplumTokenUrl"]; + var clientId = _config["Fhir:MedplumClientId"]; + var clientSecret = _config["Fhir:MedplumClientSecret"]; + + if (string.IsNullOrWhiteSpace(tokenUrl) || string.IsNullOrWhiteSpace(clientId)) + { + throw new InvalidOperationException( + "Live Medplum mode is enabled (MEDPLUM_USE_LIVE=true in .env) but MEDPLUM_TOKEN_URL / " + + "MEDPLUM_CLIENT_ID are not set. Set these in .env to connect to Medplum."); + } + + var form = new Dictionary + { + ["grant_type"] = "client_credentials", + ["client_id"] = clientId, + ["client_secret"] = clientSecret ?? string.Empty + }; + + using var response = await _http.PostAsync(tokenUrl, new FormUrlEncodedContent(form)); + response.EnsureSuccessStatusCode(); + + using var stream = await response.Content.ReadAsStreamAsync(); + var payload = await JsonSerializer.DeserializeAsync(stream); + + _cachedToken = payload.GetProperty("access_token").GetString(); + var expiresIn = payload.TryGetProperty("expires_in", out var exp) ? exp.GetInt32() : 300; + _tokenExpiresAt = DateTime.UtcNow.AddSeconds(Math.Max(30, expiresIn - 30)); + + _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _cachedToken); + + // Persist so the token survives across scoped-service instances + // within this run and is visible in the .env file, per project setup. + EnvTokenStore.SaveMedplumToken(_envFilePath, _cachedToken!, _tokenExpiresAt); + + _logger.LogInformation("Obtained Medplum client-credentials access token (expires in {ExpiresIn}s) and saved it to {EnvFile}.", expiresIn, _envFilePath); + } + + public async Task> GetPatientListAsync() + { + await EnsureAuthorizedAsync(); + var baseUrl = _config["Fhir:MedplumBaseUrl"]!.TrimEnd('/'); + + var bundle = await GetBundleAsync($"{baseUrl}/Patient?_count=50"); + var results = new List(); + + foreach (var entry in bundle.EnumerateEntries()) + { + var patient = MapPatient(entry); + results.Add(new PatientSummaryDto + { + Id = patient.Id, + FullName = $"{patient.GivenName} {patient.FamilyName}", + Age = CalculateAge(patient.BirthDate), + Gender = patient.Gender, + MedicalRecordNumber = patient.MedicalRecordNumber + }); + } + + return results; + } + + public async Task GetPatientChartAsync(string patientId) + { + await EnsureAuthorizedAsync(); + var baseUrl = _config["Fhir:MedplumBaseUrl"]!.TrimEnd('/'); + + var patientResponse = await _http.GetAsync($"{baseUrl}/Patient/{patientId}"); + if (patientResponse.StatusCode == System.Net.HttpStatusCode.NotFound) + { + throw new KeyNotFoundException($"Patient '{patientId}' was not found in Medplum."); + } + patientResponse.EnsureSuccessStatusCode(); + using var patientStream = await patientResponse.Content.ReadAsStreamAsync(); + var patientJson = await JsonSerializer.DeserializeAsync(patientStream); + var patient = MapPatient(patientJson); + + var conditions = await FetchResourceListAsync($"{baseUrl}/Condition?patient={patientId}", MapCondition, patientId); + var observations = await FetchResourceListAsync($"{baseUrl}/Observation?patient={patientId}", MapObservation, patientId); + var allergies = await FetchResourceListAsync($"{baseUrl}/AllergyIntolerance?patient={patientId}", MapAllergy, patientId); + var medications = await FetchResourceListAsync($"{baseUrl}/MedicationRequest?patient={patientId}", MapMedication, patientId); + var encounters = await FetchResourceListAsync($"{baseUrl}/Encounter?patient={patientId}", MapEncounter, patientId); + + var notes = new List { "FHIR bundle retrieved live from Medplum and validated against R4 structure definitions." }; + foreach (var c in conditions) notes.AddRange(_validation.ValidateAndNormalizeCondition(c)); + foreach (var o in observations) notes.AddRange(_validation.ValidateAndNormalizeObservation(o)); + foreach (var a in allergies) notes.AddRange(_validation.ValidateAndNormalizeAllergy(a)); + foreach (var m in medications) notes.AddRange(_validation.ValidateAndNormalizeMedication(m)); + foreach (var e in encounters) notes.AddRange(_validation.ValidateAndNormalizeEncounter(e)); + + if (!allergies.Any()) + { + notes.Add("No AllergyIntolerance resources found -- treated as 'no known allergies' (NKA)."); + } + + return new PatientChartDto + { + PatientId = patient.Id, + + FullName = $"{patient.GivenName} {patient.FamilyName}", + + Age = CalculateAge(patient.BirthDate), + + MedicalRecordNumber = patient.MedicalRecordNumber, + + IdentifierSystem = patient.IdentifierSystem, + IdentifierValue = patient.IdentifierValue, + + NameUse = patient.NameUse, + Prefix = patient.Prefix, + + GivenName = patient.GivenName, + FamilyName = patient.FamilyName, + + TelecomSystem = patient.TelecomSystem, + TelecomUse = patient.TelecomUse, + TelecomValue = patient.TelecomValue, + + Gender = patient.Gender, + + BloodGroup = patient.BloodGroup, + Email = patient.Email, + + BirthDate = patient.BirthDate, + + AddressUse = patient.AddressUse, + AddressType = patient.AddressType, + AddressLine = patient.AddressLine, + + City = patient.City, + State = patient.State, + PostalCode = patient.PostalCode, + + MaritalStatusSystem = patient.MaritalStatusSystem, + MaritalStatusCode = patient.MaritalStatusCode, + MaritalStatusDisplay = patient.MaritalStatusDisplay, + + ContactNameUse = patient.ContactNameUse, + ContactPrefix = patient.ContactPrefix, + + ContactTelecomSystem = patient.ContactTelecomSystem, + ContactTelecomUse = patient.ContactTelecomUse, + ContactTelecomValue = patient.ContactTelecomValue, + + ContactAddressUse = patient.ContactAddressUse, + ContactAddressType = patient.ContactAddressType, + ContactAddressLine = patient.ContactAddressLine, + + ContactCity = patient.ContactCity, + ContactState = patient.ContactState, + ContactPostalCode = patient.ContactPostalCode, + + ContactGender = patient.ContactGender, + ContactOrganizationId = patient.ContactOrganizationId, + + ContactPeriodStart = patient.ContactPeriodStart, + ContactPeriodEnd = patient.ContactPeriodEnd, + + ManagingOrganizationId = patient.ManagingOrganizationId, + + PhotoContentType = patient.PhotoContentType, + PhotoUrl = patient.PhotoUrl, + PhotoTitle = patient.PhotoTitle, + + GeneralPractitionerId = patient.GeneralPractitionerId, + + Conditions = conditions.Select(c => new ChartConditionDto + { + Id = c.Id, + Code = c.ConditionCode, + Display = c.ConditionDisplay, + ClinicalStatus = c.ClinicalStatusDisplay, + VerificationStatus = c.VerificationStatusDisplay, + Severity = c.SeverityDisplay, + OnsetDate = c.OnsetDateTime, + AbatementDate = c.AbatementDateTime, + RecordedDate = c.RecordedDate + }).ToList(), + + Allergies = allergies.Select(a => new ChartAllergyDto + { + Id = a.Id, + Type = a.Type, + ClinicalStatus = a.ClinicalStatus, + VerificationStatus = a.VerificationStatus, + Criticality = a.Criticality, + RecorderPractitionerId = a.RecorderPractitionerId + }).ToList(), + + Medications = medications.Select(m => new ChartMedicationDto + { + Id = m.Id, + MedicationCode = m.MedicationCode, + MedicationDisplay = m.MedicationDisplay, + Status = m.Status, + Intent = m.Intent, + Priority = m.Priority + }).ToList(), + + Observations = observations.Select(o => new ChartObservationDto + { + Id = o.Id, + Code = o.ObservationCode, + Display = o.ObservationDisplay, + Status = o.Status, + CategoryDisplay = o.CategoryDisplay, + InterpretationCode = o.InterpretationCode, + InterpretationDisplay = o.InterpretationDisplay, + EffectiveDate = o.EffectiveDateTime, + NoteText = o.NoteText + }).ToList(), + + Encounters = encounters.Select(e => new ChartEncounterDto + { + Id = e.Id, + Status = e.Status, + ClassCode = e.ClassCode, + ClassDisplay = e.ClassDisplay, + TypeDisplay = e.TypeDisplay, + ServiceTypeDisplay = e.ServiceTypeDisplay, + PeriodStart = e.PeriodStart, + PeriodEnd = e.PeriodEnd + }).ToList(), + + ValidationNotes = notes + }; + } + + private async Task> FetchResourceListAsync(string url, Func mapper, string patientId) + { + var bundle = await GetBundleAsync(url); + return bundle.EnumerateEntries().Select(e => mapper(e, patientId)).ToList(); + } + + private async Task GetBundleAsync(string url) + { + var response = await _http.GetAsync(url); + response.EnsureSuccessStatusCode(); + using var stream = await response.Content.ReadAsStreamAsync(); + var json = await JsonSerializer.DeserializeAsync(stream); + return new FhirBundle(json); + } + + // ---- FHIR JSON -> domain model mapping ---- + // Every mapper below targets the CURRENT Models/*.cs shape (the one + // ApplicationDbContext/MedplumFhirService use), not the old + // FirstName/LastName/Code/Display-style entities. These objects are only + // ever read from, never saved -- see the class-level comment. + + private static Patient MapPatient(JsonElement res) + { + var name = FirstOrDefault(res, "name"); + var given = name.ValueKind == JsonValueKind.Object ? GetFirstArrayString(name, "given") : string.Empty; + var family = name.ValueKind == JsonValueKind.Object ? GetString(name, "family") : string.Empty; + var nameUse = name.ValueKind == JsonValueKind.Object ? GetString(name, "use") : null; + var prefix = name.ValueKind == JsonValueKind.Object ? GetFirstArrayString(name, "prefix") : null; + + var identifier = FirstOrDefault(res, "identifier"); + var identifierSystem = identifier.ValueKind == JsonValueKind.Object ? GetString(identifier, "system") : null; + var identifierValue = identifier.ValueKind == JsonValueKind.Object ? GetString(identifier, "value") : null; + + var telecom = FirstOfSystem(res, "telecom", "phone"); + var email = FirstOfSystem(res, "telecom", "email"); + + var address = FirstOrDefault(res, "address"); + var addressLine = address.ValueKind == JsonValueKind.Object ? GetFirstArrayString(address, "line") : null; + + var maritalStatus = res.TryGetProperty("maritalStatus", out var ms) ? ms : default; + var managingOrgId = GetReferenceId(res, "managingOrganization"); + var generalPractitionerRef = FirstOrDefault(res, "generalPractitioner"); + var generalPractitionerId = generalPractitionerRef.ValueKind == JsonValueKind.Object + ? ExtractReferenceId(GetString(generalPractitionerRef, "reference")) + : null; + + var photo = FirstOrDefault(res, "photo"); + + return new Patient + { + Id = GetString(res, "id") ?? string.Empty, + PatientId = GetString(res, "id") ?? string.Empty, + ResourceType = "Patient", + + MedicalRecordNumber = identifierValue ?? string.Empty, + IdentifierSystem = identifierSystem ?? string.Empty, + IdentifierValue = identifierValue ?? string.Empty, + + NameUse = nameUse ?? "official", + Prefix = prefix, + GivenName = given ?? string.Empty, + FamilyName = family ?? string.Empty, + + TelecomSystem = telecom.system ?? "phone", + TelecomUse = telecom.use ?? "home", + TelecomValue = telecom.value ?? string.Empty, + + Gender = GetString(res, "gender") ?? "unknown", + + Email = email.value, + + BirthDate = GetString(res, "birthDate") is string bd && DateTime.TryParse(bd, out var d) ? d : DateTime.UtcNow, + + AddressUse = address.ValueKind == JsonValueKind.Object ? GetString(address, "use") ?? "home" : "home", + AddressType = address.ValueKind == JsonValueKind.Object ? GetString(address, "type") ?? "both" : "both", + AddressLine = addressLine, + City = address.ValueKind == JsonValueKind.Object ? GetString(address, "city") ?? string.Empty : string.Empty, + State = address.ValueKind == JsonValueKind.Object ? GetString(address, "state") ?? string.Empty : string.Empty, + PostalCode = address.ValueKind == JsonValueKind.Object ? GetString(address, "postalCode") ?? string.Empty : string.Empty, + + MaritalStatusSystem = maritalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(maritalStatus, "system") : null, + MaritalStatusCode = maritalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(maritalStatus, "code") : null, + MaritalStatusDisplay = maritalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(maritalStatus, "display") : null, + + ManagingOrganizationId = managingOrgId, + GeneralPractitionerId = generalPractitionerId, + + PhotoContentType = photo.ValueKind == JsonValueKind.Object ? GetString(photo, "contentType") : null, + PhotoUrl = photo.ValueKind == JsonValueKind.Object ? GetString(photo, "url") : null, + PhotoTitle = photo.ValueKind == JsonValueKind.Object ? GetString(photo, "title") : null + }; + } + + private static Condition MapCondition(JsonElement res, string patientId) + { + var clinicalStatus = res.TryGetProperty("clinicalStatus", out var cs) ? cs : default; + var verificationStatus = res.TryGetProperty("verificationStatus", out var vs) ? vs : default; + var category = FirstOrDefault(res, "category"); + var severity = res.TryGetProperty("severity", out var sev) ? sev : default; + + return new Condition + { + Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), + ConditionId = GetString(res, "id") ?? Guid.NewGuid().ToString(), + ResourceType = "Condition", + + ClinicalStatusCode = (clinicalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(clinicalStatus, "code") : null) ?? string.Empty, + ClinicalStatusDisplay = (clinicalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(clinicalStatus, "display") : null) ?? string.Empty, + + VerificationStatusCode = (verificationStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(verificationStatus, "code") : null) ?? string.Empty, + VerificationStatusDisplay = (verificationStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(verificationStatus, "display") : null) ?? string.Empty, + + CategorySystem = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "system") : null, + CategoryCode = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "code") : null, + CategoryDisplay = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "display") : null, + + SeverityCode = (severity.ValueKind == JsonValueKind.Object ? GetFirstCodingField(severity, "code") : null) ?? string.Empty, + SeverityDisplay = (severity.ValueKind == JsonValueKind.Object ? GetFirstCodingField(severity, "display") : null) ?? string.Empty, + + ConditionCode = GetCodeableConceptCode(res, "code"), + ConditionDisplay = GetCodeableConceptDisplay(res, "code"), + + PatientId = patientId, + + OnsetDateTime = GetString(res, "onsetDateTime") is string od && DateTime.TryParse(od, out var onset) ? onset : DateTime.UtcNow, + AbatementDateTime = GetString(res, "abatementDateTime") is string ad && DateTime.TryParse(ad, out var abate) ? abate : (DateTime?)null, + RecordedDate = GetString(res, "recordedDate") is string rd && DateTime.TryParse(rd, out var recorded) ? recorded : DateTime.UtcNow, + + RecorderId = ExtractReferenceId(GetReferenceString(res, "recorder")) ?? string.Empty, + AsserterId = ExtractReferenceId(GetReferenceString(res, "asserter")) ?? string.Empty + }; + } + + private static Observation MapObservation(JsonElement res, string patientId) + { + var category = FirstOrDefault(res, "category"); + var interpretation = FirstOrDefault(res, "interpretation"); + var note = FirstOrDefault(res, "note"); + + return new Observation + { + Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), + Status = GetString(res, "status") ?? "final", + + CategoryCode = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "code") : null, + CategoryDisplay = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "display") : null, + + ObservationCode = GetCodeableConceptCode(res, "code"), + ObservationDisplay = GetCodeableConceptDisplay(res, "code"), + + PatientId = patientId, + + PerformerPractitionerId = ExtractReferenceId(GetFirstReferenceString(res, "performer")), + + EffectiveDateTime = GetString(res, "effectiveDateTime") is string ed && DateTimeOffset.TryParse(ed, out var eff) ? eff : (DateTimeOffset?)null, + Issued = GetString(res, "issued") is string iss && DateTimeOffset.TryParse(iss, out var issued) ? issued : (DateTimeOffset?)null, + + NoteText = note.ValueKind == JsonValueKind.Object ? GetString(note, "text") : null, + NoteAuthorPractitionerId = note.ValueKind == JsonValueKind.Object ? ExtractReferenceId(GetString(note, "authorReference")) : null, + NoteTime = note.ValueKind == JsonValueKind.Object && GetString(note, "time") is string nt && DateTimeOffset.TryParse(nt, out var noteTime) ? noteTime : (DateTimeOffset?)null, + + InterpretationCode = interpretation.ValueKind == JsonValueKind.Object ? GetFirstCodingField(interpretation, "code") : null, + InterpretationDisplay = interpretation.ValueKind == JsonValueKind.Object ? GetFirstCodingField(interpretation, "display") : null + }; + } + + private static AllergyIntolerance MapAllergy(JsonElement res, string patientId) + { + var clinicalStatus = res.TryGetProperty("clinicalStatus", out var cs) ? cs : default; + var verificationStatus = res.TryGetProperty("verificationStatus", out var vs) ? vs : default; + + return new AllergyIntolerance + { + Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), + ClinicalStatus = (clinicalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(clinicalStatus, "code") : null) ?? "active", + VerificationStatus = (verificationStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(verificationStatus, "code") : null) ?? "unconfirmed", + Type = GetString(res, "type") ?? "allergy", + Criticality = GetString(res, "criticality"), + + PatientId = patientId, + + AsserterPatientId = ExtractReferenceId(GetReferenceString(res, "asserter")), + RecorderPractitionerId = ExtractReferenceId(GetReferenceString(res, "recorder")) + }; + } + + private static MedicationRequest MapMedication(JsonElement res, string patientId) => new() + { + Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), + Status = GetString(res, "status") ?? "active", + Intent = GetString(res, "intent") ?? "order", + Priority = GetString(res, "priority"), + + PatientId = patientId, + + MedicationCode = GetCodeableConceptCode(res, "medicationCodeableConcept"), + MedicationDisplay = GetCodeableConceptDisplay(res, "medicationCodeableConcept") + }; + + private static Encounter MapEncounter(JsonElement res, string patientId) + { + var classField = res.TryGetProperty("class", out var cls) ? cls : default; + var type = FirstOrDefault(res, "type"); + var serviceType = res.TryGetProperty("serviceType", out var st) ? st : default; + var priority = res.TryGetProperty("priority", out var pr) ? pr : default; + var period = res.TryGetProperty("period", out var p) ? p : default; + + return new Encounter + { + Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), + EncounterId = GetString(res, "id") ?? Guid.NewGuid().ToString(), + ResourceType = "Encounter", + Status = GetString(res, "status") ?? "finished", + + ClassSystem = (classField.ValueKind == JsonValueKind.Object ? GetString(classField, "system") : null) ?? string.Empty, + ClassCode = (classField.ValueKind == JsonValueKind.Object ? GetString(classField, "code") : null) ?? string.Empty, + ClassDisplay = (classField.ValueKind == JsonValueKind.Object ? GetString(classField, "display") : null) ?? string.Empty, + + TypeSystem = type.ValueKind == JsonValueKind.Object ? GetFirstCodingField(type, "system") : null, + TypeCode = type.ValueKind == JsonValueKind.Object ? GetFirstCodingField(type, "code") : null, + TypeDisplay = type.ValueKind == JsonValueKind.Object ? GetFirstCodingField(type, "display") : null, + + ServiceTypeSystem = serviceType.ValueKind == JsonValueKind.Object ? GetFirstCodingField(serviceType, "system") : null, + ServiceTypeCode = serviceType.ValueKind == JsonValueKind.Object ? GetFirstCodingField(serviceType, "code") : null, + ServiceTypeDisplay = serviceType.ValueKind == JsonValueKind.Object ? GetFirstCodingField(serviceType, "display") : null, + + PrioritySystem = priority.ValueKind == JsonValueKind.Object ? GetFirstCodingField(priority, "system") : null, + PriorityCode = priority.ValueKind == JsonValueKind.Object ? GetFirstCodingField(priority, "code") : null, + PriorityDisplay = priority.ValueKind == JsonValueKind.Object ? GetFirstCodingField(priority, "display") : null, + + PatientId = patientId, + + PeriodStart = period.ValueKind == JsonValueKind.Object && GetString(period, "start") is string ps && DateTime.TryParse(ps, out var start) ? start : DateTime.UtcNow, + PeriodEnd = period.ValueKind == JsonValueKind.Object && GetString(period, "end") is string pe && DateTime.TryParse(pe, out var end) ? end : DateTime.UtcNow + }; + } + + // ---- small JSON helpers ---- + + private static string? GetString(JsonElement el, string propertyName) => + el.ValueKind == JsonValueKind.Object && el.TryGetProperty(propertyName, out var v) && v.ValueKind == JsonValueKind.String + ? v.GetString() + : null; + + private static JsonElement FirstOrDefault(JsonElement el, string arrayPropertyName) => + el.ValueKind == JsonValueKind.Object && el.TryGetProperty(arrayPropertyName, out var arr) && + arr.ValueKind == JsonValueKind.Array && arr.GetArrayLength() > 0 + ? arr[0] + : default; + + private static string? GetFirstArrayString(JsonElement el, string arrayPropertyName) + { + if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(arrayPropertyName, out var arr) || + arr.ValueKind != JsonValueKind.Array || arr.GetArrayLength() == 0) + { + return null; + } + return arr[0].ValueKind == JsonValueKind.String ? arr[0].GetString() : null; + } + + // Finds the first entry of a telecom-style array whose "system" matches, + // e.g. FirstOfSystem(patient, "telecom", "phone"). + private static (string? system, string? use, string? value) FirstOfSystem(JsonElement el, string arrayPropertyName, string system) + { + if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(arrayPropertyName, out var arr) || arr.ValueKind != JsonValueKind.Array) + { + return (null, null, null); + } + + foreach (var item in arr.EnumerateArray()) + { + if (GetString(item, "system") == system) + { + return (GetString(item, "system"), GetString(item, "use"), GetString(item, "value")); + } + } + + return (null, null, null); + } + + // Reads system/code/display off the first entry of a CodeableConcept's + // "coding" array (or a "system"/"code"/"display" set directly on the + // element itself, for the maritalStatus / clinicalStatus shape). + private static string? GetFirstCodingField(JsonElement codeableConcept, string field) + { + if (codeableConcept.ValueKind != JsonValueKind.Object) return null; + + if (codeableConcept.TryGetProperty("coding", out var coding) && + coding.ValueKind == JsonValueKind.Array && coding.GetArrayLength() > 0) + { + return GetString(coding[0], field); + } + + return GetString(codeableConcept, field); + } + + private static string GetCodeableConceptCode(JsonElement res, string propertyName) + { + if (!res.TryGetProperty(propertyName, out var cc)) return string.Empty; + if (cc.ValueKind == JsonValueKind.String) return cc.GetString() ?? string.Empty; + if (cc.TryGetProperty("coding", out var coding) && coding.GetArrayLength() > 0 && coding[0].TryGetProperty("code", out var code)) + return code.GetString() ?? string.Empty; + return string.Empty; + } + + private static string GetCodeableConceptDisplay(JsonElement res, string propertyName) + { + if (!res.TryGetProperty(propertyName, out var cc)) return string.Empty; + return GetCodeableConceptDisplay(cc); + } + + private static string GetCodeableConceptDisplay(JsonElement cc) + { + if (cc.TryGetProperty("text", out var text)) return text.GetString() ?? string.Empty; + if (cc.TryGetProperty("coding", out var coding) && coding.GetArrayLength() > 0 && coding[0].TryGetProperty("display", out var display)) + return display.GetString() ?? string.Empty; + return string.Empty; + } + + // reference-style: { "reference": "Practitioner/123" } + private static string? GetReferenceString(JsonElement res, string propertyName) => + res.ValueKind == JsonValueKind.Object && res.TryGetProperty(propertyName, out var refEl) && refEl.ValueKind == JsonValueKind.Object + ? GetString(refEl, "reference") + : null; + + // array-of-reference-style: "performer": [ { "reference": "Practitioner/123" } ] + private static string? GetFirstReferenceString(JsonElement res, string arrayPropertyName) + { + var first = FirstOrDefault(res, arrayPropertyName); + return first.ValueKind == JsonValueKind.Object ? GetString(first, "reference") : null; + } + + // "Practitioner/123" -> "123" + private static string? ExtractReferenceId(string? reference) + { + if (string.IsNullOrWhiteSpace(reference)) return null; + var slashIndex = reference.LastIndexOf('/'); + return slashIndex >= 0 && slashIndex < reference.Length - 1 ? reference[(slashIndex + 1)..] : reference; + } + + private static string? GetReferenceId(JsonElement res, string propertyName) => + ExtractReferenceId(GetReferenceString(res, propertyName)); + + private static int CalculateAge(DateTime dob) + { + var today = DateTime.UtcNow; + var age = today.Year - dob.Year; + if (dob.Date > today.AddYears(-age)) age--; + return age; + } + + // Small helper over a FHIR searchset Bundle to enumerate entry.resource elements. + private readonly struct FhirBundle + { + private readonly JsonElement _json; + public FhirBundle(JsonElement json) { _json = json; } + + public IEnumerable EnumerateEntries() + { + if (!_json.TryGetProperty("entry", out var entries) || entries.ValueKind != JsonValueKind.Array) + yield break; + + foreach (var entry in entries.EnumerateArray()) + { + if (entry.TryGetProperty("resource", out var resource)) + yield return resource; + } + } + } +}