diff --git a/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java b/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java index fffc99b..974ede9 100644 --- a/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java +++ b/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java @@ -19,6 +19,7 @@ import java.security.Principal; import java.util.List; +import java.util.Map; @RestController @RequestMapping(path = "/terminology") @@ -85,4 +86,13 @@ public ResponseEntity getCode( return ResponseEntity.ok() .body(fhirTerminologyService.retrieveCode(code, codeSystem, version, user.getApiKey())); } + + @PostMapping(path = "/codes", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getCodesAndCodeSystems( + @RequestBody() List> codeList, Principal principal) { + final String username = principal.getName(); + UmlsUser user = vsacService.verifyUmlsAccess(username); + return ResponseEntity.ok() + .body(fhirTerminologyService.retrieveCodesAndCodeSystems(codeList, user.getApiKey())); + } } diff --git a/src/main/java/gov/cms/madie/terminology/dto/Code.java b/src/main/java/gov/cms/madie/terminology/dto/Code.java index 17b916c..f681ef8 100644 --- a/src/main/java/gov/cms/madie/terminology/dto/Code.java +++ b/src/main/java/gov/cms/madie/terminology/dto/Code.java @@ -8,7 +8,8 @@ public class Code { private String name; private String display; - private String version; + private String version; // 'fhir' in the code-system-entry.json + private String svsVersion; // 'vsac' in the code-system-entry.json private String codeSystem; private String codeSystemOid; private CodeStatus status; diff --git a/src/main/java/gov/cms/madie/terminology/repositories/CodeSystemRepository.java b/src/main/java/gov/cms/madie/terminology/repositories/CodeSystemRepository.java index 03e3e57..e199378 100644 --- a/src/main/java/gov/cms/madie/terminology/repositories/CodeSystemRepository.java +++ b/src/main/java/gov/cms/madie/terminology/repositories/CodeSystemRepository.java @@ -8,4 +8,6 @@ public interface CodeSystemRepository extends MongoRepository findById(String id); Optional findByNameAndVersion(String name, String version); + + Optional findByOidAndVersion(String oid, String version); } diff --git a/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java b/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java index 362f88c..520e7f3 100644 --- a/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java +++ b/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java @@ -24,9 +24,8 @@ import org.springframework.web.util.UriComponentsBuilder; import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; @Service @Slf4j @@ -278,4 +277,94 @@ private void updateOrInsertAllCodeSystems(List codeSystemList) { } } } + + public List retrieveCodesAndCodeSystems(List> codeList, String apiKey) { + return codeList.stream() + .map( + code -> { + List codeSystemEntries = mappingService.getCodeSystemEntries(); + String codeName = code.get("code"); + String codeSystemName = code.get("codeSystem"); + String oid = code.get("oid") != null ? code.get("oid").replaceAll("'|'", "") : null; + + Optional> mappedVersion = + mapToFhirVersion(code.get("version"), oid, codeSystemEntries); + + if (mappedVersion.isPresent()) { + String vsacVersion = mappedVersion.get().getKey(); + String fhirVersion = mappedVersion.get().getValue(); + + return retrieveCodes( + codeName, codeSystemName, vsacVersion, fhirVersion, oid, apiKey); + } + return null; + }) + .collect(Collectors.toList()); + } + + private Optional> mapToFhirVersion( + String version, String oid, List codeSystemEntries) { + if (oid == null) { + return Optional.empty(); + } + + Optional> result; + if (version == null) { + result = + codeSystemEntries.stream() + .filter(codeSystemEntry -> StringUtils.equals(codeSystemEntry.getOid(), oid)) + .findFirst() + .map( + codeSystemVersion -> + Map.entry( + codeSystemVersion.getVersions().get(0).getVsac(), + codeSystemVersion.getVersions().get(0).getFhir())); + } else { + result = + codeSystemEntries.stream() + .filter(codeSystemEntry -> StringUtils.equals(codeSystemEntry.getOid(), oid)) + .flatMap(codeSystemEntry -> codeSystemEntry.getVersions().stream()) + .filter(codeSystemVersion -> StringUtils.equals(codeSystemVersion.getVsac(), version)) + .map( + codeSystemVersion -> + Map.entry(codeSystemVersion.getVsac(), codeSystemVersion.getFhir())) + .findFirst(); + } + + return result; + } + + private Code retrieveCodes( + String codeName, + String codeSystemName, + String vsacVersion, + String fhirVersion, + String oid, + String apiKey) { + if (StringUtils.isEmpty(codeName) + || StringUtils.isEmpty(codeSystemName) + || StringUtils.isEmpty(fhirVersion)) { + return null; + } + CodeSystem codeSystem = codeSystemRepository.findByOidAndVersion(oid, fhirVersion).orElse(null); + if (codeSystem == null) { + return null; + } + String codeJson = fhirTerminologyServiceWebClient.getCodeResource(codeName, codeSystem, apiKey); + + Parameters parameters = fhirContext.newJsonParser().parseResource(Parameters.class, codeJson); + Code code = + Code.builder() + .name(codeName) + .codeSystem(codeSystemName) + .version(fhirVersion) + .svsVersion(vsacVersion) + .display(parameters.getParameter("display").getValue().toString()) + .codeSystemOid(parameters.getParameter("Oid").getValue().toString()) + .build(); + + CodeStatus status = vsacService.getCodeStatus(code, apiKey); + code.setStatus(status); + return code; + } } diff --git a/src/main/java/gov/cms/madie/terminology/service/MappingService.java b/src/main/java/gov/cms/madie/terminology/service/MappingService.java index d87bbb8..3630186 100644 --- a/src/main/java/gov/cms/madie/terminology/service/MappingService.java +++ b/src/main/java/gov/cms/madie/terminology/service/MappingService.java @@ -38,13 +38,15 @@ public List getCodeSystemEntries() { return Collections.emptyList(); } - public CodeSystemEntry getCodeSystemEntry(String codeSystemName) { + public CodeSystemEntry getCodeSystemEntryByOid(String oid) { List codeSystemEntries = getCodeSystemEntries(); if (CollectionUtils.isEmpty(codeSystemEntries)) { return null; } return codeSystemEntries.stream() - .filter(entry -> Objects.equals(entry.getName(), codeSystemName)) + .filter( + entry -> + Objects.equals(entry.getOid(), oid.startsWith("urn:oid:") ? oid : "urn:oid:" + oid)) .findFirst() .orElse(null); } diff --git a/src/main/java/gov/cms/madie/terminology/service/VsacService.java b/src/main/java/gov/cms/madie/terminology/service/VsacService.java index 56389fa..a214d54 100644 --- a/src/main/java/gov/cms/madie/terminology/service/VsacService.java +++ b/src/main/java/gov/cms/madie/terminology/service/VsacService.java @@ -161,12 +161,34 @@ public List validateCodes(List cqlCodes, UmlsUser umlsUser, St } public CodeStatus getCodeStatus(Code code, String apiKey) { - CodeSystemEntry systemEntry = mappingService.getCodeSystemEntry(code.getCodeSystem()); + String svsVersion = getSvsCodeSystemVersion(code); + if (svsVersion == null) { + return CodeStatus.NA; + } + // prepare code path e.g. CODE:/CodeSystem/ActCode/Version/9.0.0/Code/AMB/Info + String codePath = + TerminologyServiceUtil.buildCodePath(code.getCodeSystem(), svsVersion, code.getName()); + VsacCode svsCode = terminologyWebClient.getCode(codePath, apiKey); + if (svsCode.getStatus().equalsIgnoreCase("ok")) { + if ("Yes".equals(svsCode.getData().getResultSet().get(0).getActive())) { + return CodeStatus.ACTIVE; + } else { + return CodeStatus.INACTIVE; + } + } + return CodeStatus.NA; + } + + private String getSvsCodeSystemVersion(Code code) { + if (StringUtils.isNotBlank(code.getSvsVersion())) { + return code.getSvsVersion(); + } + CodeSystemEntry systemEntry = mappingService.getCodeSystemEntryByOid(code.getCodeSystemOid()); // do not call SVS API to get code status if the system is not in SVS API if (systemEntry == null || systemEntry.getOid().contains("NOT.IN.VSAC") || CollectionUtils.isEmpty(systemEntry.getVersions())) { - return CodeStatus.NA; + return null; } // get corresponding SVS version for given FHIR version @@ -176,21 +198,9 @@ public CodeStatus getCodeStatus(Code code, String apiKey) { .findFirst() .orElse(null); if (version == null || version.getVsac() == null) { - return CodeStatus.NA; - } - // prepare code path e.g. CODE:/CodeSystem/ActCode/Version/9.0.0/Code/AMB/Info - String codePath = - TerminologyServiceUtil.buildCodePath( - code.getCodeSystem(), version.getVsac(), code.getName()); - VsacCode svsCode = terminologyWebClient.getCode(codePath, apiKey); - if (svsCode.getStatus().equalsIgnoreCase("ok")) { - if ("Yes".equals(svsCode.getData().getResultSet().get(0).getActive())) { - return CodeStatus.ACTIVE; - } else { - return CodeStatus.INACTIVE; - } + return null; } - return CodeStatus.NA; + return version.getVsac(); } /** diff --git a/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java b/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java index b3b9b4c..2ae6f6f 100644 --- a/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java +++ b/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java @@ -2,6 +2,7 @@ import gov.cms.madie.models.measure.ManifestExpansion; import gov.cms.madie.terminology.dto.Code; +import gov.cms.madie.terminology.dto.CodeStatus; import gov.cms.madie.terminology.dto.QdmValueSet; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; import gov.cms.madie.terminology.exceptions.VsacUnauthorizedException; @@ -22,10 +23,7 @@ import java.security.Principal; import java.time.Instant; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -230,4 +228,33 @@ void testGetCodeIfNoUmlsUserFound() { () -> vsacFhirTerminologyController.getCode(codeName, codeSystem, version, principal)); assertEquals(ex.getMessage(), "Please login to UMLS before proceeding"); } + + @Test + void testGetCodesList() { + List> codeList = + List.of( + Map.of( + "code", "1963-8", "codeSystem", "LOINC", "oid", "'urn:oid:2.16.840.1.113883.6.1'"), + Map.of( + "code", "8462-4", "codeSystem", "LOINC", "oid", "'urn:oid:2.16.840.1.113883.6.1'")); + Code code = + Code.builder() + .name("1963-8") + .codeSystem("LOINC") + .version("2.72") + .display("Bicarbonate [Moles/volume] in Serum") + .codeSystemOid("2.16.840.1.113883.6.1") + .status(CodeStatus.valueOf("ACTIVE")) + .build(); + + Principal principal = mock(Principal.class); + when(principal.getName()).thenReturn(TEST_USER); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); + when(fhirTerminologyService.retrieveCodesAndCodeSystems(any(), anyString())) + .thenReturn(List.of(code)); + ResponseEntity> response = + vsacFhirTerminologyController.getCodesAndCodeSystems(codeList, principal); + assertEquals(response.getStatusCode(), HttpStatus.OK); + assertEquals(response.getBody().get(0), code); + } } diff --git a/src/test/java/gov/cms/madie/terminology/service/FhirTerminologyServiceTest.java b/src/test/java/gov/cms/madie/terminology/service/FhirTerminologyServiceTest.java index 727071e..e483f25 100644 --- a/src/test/java/gov/cms/madie/terminology/service/FhirTerminologyServiceTest.java +++ b/src/test/java/gov/cms/madie/terminology/service/FhirTerminologyServiceTest.java @@ -451,4 +451,70 @@ void testRetrieveCodeSuccessfully() { assertThat(code.getVersion(), is(equalTo(version))); assertThat(code.getStatus(), is(equalTo(CodeStatus.ACTIVE))); } + + @Test + void testRetrieveCodesListSuccessfully() { + List> codeList = + List.of( + Map.of( + "code", "1963-8", "codeSystem", "LOINC", "oid", "'urn:oid:2.16.840.1.113883.6.1'")); + + String codeJson = + "{\n" + + " \"resourceType\": \"Parameters\",\n" + + " \"parameter\": [ {\n" + + " \"name\": \"name\",\n" + + " \"valueString\": \"LOINC\"\n" + + " }, {\n" + + " \"name\": \"version\",\n" + + " \"valueString\": \"2.40\"\n" + + " }, {\n" + + " \"name\": \"display\",\n" + + " \"valueString\": \"Bicarbonate [Moles/volume] in Serum\"\n" + + " }, {\n" + + " \"name\": \"Oid\",\n" + + " \"valueString\": \"2.16.840.1.113883.6.1\"\n" + + " } ]\n" + + "}"; + + codeSystemEntries = new ArrayList<>(); + CodeSystemEntry.Version version = new CodeSystemEntry.Version(); + version.setVsac("2.40"); + version.setFhir("2.40"); + var codeSystemEntry = + CodeSystemEntry.builder() + .name("8462-4") + .oid("urn:oid:2.16.840.1.113883.6.1") + .url("http://loinc.org") + .versions(List.of(version)) + .build(); + codeSystemEntries.add(codeSystemEntry); + + gov.cms.madie.terminology.models.CodeSystem codeSystem = + gov.cms.madie.terminology.models.CodeSystem.builder() + .id("LOINC2.40") + .fullUrl("http://loinc.org") + .title("LOINC") + .name("LOINC") + .version("2.40") + .versionId("404676818") + .oid("urn:oid:2.16.840.1.113883.6.1") + .lastUpdated(Instant.parse("2024-04-30T20:18:48.706Z")) + .lastUpdatedUpstream(new Date("Fri Apr 01 00:00:00 EDT 2022")) + .build(); + + when(mappingService.getCodeSystemEntries()).thenReturn(codeSystemEntries); + when(codeSystemRepository.findByOidAndVersion(anyString(), anyString())) + .thenReturn(Optional.ofNullable(codeSystem)); + when(fhirTerminologyServiceWebClient.getCodeResource(anyString(), any(), any())) + .thenReturn(codeJson); + when(fhirContext.newJsonParser()).thenReturn(FhirContext.forR4().newJsonParser()); + when(vsacService.getCodeStatus(any(), anyString())).thenReturn(CodeStatus.ACTIVE); + List code = fhirTerminologyService.retrieveCodesAndCodeSystems(codeList, TEST_API_KEY); + assertThat(code.get(0).getName(), is(equalTo("1963-8"))); + assertThat(code.get(0).getDisplay(), is(equalTo("Bicarbonate [Moles/volume] in Serum"))); + assertThat(code.get(0).getCodeSystem(), is(equalTo("LOINC"))); + assertThat(code.get(0).getVersion(), is(equalTo("2.40"))); + assertThat(code.get(0).getStatus(), is(equalTo(CodeStatus.ACTIVE))); + } } diff --git a/src/test/java/gov/cms/madie/terminology/service/VsacServiceTest.java b/src/test/java/gov/cms/madie/terminology/service/VsacServiceTest.java index 56f5a00..2ed9d5d 100644 --- a/src/test/java/gov/cms/madie/terminology/service/VsacServiceTest.java +++ b/src/test/java/gov/cms/madie/terminology/service/VsacServiceTest.java @@ -456,27 +456,29 @@ void testVerifyUmlsAccess() { @Test void testGetCodeStatusIfCodeSystemMappingAbsent() { - when(mappingService.getCodeSystemEntry(anyString())).thenReturn(null); + when(mappingService.getCodeSystemEntryByOid(anyString())).thenReturn(null); assertThat( - vsacService.getCodeStatus(Code.builder().codeSystem("test").build(), TEST_API_KEY), + vsacService.getCodeStatus( + Code.builder().codeSystemOid("oid").version("version").build(), TEST_API_KEY), is(equalTo(CodeStatus.NA))); } @Test void testGetCodeStatusIfCodeSystemNotInSvs() { var cse = CodeSystemEntry.builder().oid("NOT.IN.VSAC1").build(); - when(mappingService.getCodeSystemEntry(anyString())).thenReturn(cse); + when(mappingService.getCodeSystemEntryByOid(anyString())).thenReturn(cse); assertThat( - vsacService.getCodeStatus(Code.builder().codeSystem("test").build(), TEST_API_KEY), + vsacService.getCodeStatus( + Code.builder().codeSystemOid("oid").version("version").build(), TEST_API_KEY), is(equalTo(CodeStatus.NA))); } @Test void testGetCodeStatusIfCodeSystemVersionEmpty() { var cse = CodeSystemEntry.builder().oid("1.1.1.1").versions(List.of()).build(); - when(mappingService.getCodeSystemEntry(anyString())).thenReturn(cse); + when(mappingService.getCodeSystemEntryByOid(anyString())).thenReturn(cse); assertThat( - vsacService.getCodeStatus(Code.builder().codeSystem("test").build(), TEST_API_KEY), + vsacService.getCodeStatus(Code.builder().codeSystemOid("oid").build(), TEST_API_KEY), is(equalTo(CodeStatus.NA))); } @@ -486,9 +488,9 @@ void testGetCodeStatusIfCodeSystemVersionForVsacIsNull() { version.setVsac(null); version.setFhir("https://fhir-version"); var cse = CodeSystemEntry.builder().oid("1.1.1.1").versions(List.of(version)).build(); - when(mappingService.getCodeSystemEntry(anyString())).thenReturn(cse); + when(mappingService.getCodeSystemEntryByOid(anyString())).thenReturn(cse); assertThat( - vsacService.getCodeStatus(Code.builder().codeSystem("test").build(), TEST_API_KEY), + vsacService.getCodeStatus(Code.builder().codeSystemOid("oid").build(), TEST_API_KEY), is(equalTo(CodeStatus.NA))); } @@ -519,7 +521,7 @@ void testGetCodeStatusActive() { VsacCode vsacCode = new VsacCode(); vsacCode.setStatus("ok"); vsacCode.setData(codeData); - when(mappingService.getCodeSystemEntry(anyString())).thenReturn(codeSystemEntry); + when(mappingService.getCodeSystemEntryByOid(anyString())).thenReturn(codeSystemEntry); when(terminologyServiceWebClient.getCode(anyString(), anyString())).thenReturn(vsacCode); CodeStatus status = vsacService.getCodeStatus(code, TEST_API_KEY); assertThat(status, is(equalTo(CodeStatus.ACTIVE))); @@ -552,14 +554,14 @@ void testGetCodeStatusInactive() { VsacCode vsacCode = new VsacCode(); vsacCode.setStatus("ok"); vsacCode.setData(codeData); - when(mappingService.getCodeSystemEntry(anyString())).thenReturn(codeSystemEntry); + when(mappingService.getCodeSystemEntryByOid(anyString())).thenReturn(codeSystemEntry); when(terminologyServiceWebClient.getCode(anyString(), anyString())).thenReturn(vsacCode); CodeStatus status = vsacService.getCodeStatus(code, TEST_API_KEY); assertThat(status, is(equalTo(CodeStatus.INACTIVE))); } @Test - void testGetCodeStatusIfCOdeNotFoundInSvs() { + void testGetCodeStatusIfCodeNotFoundInSvs() { CodeSystemEntry.Version version = new CodeSystemEntry.Version(); version.setVsac("2023-09"); version.setFhir("abc.info/20230901"); @@ -585,7 +587,7 @@ void testGetCodeStatusIfCOdeNotFoundInSvs() { VsacCode vsacCode = new VsacCode(); vsacCode.setStatus("non-ok"); vsacCode.setData(codeData); - when(mappingService.getCodeSystemEntry(anyString())).thenReturn(codeSystemEntry); + when(mappingService.getCodeSystemEntryByOid(anyString())).thenReturn(codeSystemEntry); when(terminologyServiceWebClient.getCode(anyString(), anyString())).thenReturn(vsacCode); CodeStatus status = vsacService.getCodeStatus(code, TEST_API_KEY); assertThat(status, is(equalTo(CodeStatus.NA)));