Skip to content

Commit

Permalink
Map single entry symptom responses to FHIR
Browse files Browse the repository at this point in the history
  • Loading branch information
emyl3 committed Oct 10, 2024
1 parent 0fc7b5b commit 5f7623b
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,51 @@ public static String parseState(String s) {
throw IllegalGraphqlArgumentException.invalidInput(s, "state");
}

private static final Map<String, String> RESPIRATORY_SYMPTOMS =
Map.ofEntries(
Map.entry("426000000", "Fever over 100.4F"),
Map.entry("103001002", "Feeling feverish"),
Map.entry("43724002", "Chills"),
Map.entry("49727002", "Cough"),
Map.entry("267036007", "Shortness of breath"),
Map.entry("230145002", "Difficulty breathing"),
Map.entry("84229001", "Fatigue"),
Map.entry("68962001", "Muscle or body aches"),
Map.entry("25064002", "Headache"),
Map.entry("36955009", "New loss of taste"),
Map.entry("44169009", "New loss of smell"),
Map.entry("162397003", "Sore throat"),
Map.entry("68235000", "Nasal congestion"),
Map.entry("64531003", "Runny nose"),
Map.entry("422587007", "Nausea"),
Map.entry("422400008", "Vomiting"),
Map.entry("62315008", "Diarrhea"),
Map.entry("261665006", "Other symptom not listed"));

private static final Map<String, String> SYPHILIS_SYMPTOMS =
Map.ofEntries(
Map.entry("724386005", "Genital sore/lesion"),
Map.entry("195469007", "Anal sore/lesion"),
Map.entry("26284000", "Sore(s) in mouth/lips"),
Map.entry("266128007", "Body Rash"),
Map.entry("56940005", "Palmar (hand)/plantar (foot) rash"),
Map.entry("91554004", "Flat white warts"),
Map.entry("15188001", "Hearing loss"),
Map.entry("246636008", "Blurred vision"),
Map.entry("56317004", "Alopecia"));

private static Map<String, String> getSupportedSymptoms() {
Map<String, String> supportedSymptoms = new HashMap<>();
supportedSymptoms.putAll(RESPIRATORY_SYMPTOMS);
supportedSymptoms.putAll(SYPHILIS_SYMPTOMS);
return supportedSymptoms;
}

public static String getSymptomName(String snomedCode) {
Map<String, String> supportedSymptoms = getSupportedSymptoms();
return supportedSymptoms.get(snomedCode);
}

public static Map<String, Boolean> parseSymptoms(String symptoms) {
if (symptoms == null) {
return Collections.emptyMap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ private FhirConstants() {

public static final String LOINC_AOE_IDENTIFIER = "81959-9";
public static final String LOINC_AOE_SYMPTOMATIC = "95419-8";
public static final String LOINC_SYMPTOM_TIMING_PANEL = "99582-9";
public static final String LOINC_SYMPTOM = "75325-1";
public static final String LOINC_AOE_SYMPTOM_ONSET = "11368-8";
public static final String LOINC_AOE_PREGNANCY_STATUS = "82810-3";
public static final String LOINC_AOE_EMPLOYED_IN_HEALTHCARE = "95418-0";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import static gov.cdc.usds.simplereport.api.Translators.REFUSED;
import static gov.cdc.usds.simplereport.api.Translators.TRANS_MAN;
import static gov.cdc.usds.simplereport.api.Translators.TRANS_WOMAN;
import static gov.cdc.usds.simplereport.api.Translators.getSymptomName;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.ABNORMAL_FLAGS_CODE_SYSTEM;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.ABNORMAL_FLAG_ABNORMAL;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.ABNORMAL_FLAG_NORMAL;
Expand All @@ -34,6 +35,8 @@
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.LOINC_AOE_SYMPTOM_ONSET;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.LOINC_CODE_SYSTEM;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.LOINC_GENDER_IDENTITY;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.LOINC_SYMPTOM;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.LOINC_SYMPTOM_TIMING_PANEL;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.NOTE_TYPE_CODING_SYSTEM;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.NOTE_TYPE_CODING_SYSTEM_CODE;
import static gov.cdc.usds.simplereport.api.converter.FhirConstants.NOTE_TYPE_CODING_SYSTEM_CODE_INDEX_EXTENSION_URL;
Expand Down Expand Up @@ -114,12 +117,15 @@
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -802,7 +808,7 @@ private void addCorrectionNote(
}
}

public Set<Observation> convertToAOESymptomObservation(
public Set<Observation> convertToAOESymptomaticObservation(
String eventId, Boolean symptomatic, LocalDate symptomOnsetDate) {
var observations = new LinkedHashSet<Observation>();
var symptomaticCode =
Expand All @@ -827,6 +833,33 @@ public Set<Observation> convertToAOESymptomObservation(
return observations;
}

public Observation createSymptomObservation(CodeableConcept code, Type value) {
Observation observation =
new Observation().setStatus(ObservationStatus.FINAL).setCode(code).setValue(value);
observation.setId(uuidGenerator.randomUUID().toString());
observation
.addIdentifier()
.setUse(IdentifierUse.OFFICIAL)
.setType(createLoincConcept(LOINC_SYMPTOM_TIMING_PANEL, "Symptom and timing panel", null));
return observation;
}

public Set<Observation> convertToSymptomsObservations(List<String> symptoms) {
HashSet<Observation> observations = new HashSet<>();

CodeableConcept symptomStatusCode = createLoincConcept(LOINC_SYMPTOM, "Symptom", "Symptom");

symptoms.forEach(
symptom -> {
String symptomName = getSymptomName(symptom);
if (symptomName != null && !symptomName.isBlank()) {
CodeableConcept symptomValueCode = createSNOMEDConcept(symptom, symptomName, null);
observations.add(createSymptomObservation(symptomStatusCode, symptomValueCode));
}
});
return observations;
}

public Set<Observation> convertToAOEGenderOfSexualPartnersObservation(
Set<String> sexualPartners) {
HashSet<Observation> observations = new LinkedHashSet<>();
Expand Down Expand Up @@ -972,10 +1005,12 @@ public Set<Observation> convertToAOEObservations(
} else if (surveyData.getSymptoms() != null
&& surveyData.getSymptoms().containsValue(Boolean.TRUE)) {
symptomatic = true;
List<String> positiveSymptoms = getPositiveSymptoms(surveyData.getSymptoms());
observations.addAll(convertToSymptomsObservations(positiveSymptoms));
} // implied else: AoE form was not completed. Symptomatic set to null

var symptomOnsetDate = surveyData.getSymptomOnsetDate();
observations.addAll(convertToAOESymptomObservation(eventId, symptomatic, symptomOnsetDate));
observations.addAll(convertToAOESymptomaticObservation(eventId, symptomatic, symptomOnsetDate));

String pregnancyStatus = surveyData.getPregnancy();
if (pregnancyStatus != null && pregnancyStatusSnomedMap.values().contains(pregnancyStatus)) {
Expand Down Expand Up @@ -1616,6 +1651,13 @@ private Patient assignPotentiallyAbsentName(
return patient;
}

private List<String> getPositiveSymptoms(Map<String, Boolean> symptomsMap) {
return symptomsMap.entrySet().stream()
.filter(symptom -> Boolean.TRUE.equals(symptom.getValue()))
.map(Entry::getKey)
.collect(Collectors.toList());
}

public Bundle createFhirBundle(ConditionAgnosticCreateFhirBundleProps props) {

var patientFullUrl = ResourceType.Patient + "/" + props.getPatient().getId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,8 @@ private Bundle convertRowToFhirBundle(TestResultRow row, UUID orgId) {
}

aoeObservations.addAll(
fhirConverter.convertToAOESymptomObservation(testEventId, symptomatic, symptomOnsetDate));
fhirConverter.convertToAOESymptomaticObservation(
testEventId, symptomatic, symptomOnsetDate));

String pregnancyValue = row.getPregnant().getValue();
if (StringUtils.isNotBlank(pregnancyValue)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ void convertToAoeObservation_noSymptoms_matchesJson() throws IOException {
AskOnEntrySurvey.builder()
.pregnancy(null)
.syphilisHistory(null)
.symptoms(Map.of("fake", false))
.symptoms(Map.of("195469007", true))
.noSymptoms(true)
.symptomOnsetDate(null)
.genderOfSexualPartners(null)
Expand Down Expand Up @@ -1270,14 +1270,43 @@ void convertToAoeObservation_syphilisHistory_matchesJson() throws IOException {
JSONAssert.assertEquals(expectedSerialized, actualSerialized, true);
}

@Test
void convertToAoeObservation_symptoms_matchesJson() throws IOException {
AskOnEntrySurvey answers =
AskOnEntrySurvey.builder()
.pregnancy(null)
.syphilisHistory(null)
.symptoms(
Map.of("36955009", true, "261665006", false, "68962001", true, "195469007", true))
.genderOfSexualPartners(null)
.symptomOnsetDate(null)
.noSymptoms(false)
.build();

String testId = "fakeId";

Set<Observation> actual =
fhirConverter.convertToAOEObservations(
testId, answers, new Person("first", "last", "middle", "suffix", null));

String actualSerialized =
actual.stream().map(parser::encodeResourceToString).collect(Collectors.toSet()).toString();
String expectedSerialized =
IOUtils.toString(
Objects.requireNonNull(
getClass().getClassLoader().getResourceAsStream("fhir/observationSymptom.json")),
StandardCharsets.UTF_8);
JSONAssert.assertEquals(expectedSerialized, actualSerialized, true);
}

@Test
void convertToAoeObservation_allAOE_matchesJson() throws IOException {
List<String> sexualPartners = List.of("male", "female");
AskOnEntrySurvey answers =
AskOnEntrySurvey.builder()
.pregnancy(PREGNANT_UNKNOWN_SNOMED)
.syphilisHistory(NO_SYPHILIS_HISTORY_SNOMED)
.symptoms(Map.of("fake", true))
.symptoms(Map.of("36955009", true))
.symptomOnsetDate(LocalDate.of(2023, 3, 4))
.genderOfSexualPartners(sexualPartners)
.noSymptoms(false)
Expand Down
49 changes: 48 additions & 1 deletion backend/src/test/resources/fhir/bundle-integration-testing.json
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,9 @@
{
"reference": "Observation/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
{
"reference": "Observation/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
{
"reference": "Observation/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
Expand Down Expand Up @@ -889,6 +892,50 @@
]
}
}
},
{
"fullUrl": "Observation/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"resource": {
"resourceType": "Observation",
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "99582-9",
"display": "Symptom and timing panel"
}
]
}
}
],
"status": "final",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "75325-1",
"display": "Symptom"
}
],
"text": "Symptom"
},
"subject": {
"reference": "Patient/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"valueCodeableConcept": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "84229001",
"display": "Fatigue"
}
]
}
}
}
]
}
}
38 changes: 38 additions & 0 deletions backend/src/test/resources/fhir/observationAllAoe.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,42 @@
[
{
"resourceType": "Observation",
"id": "5db534ea-5e97-4861-ba18-d74acc46db15",
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "99582-9",
"display": "Symptom and timing panel"
}
]
}
}
],
"status": "final",
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "75325-1",
"display": "Symptom"
}
],
"text": "Symptom"
},
"valueCodeableConcept": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "36955009",
"display": "New loss of taste"
}
]
}
},
{
"resourceType": "Observation",
"id": "8526bd3b-42e4-3a6f-88ff-3fc43b69dc20",
Expand Down
Loading

0 comments on commit 5f7623b

Please sign in to comment.