Skip to content

Commit

Permalink
NIAD-3148 - Immunizations outside of consultation are mapped to Obser…
Browse files Browse the repository at this point in the history
…vation (#752)

* [NIAD-3148] Add method to TestUtility.java `getEhrFolderComponent`

* [NIAD-3148] Add method to TestUtility.java `getEhrFolderComponent`

* [NIAD-3148] Add some unit tests for current logic

* [NIAD-3148] Remove redundant test file

* [NIAD-3148] Initial refactor of DatabaseImmunizationChecker.java

* [NIAD-3148] Create DatabaseImmunizationCheckerTest

* [NIAD-3148] Create utility to create CD values

* [NIAD-3148] Add functionality to create-database-postgres.sql to join the description ID and concept ID

* [NIAD-3148] Method signature renamed to `getImmunizationSnomedUsingConceptOrDescriptionId`

* [NIAD-3148] Address problem with test-load-immunization-codes.sh

* Add DirtiesContext to DatabaseImmunizationCheckerIT

* [NIAD-3148] Address PR to #752 (comment)

* [NIAD-3148] Address PR comments https://github.com/NHSDigital/nia-patient-switching-standard-adaptor/pull/752/files#r1710239250 and https://github.com/NHSDigital/nia-patient-switching-standard-adaptor/pull/752/files#r1710234310

* [NIAD-3148] Address PR comment https://github.com/NHSDigital/nia-patient-switching-standard-adaptor/pull/752/files#r1710219516

* [NIAD-3148] Update CHANGELOG.md

* [NIAD-3148] Update CHANGELOG.md

---------

Co-authored-by: MartinWheelerMT <[email protected]>
  • Loading branch information
martin-nhs and MartinWheelerMT authored Aug 12, 2024
1 parent 58e6022 commit fccd428
Show file tree
Hide file tree
Showing 14 changed files with 495 additions and 51 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ corresponding FHIR resource will now be [appropriately populated][nopat-docs].
corresponding FHIR resource will now be [appropriately populated][nopat-docs].
* If a `bloodPressure` record includes a `confidentialityCode`, the `meta.security` field of the
corresponding FHIR resource will now be [appropriately populated][nopat-docs].
* Addressed a bug in the PS adaptor where immunizations were incorrectly mapped to observations.
The adaptor now verifies the Snomed CT ID against both the Concept ID and the Description ID, ensuring
that immunizations are correctly identified when a match is found.

### Fixed
* Resolved issue where the SNOMED import script would reject a password containing a '%' character.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
public interface ImmunizationSnomedCTDao {
@SqlQuery("select_immunization_concept_id")
@UseClasspathSqlLocator
ImmunizationSnomedCT getImmunizationSnomednUsingConceptId(@Bind("conceptId") String conceptId);
ImmunizationSnomedCT getImmunizationSnomedUsingConceptOrDescriptionId(@Bind("snomedId") String snomedId);

@SqlQuery("verify_immunizations_loaded")
@UseClasspathSqlLocator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@

@Component
public class ImmunizationSnomedCTMapper implements RowMapper<ImmunizationSnomedCT> {
private static final String COLUMN_NAME = "concept_or_description_id";

@Override
public ImmunizationSnomedCT map(ResultSet rs, StatementContext ctx) throws SQLException {
final String conceptOrDescriptionId = rs.getString(COLUMN_NAME);
return ImmunizationSnomedCT.builder()
.conceptId(rs.getString("conceptId"))
.snomedId(conceptOrDescriptionId)
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
@Setter
@Builder
public class ImmunizationSnomedCT {
private String conceptId;
private String snomedId;
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
SELECT i.conceptid FROM "snomedct".immunization_codes i WHERE conceptid = :conceptId;
SELECT i.concept_or_description_id FROM "snomedct".immunization_codes i WHERE i.concept_or_description_id = :snomedId;
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
-- the conceptIds being checked for below represent the immunization root codes and those immunization codes outside
-- of the root code hierarchy, as described in the snomed-database-loader README.md file

SELECT COUNT(i.conceptid) = 16 -- total number of codes
SELECT COUNT(DISTINCT i.concept_or_description_id) = 16 -- total number of codes
FROM "snomedct".immunization_codes i
WHERE i.conceptid IN (
WHERE i.concept_or_description_id IN (
'787859002', '127785005', '304250009', '90351000119108','713404003','2997511000001102',
'308101000000104','1036721000000101', '1373691000000102', '945831000000105', '542931000000103',
'735981009', '90640007', '571631000119106', '764141000000106', '170399005'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package uk.nhs.adaptors.pss.translator;

import org.hl7.v3.CD;
import org.hl7.v3.RCMRMT030101UKObservationStatement;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import uk.nhs.adaptors.pss.translator.util.DatabaseImmunizationChecker;

import static org.assertj.core.api.Assertions.assertThat;
import static uk.nhs.adaptors.pss.translator.TestUtility.createCd;

@SpringBootTest
@DirtiesContext
@ExtendWith(SpringExtension.class)
class DatabaseImmunizationCheckerIT {
private static final String SNOMED_CODE_SYSTEM_OID = "2.16.840.1.113883.2.1.3.2.4.15";
private static final String DISPLAY_NAME = "Influenza vaccination";

@Autowired
private DatabaseImmunizationChecker databaseImmunizationChecker;

@Test
void When_Immunization_With_SnomedDescriptionId_Expect_True() {
final String immunizationDescriptionSnomedId = "142934010";
final CD cd = getCdForSnomedId(immunizationDescriptionSnomedId);
final RCMRMT030101UKObservationStatement observationStatement =
new RCMRMT030101UKObservationStatement();

observationStatement.setCode(cd);

final boolean result = databaseImmunizationChecker.isImmunization(observationStatement);

assertThat(result).isTrue();
}

@Test
void When_Immunization_With_SnomedConceptId_Expect_True() {
final String immunizationConceptSnomedId = "86198006";
final CD cd = getCdForSnomedId(immunizationConceptSnomedId);
final RCMRMT030101UKObservationStatement observationStatement =
new RCMRMT030101UKObservationStatement();

observationStatement.setCode(cd);

final boolean result = databaseImmunizationChecker.isImmunization(observationStatement);

assertThat(result).isTrue();
}

private CD getCdForSnomedId(String snomedId) {
return createCd(
snomedId,
SNOMED_CODE_SYSTEM_OID,
DISPLAY_NAME
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,39 @@

import lombok.RequiredArgsConstructor;
import uk.nhs.adaptors.connector.dao.ImmunizationSnomedCTDao;
import uk.nhs.adaptors.connector.model.ImmunizationSnomedCT;

@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class DatabaseImmunizationChecker implements ImmunizationChecker {
private final ImmunizationSnomedCTDao immunizationSnomedDao;

@Override
public boolean isImmunization(RCMRMT030101UKObservationStatement observationStatement) {
ImmunizationSnomedCT immunizationCode = null;
final boolean translationIsPresent = !observationStatement.getCode().getTranslation().isEmpty();

if (!observationStatement.getCode().getTranslation().isEmpty()) {
immunizationCode = immunizationSnomedDao
.getImmunizationSnomednUsingConceptId(observationStatement.getCode().getTranslation().get(0).getCode());
}
if (translationIsPresent) {
final boolean isImmunization = isTranslationCodeImmunization(observationStatement);

if (immunizationCode == null) {
immunizationCode = immunizationSnomedDao.getImmunizationSnomednUsingConceptId(observationStatement.getCode().getCode());
if (isImmunization) {
return true;
}
}

return immunizationCode != null;
return isCodeImmunization(observationStatement);
}

private boolean isTranslationCodeImmunization(RCMRMT030101UKObservationStatement observationStatement) {
final String code = observationStatement.getCode()
.getTranslation()
.getFirst()
.getCode();

return immunizationSnomedDao.getImmunizationSnomedUsingConceptOrDescriptionId(code) != null;
}

private boolean isCodeImmunization(RCMRMT030101UKObservationStatement observationStatement) {
final String code = observationStatement.getCode().getCode();

return immunizationSnomedDao.getImmunizationSnomedUsingConceptOrDescriptionId(code) != null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package uk.nhs.adaptors.pss.translator;

import org.hl7.v3.CD;
import org.hl7.v3.CV;
import org.hl7.v3.RCMRMT030101UKComponent3;
import org.hl7.v3.RCMRMT030101UKEhrComposition;
import org.hl7.v3.RCMRMT030101UKEhrExtract;
import org.hl7.v3.RCMRMT030101UKEhrFolder;

import java.util.List;
import java.util.function.Function;

public final class TestUtility {
private TestUtility() { }

public static final Function<RCMRMT030101UKEhrExtract, RCMRMT030101UKEhrComposition> GET_EHR_COMPOSITION =
extract -> extract
.getComponent().get(0)
.getComponent().getFirst()
.getEhrFolder()
.getComponent().get(0)
.getComponent().getFirst()
.getEhrComposition();

public static CV createCv(String code, String codeSystem, String displayName) {
Expand All @@ -28,6 +32,32 @@ public static CV createCv(String code) {
return createCv(code, "", "");
}

public static CD createCd(String code, String codeSystem, String displayName) {
final CD cd = new CD();
cd.setCode(code);
cd.setCodeSystem(codeSystem);
cd.setDisplayName(displayName);
return cd;
}

/**
* An EHR Extract has a cardinality of one to many components, each component (based
* of the UK05 schema) can contain one and only one EHR Folder. This utility method provides
* a means of extracting ALL components from within a target EHR Folder.
* @param extract The EHR Extract.
* @param extractComponentIndex The index of the RCMRMT030101UKComponent which houses the EHR Folder.
* @return A list of RCMRMT030101UKComponent3.
*/
public static List<RCMRMT030101UKComponent3> getEhrFolderComponents(RCMRMT030101UKEhrExtract extract,
int extractComponentIndex) {
final RCMRMT030101UKEhrFolder targetFolder = extract
.getComponent()
.get(extractComponentIndex)
.getEhrFolder();

return targetFolder.getComponent();
}

public static class NoConfidentialityCodePresentException extends RuntimeException {
private static final String EXCEPTION_MESSAGE = "No confidentiality code is present within the test file.";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package uk.nhs.adaptors.pss.translator.util;

import jakarta.xml.bind.JAXBException;
import org.hl7.v3.RCMRMT030101UKComponent3;
import org.hl7.v3.RCMRMT030101UKEhrExtract;
import org.hl7.v3.RCMRMT030101UKObservationStatement;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import uk.nhs.adaptors.connector.dao.ImmunizationSnomedCTDao;
import uk.nhs.adaptors.connector.model.ImmunizationSnomedCT;
import uk.nhs.adaptors.pss.translator.FileFactory;

import java.io.File;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.Mockito.when;
import static uk.nhs.adaptors.pss.translator.TestUtility.getEhrFolderComponents;
import static uk.nhs.adaptors.pss.translator.util.XmlUnmarshallUtil.unmarshallFile;

@ExtendWith(MockitoExtension.class)
class DatabaseImmunizationCheckerTest {
@Mock
private ImmunizationSnomedCTDao immunizationSnomedCTDao;

@InjectMocks
private DatabaseImmunizationChecker databaseImmunizationChecker;

@Captor
private ArgumentCaptor<String> snomedCtIdCaptor;

private static final String TEST_FILES_DIRECTORY = "Immunization";

@Test
void When_IsObservationStatementImmunization_With_ImmunizationCode_Expect_True() throws JAXBException {
final String expectedCode = "3955997015";
final RCMRMT030101UKObservationStatement observationStatement = getObservationStatementFromExtract(
"full_valid_immunization_with_no_translation.xml");
final ImmunizationSnomedCT immunizationSnomedCT = ImmunizationSnomedCT.builder()
.snomedId(expectedCode)
.build();

when(immunizationSnomedCTDao.getImmunizationSnomedUsingConceptOrDescriptionId(
snomedCtIdCaptor.capture()
)).thenReturn(immunizationSnomedCT);

final boolean result = databaseImmunizationChecker.isImmunization(observationStatement);

assertAll(
() -> assertThat(result).isTrue(),
() -> assertThat(snomedCtIdCaptor.getValue()).isEqualTo(expectedCode)
);
}

@Test
void When_IsObservationStatementImmunization_With_ImmunizationCodeAndNonImmunizationTranslation_Expect_True() throws JAXBException {
final String snomedCode = "142934010";
final String readsV2Code = "65E..00";
final RCMRMT030101UKObservationStatement observationStatement = getObservationStatementFromExtract(
"full_valid_immunization_with_translation.xml"
);

final ImmunizationSnomedCT immunizationSnomedCT = ImmunizationSnomedCT.builder()
.snomedId(snomedCode)
.build();

when(immunizationSnomedCTDao.getImmunizationSnomedUsingConceptOrDescriptionId(
snomedCtIdCaptor.capture()
)).thenReturn(null, immunizationSnomedCT);

final boolean result = databaseImmunizationChecker.isImmunization(observationStatement);

assertAll(
() -> assertThat(result).isTrue(),
() -> assertThat(snomedCtIdCaptor.getAllValues().getFirst()).isEqualTo(readsV2Code),
() -> assertThat(snomedCtIdCaptor.getAllValues().get(1)).isEqualTo(snomedCode)
);
}

private RCMRMT030101UKObservationStatement getObservationStatementFromExtract(String filename) throws JAXBException {
final RCMRMT030101UKEhrExtract ehrExtract = getEhrExtractFromFile(filename);
final List<RCMRMT030101UKComponent3> components = getEhrFolderComponents(ehrExtract, 0);

return components.getFirst()
.getEhrComposition()
.getComponent()
.getFirst()
.getObservationStatement();
}

private RCMRMT030101UKEhrExtract getEhrExtractFromFile(String filename) throws JAXBException {
final File file = FileFactory.getXmlFileFor(TEST_FILES_DIRECTORY, filename);
return unmarshallFile(file, RCMRMT030101UKEhrExtract.class);
}
}
Loading

0 comments on commit fccd428

Please sign in to comment.