Compare commits
13 Commits
main
...
rushali_do
| Author | SHA1 | Date | |
|---|---|---|---|
| 0324d207cc | |||
| 888eaa3dac | |||
| 9925126701 | |||
| da525bc8d3 | |||
| d549583de0 | |||
| 2806df61f1 | |||
| c928f1493f | |||
| 8fc19f1868 | |||
| 502918bbe2 | |||
| b352665fda | |||
| 457b801fb1 | |||
| e3ed1134ca | |||
| 7ab8bff37b |
BIN
.Net Capstone Project/ClinicalInsightsPro-fixed.zip
Normal file
BIN
.Net Capstone Project/ClinicalInsightsPro-fixed.zip
Normal file
Binary file not shown.
BIN
.Net Capstone Project/ClinicalInsightsPro-fixedv1.zip
Normal file
BIN
.Net Capstone Project/ClinicalInsightsPro-fixedv1.zip
Normal file
Binary file not shown.
BIN
.Net Capstone Project/ClinicalInsightsPro-fixedv2.zip
Normal file
BIN
.Net Capstone Project/ClinicalInsightsPro-fixedv2.zip
Normal file
Binary file not shown.
BIN
.Net Capstone Project/ClinicalInsightsPro.APINEWPROJECT.zip
Normal file
BIN
.Net Capstone Project/ClinicalInsightsPro.APINEWPROJECT.zip
Normal file
Binary file not shown.
BIN
.Net Capstone Project/ClinicalInsightsPro.zip
Normal file
BIN
.Net Capstone Project/ClinicalInsightsPro.zip
Normal file
Binary file not shown.
BIN
.Net Capstone Project/ClinicalInsightsProNew.zip
Normal file
BIN
.Net Capstone Project/ClinicalInsightsProNew.zip
Normal file
Binary file not shown.
665
.Net Capstone Project/LiveMedplumFhirService.cs
Normal file
665
.Net Capstone Project/LiveMedplumFhirService.cs
Normal file
@ -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<LiveMedplumFhirService> _logger;
|
||||
private readonly string _envFilePath;
|
||||
|
||||
private string? _cachedToken;
|
||||
private DateTime _tokenExpiresAt = DateTime.MinValue;
|
||||
|
||||
public LiveMedplumFhirService(HttpClient http, IConfiguration config, IValidationService validation,
|
||||
ILogger<LiveMedplumFhirService> 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<string, string>
|
||||
{
|
||||
["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<JsonElement>(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<List<PatientSummaryDto>> GetPatientListAsync()
|
||||
{
|
||||
await EnsureAuthorizedAsync();
|
||||
var baseUrl = _config["Fhir:MedplumBaseUrl"]!.TrimEnd('/');
|
||||
|
||||
var bundle = await GetBundleAsync($"{baseUrl}/Patient?_count=50");
|
||||
var results = new List<PatientSummaryDto>();
|
||||
|
||||
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<PatientChartDto> 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<JsonElement>(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<string> { "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<List<T>> FetchResourceListAsync<T>(string url, Func<JsonElement, string, T> mapper, string patientId)
|
||||
{
|
||||
var bundle = await GetBundleAsync(url);
|
||||
return bundle.EnumerateEntries().Select(e => mapper(e, patientId)).ToList();
|
||||
}
|
||||
|
||||
private async Task<FhirBundle> GetBundleAsync(string url)
|
||||
{
|
||||
var response = await _http.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
using var stream = await response.Content.ReadAsStreamAsync();
|
||||
var json = await JsonSerializer.DeserializeAsync<JsonElement>(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<JsonElement> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
.Net Capstone Project/V1files.zip
Normal file
BIN
.Net Capstone Project/V1files.zip
Normal file
Binary file not shown.
BIN
.Net Capstone Project/clinicalinsightpro31.sql
Normal file
BIN
.Net Capstone Project/clinicalinsightpro31.sql
Normal file
Binary file not shown.
BIN
.Net Capstone Project/clinicalinsightsprodb31082026.sql
Normal file
BIN
.Net Capstone Project/clinicalinsightsprodb31082026.sql
Normal file
Binary file not shown.
BIN
Demo/CDS-Enterprise-Appp (2).zip
Normal file
BIN
Demo/CDS-Enterprise-Appp (2).zip
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user