Skip to content

Commit

Permalink
Map syphilis history to FHIR (#8151)
Browse files Browse the repository at this point in the history
  • Loading branch information
emyl3 authored Oct 3, 2024
1 parent 923e291 commit 3c98da4
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
import static gov.cdc.usds.simplereport.api.model.TestEventExport.DEFAULT_LOCATION_NAME;
import static gov.cdc.usds.simplereport.api.model.TestEventExport.FALLBACK_DEFAULT_TEST_MINUTES;
import static gov.cdc.usds.simplereport.api.model.TestEventExport.UNKNOWN_ADDRESS_INDICATOR;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.NO_SYPHILIS_HISTORY_SNOMED;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.YES_SYPHILIS_HISTORY_SNOMED;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.genderIdentityDisplaySet;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.genderIdentitySnomedSet;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.getResidenceTypeMap;
Expand Down Expand Up @@ -865,6 +867,20 @@ public Observation convertToAOEPregnancyObservation(String pregnancyStatusSnomed
pregnancyStatusValueCode);
}

public Observation convertToAOESyphilisHistoryObservation(String syphilisHistory) {
CodeableConcept observationCode =
createSNOMEDConcept(
YES_SYPHILIS_HISTORY_SNOMED, "History of syphilis", "History of syphilis");
Boolean hasHistory = null;
if (syphilisHistory.equalsIgnoreCase(YES_SYPHILIS_HISTORY_SNOMED)) {
hasHistory = true;
} else if (syphilisHistory.equalsIgnoreCase(NO_SYPHILIS_HISTORY_SNOMED)) {
hasHistory = false;
}
return createAOEObservation(
uuidGenerator.randomUUID().toString(), observationCode, createYesNoUnkConcept(hasHistory));
}

public Observation convertToAOEYesNoUnkObservation(
Boolean isObserved, String observationLoinc, String observationDisplayText) {
CodeableConcept observationCode =
Expand Down Expand Up @@ -991,6 +1007,10 @@ public Set<Observation> convertToAOEObservations(
observations.addAll(convertToAOEGenderOfSexualPartnersObservation(sexualPartners));
}

if (surveyData.getSyphilisHistory() != null) {
observations.add(convertToAOESyphilisHistoryObservation(surveyData.getSyphilisHistory()));
}

return observations;
}

Expand Down Expand Up @@ -1050,7 +1070,17 @@ private CodeableConcept createYesNoUnkConcept(Boolean val) {
}

private CodeableConcept createSNOMEDConcept(String resultCode, String resultDisplay) {
return createSNOMEDConcept(resultCode, resultDisplay, null);
}

private CodeableConcept createSNOMEDConcept(
String resultCode, String resultDisplay, String resultText) {
CodeableConcept concept = new CodeableConcept();

if (StringUtils.isNotBlank(resultText)) {
concept.setText(resultText);
}

Coding coding = concept.addCoding();
coding.setSystem(SNOMED_CODE_SYSTEM);
coding.setCode(resultCode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ public List<FeedbackMessage> validateIndividualValues() {
testResult, DiseaseService.HIV_NAME, List.of(gendersOfSexualPartners, pregnant)));
}

errors.addAll(validateYesNoAnswer(syphilisHistory));
if (isSyphilisResult()) {
errors.addAll(
validateRequiredFieldsForPositiveResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -855,4 +855,13 @@ public static Map<String, String> getGenderIdentityAbbreviationMap() {

return genderMap;
}

public static final Map<String, String> syphilisHistorySnomedMap =
Map.of(
"YES".toLowerCase(), YES_SYPHILIS_HISTORY_SNOMED,
"Y".toLowerCase(), YES_SYPHILIS_HISTORY_SNOMED,
"NO".toLowerCase(), NO_SYPHILIS_HISTORY_SNOMED,
"N".toLowerCase(), NO_SYPHILIS_HISTORY_SNOMED,
"UNK".toLowerCase(), UNKNOWN_SYPHILIS_HISTORY_SNOMED,
"U".toLowerCase(), UNKNOWN_SYPHILIS_HISTORY_SNOMED);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static gov.cdc.usds.simplereport.api.model.filerow.TestResultRow.diseaseSpecificLoincMap;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.getGenderIdentityAbbreviationMap;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.getResidenceTypeMap;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.syphilisHistorySnomedMap;
import static gov.cdc.usds.simplereport.utils.DateTimeUtils.DATE_TIME_FORMATTER;
import static gov.cdc.usds.simplereport.utils.DateTimeUtils.convertToZonedDateTime;
import static gov.cdc.usds.simplereport.utils.ResultUtils.mapTestResultStatusToSRValue;
Expand Down Expand Up @@ -475,6 +476,13 @@ private Bundle convertRowToFhirBundle(TestResultRow row, UUID orgId) {
AOE_EMPLOYED_IN_HEALTHCARE_DISPLAY));
}

String syphilisHistory = row.getSyphilisHistory().getValue();
if (StringUtils.isNotBlank(syphilisHistory)) {
String syphilisHistorySnomed = getSyphilisHistorySnomed(syphilisHistory);
aoeObservations.add(
fhirConverter.convertToAOESyphilisHistoryObservation(syphilisHistorySnomed));
}

String hospitalizedValue = row.getHospitalized().getValue();
if (StringUtils.isNotBlank(hospitalizedValue)) {
Boolean hospitalized = yesNoToBooleanMap.get(hospitalizedValue.toLowerCase());
Expand Down Expand Up @@ -601,6 +609,13 @@ private String getPregnancyStatusSnomed(String input) {
return null;
}

private String getSyphilisHistorySnomed(String input) {
if (input != null && input.matches(ALPHABET_REGEX)) {
return syphilisHistorySnomedMap.get(input.toLowerCase());
}
return null;
}

private String getResidenceTypeSnomed(String input) {
if (input != null && input.matches(ALPHABET_REGEX)) {
return getResidenceTypeMap().get(input.toLowerCase());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import static gov.cdc.usds.simplereport.api.model.TestEventExport.DEFAULT_LOCATION_CODE;
import static gov.cdc.usds.simplereport.api.model.TestEventExport.DEFAULT_LOCATION_NAME;
import static gov.cdc.usds.simplereport.api.model.TestEventExport.UNKNOWN_ADDRESS_INDICATOR;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.NO_SYPHILIS_HISTORY_SNOMED;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.PREGNANT_UNKNOWN_SNOMED;
import static gov.cdc.usds.simplereport.db.model.PersonUtils.YES_SYPHILIS_HISTORY_SNOMED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.from;
import static org.junit.jupiter.params.provider.Arguments.arguments;
Expand Down Expand Up @@ -1238,22 +1240,53 @@ void convertToAoeObservation_genderOfSexualPartners_matchesJson() throws IOExcep
JSONAssert.assertEquals(expectedSerialized, actualSerialized, true);
}

@Test
void convertToAoeObservation_syphilisHistory_matchesJson() throws IOException {
AskOnEntrySurvey answers =
AskOnEntrySurvey.builder()
.pregnancy(null)
.syphilisHistory(YES_SYPHILIS_HISTORY_SNOMED)
.symptoms(null)
.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/observationSyphilisHistory.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(null)
.syphilisHistory(NO_SYPHILIS_HISTORY_SNOMED)
.symptoms(Map.of("fake", true))
.symptomOnsetDate(LocalDate.of(2023, 3, 4))
.genderOfSexualPartners(null)
.genderOfSexualPartners(sexualPartners)
.noSymptoms(false)
.build();

String testId = "fakeId";

var birthDate = LocalDate.of(2022, 12, 13);
var person =
LocalDate birthDate = LocalDate.of(2022, 12, 13);
Person person =
new Person(
null,
null,
Expand All @@ -1277,11 +1310,11 @@ void convertToAoeObservation_allAOE_matchesJson() throws IOException {
"English",
null);

var actual = fhirConverter.convertToAOEObservations(testId, answers, person);
Set<Observation> actual = fhirConverter.convertToAOEObservations(testId, answers, person);

String actualSerialized =
actual.stream().map(parser::encodeResourceToString).collect(Collectors.toSet()).toString();
var expectedSerialized =
String expectedSerialized =
IOUtils.toString(
Objects.requireNonNull(
getClass().getClassLoader().getResourceAsStream("fhir/observationAllAoe.json")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,22 @@ public void beforeEach() {
when(resultsUploaderCachingService.getSpecimenTypeNameToSNOMEDMap())
.thenReturn(
Map.of(
"Nasal swab".toLowerCase(), "445297001",
"Anterior nares swab".toLowerCase(), "697989009"));
"Nasal swab".toLowerCase(),
"445297001",
"Anterior nares swab".toLowerCase(),
"697989009",
"Venous blood specimen".toLowerCase(),
"122555007"));

when(resultsUploaderCachingService.getSNOMEDToSpecimenTypeNameMap())
.thenReturn(
Map.of(
"445297001", "Nasal swab",
"697989009", "Anterior nares swab"));
"445297001",
"Nasal swab",
"697989009",
"Anterior nares swab",
"122555007",
"Venous blood specimen"));

when(resultsUploaderCachingService.getZoneIdByAddress(any()))
.thenReturn(ZoneId.of("US/Central"));
Expand Down Expand Up @@ -650,4 +658,29 @@ void convertExistingCsv_validHivPositive_withAOEdataColumns() {
assertThat(gendersOfSexualPartnersObservations).hasSize(5);
assertThat(codeableConceptValues).containsExactlyInAnyOrderElementsOf(expectedGenders);
}

@Test
void convertExistingCsv_validSyphilisPositive_withAOEdataColumns() {
InputStream input = loadCsv("testResultUpload/test-results-upload-valid-syphilis-only.csv");

FHIRBundleRecord bundleRecord = sut.convertToFhirBundles(input, UUID.randomUUID());
List<String> serializedBundles = bundleRecord.serializedBundle();
String first = serializedBundles.get(0);
Bundle deserializedBundle = (Bundle) parser.parseResource(first);

List<Observation> syphilisHistoryObservations =
deserializedBundle.getEntry().stream()
.filter(entry -> entry.getFullUrl().contains("Observation/"))
.map(x -> (Observation) x.getResource())
.filter(x -> Objects.equals(x.getCode().getText(), "History of syphilis"))
.collect(Collectors.toList());

List<String> codeableConceptValues =
syphilisHistoryObservations.stream()
.map(x -> x.getValueCodeableConcept().getCoding().get(0).getDisplay())
.collect(Collectors.toList());

assertThat(syphilisHistoryObservations).hasSize(1);
assertThat(codeableConceptValues).isEqualTo(List.of("unknown"));
}
}
114 changes: 114 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": "8526bd3b-42e4-3a6f-88ff-3fc43b69dc20",
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "81959-9",
"display": "Public health laboratory ask at order entry panel"
}
]
}
}
],
"status": "final",
"code": {
"coding": [
{
"system": "http://simplereport.gov/",
"code": "SR0001",
"display": "What is the gender of their sexual partners"
}
],
"text": "What is the gender of their sexual partners"
},
"valueCodeableConcept": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "446151000124109",
"display": "Male gender identity"
}
]
}
},
{
"resourceType": "Observation",
"id": "5f3026ea-845b-3649-8b62-d3dbfd3e7b62",
Expand Down Expand Up @@ -37,6 +75,82 @@
]
}
},
{
"resourceType": "Observation",
"id": "8526bd3b-42e4-3a6f-88ff-3fc43b69dc20",
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "81959-9",
"display": "Public health laboratory ask at order entry panel"
}
]
}
}
],
"status": "final",
"code": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "1087151000119108",
"display": "History of syphilis"
}
],
"text": "History of syphilis"
},
"valueCodeableConcept": {
"coding": [
{
"system": "http://terminology.hl7.org/ValueSet/v2-0136",
"code": "N",
"display": "No"
}
]
}
},
{
"resourceType": "Observation",
"id": "8526bd3b-42e4-3a6f-88ff-3fc43b69dc20",
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://loinc.org",
"code": "81959-9",
"display": "Public health laboratory ask at order entry panel"
}
]
}
}
],
"status": "final",
"code": {
"coding": [
{
"system": "http://simplereport.gov/",
"code": "SR0001",
"display": "What is the gender of their sexual partners"
}
],
"text": "What is the gender of their sexual partners"
},
"valueCodeableConcept": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "446141000124107",
"display": "Female gender identity"
}
]
}
},
{
"resourceType": "Observation",
"id": "07099f95-8754-3317-8e87-e1c4717c1af1",
Expand Down
Loading

0 comments on commit 3c98da4

Please sign in to comment.