From f408434135e74fbbb982c4f54bd42a2edfa1dd01 Mon Sep 17 00:00:00 2001 From: MartinWheelerMT <88717465+MartinWheelerMT@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:28:21 +0100 Subject: [PATCH] NIAD-2961-preserve-test-group-header-ordering (#693) * Add unit test to SpecimenCompoundsMapper to test ordering or result references. Add associated test XML file. * Update SpecimenCompoundsMapper to preserve ordering for result observation references when dealing with `CLUSTER` and `BATTERY`. * Update PWTP9-output.json to reflect ordering changes above. Update CHANGELOG.md --- CHANGELOG.md | 3 + .../e2e-mapping/output-json/PWTP9-output.json | 24 ++--- .../SpecimenCompoundsMapper.java | 94 ++++++++++------ .../SpecimenCompoundsMapperTest.java | 65 ++++++++++++ ...specimen_with_three_test_group_headers.xml | 100 ++++++++++++++++++ 5 files changed, 241 insertions(+), 45 deletions(-) create mode 100644 gp2gp-translator/src/test/resources/xml/SpecimenComponents/specimen_with_three_test_group_headers.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 97d6e7a13..759ab5586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added * The AllergyIntoleranceMapper has been enhanced to support the redaction fix. If an Allergy Intolerance record includes a confidentialityCode, the meta.security field of the corresponding FHIR resource will now be appropriately populated. +### Fixed +* DiagnosticReports now preserves original ordering for mapped result references. + ## [3.0.0] - 2024-07-02 ### Removed diff --git a/gp2gp-translator/src/integrationTest/resources/e2e-mapping/output-json/PWTP9-output.json b/gp2gp-translator/src/integrationTest/resources/e2e-mapping/output-json/PWTP9-output.json index fd83c3f94..75b2b9859 100644 --- a/gp2gp-translator/src/integrationTest/resources/e2e-mapping/output-json/PWTP9-output.json +++ b/gp2gp-translator/src/integrationTest/resources/e2e-mapping/output-json/PWTP9-output.json @@ -13733,16 +13733,16 @@ } ], "result": [ { "reference": "Observation/00000000-0000-0000-0000-000000000006" + }, { + "reference": "Observation/B39D6784-5532-45FB-BDCC-7F7F21A70857" + }, { + "reference": "Observation/CDC2077F-F701-47F9-AFF9-87E8C07E8C69" }, { "reference": "Observation/05E6D1C6-9B66-4D02-8FC3-C5CF3CC0EB1C" }, { "reference": "Observation/CFAF8307-C6B6-4EB4-A69D-81DDE4D2CD91" }, { "reference": "Observation/45F8816D-FC99-4131-ABE6-01CD8A8DE49C" - }, { - "reference": "Observation/B39D6784-5532-45FB-BDCC-7F7F21A70857" - }, { - "reference": "Observation/CDC2077F-F701-47F9-AFF9-87E8C07E8C69" } ], "conclusion": " Clinical Details:\n NOTE:Specimen date" } @@ -13780,14 +13780,14 @@ } ], "result": [ { "reference": "Observation/00000000-0000-0000-0000-000000000007" + }, { + "reference": "Observation/15A8BA89-FF03-4880-B540-613745F6BFE3" }, { "reference": "Observation/471FD197-79B8-47C0-B5D1-6A4AFBA3D31D" }, { "reference": "Observation/59F968AD-21F1-4B97-96DE-D1AE22091132" }, { "reference": "Observation/36DA8895-114B-40D6-9FA1-82B543D86893" - }, { - "reference": "Observation/15A8BA89-FF03-4880-B540-613745F6BFE3" } ] } }, { @@ -13863,12 +13863,12 @@ } ], "result": [ { "reference": "Observation/00000000-0000-0000-0000-000000000008" + }, { + "reference": "Observation/0F1AD848-FA61-4278-B273-048B0E247D39" }, { "reference": "Observation/768BF90A-8E36-49D0-9D29-BD3FF0DE733D" }, { "reference": "Observation/69832EFE-727E-4270-BDF5-179851BDF295" - }, { - "reference": "Observation/0F1AD848-FA61-4278-B273-048B0E247D39" } ], "conclusion": " Infection" } @@ -13905,6 +13905,8 @@ "reference": "Specimen/EC494B1A-FFEF-41E2-94AB-B287AE516361" } ], "result": [ { + "reference": "Observation/8DDBEE57-3545-490B-BE7F-EEB352A5BB6F" + }, { "reference": "Observation/E956E311-2809-400A-97E1-73F6CD15F98A" }, { "reference": "Observation/701F4CF2-3A60-4117-9DF5-8178D4FCA019" @@ -13924,8 +13926,6 @@ "reference": "Observation/CF963013-BE3D-4845-865A-05722B1DB292" }, { "reference": "Observation/D04342AD-DD02-40A4-AE8B-CBB62588B448" - }, { - "reference": "Observation/8DDBEE57-3545-490B-BE7F-EEB352A5BB6F" } ] } }, { @@ -13961,11 +13961,11 @@ "reference": "Specimen/EFB6D2BC-B95B-42DE-A021-6C83225E7BAC" } ], "result": [ { - "reference": "Observation/5578BCAA-4C60-482E-ADFD-095CC8CAB102" - }, { "reference": "Observation/EBEDD7EA-EE7A-4632-BEE4-FDDA9B4D4B11" }, { "reference": "Observation/2418B6B6-C4C0-46CB-9030-5B7DD39C80FC" + }, { + "reference": "Observation/5578BCAA-4C60-482E-ADFD-095CC8CAB102" } ], "conclusion": " No clinical details given" } diff --git a/gp2gp-translator/src/main/java/uk/nhs/adaptors/pss/translator/mapper/diagnosticreport/SpecimenCompoundsMapper.java b/gp2gp-translator/src/main/java/uk/nhs/adaptors/pss/translator/mapper/diagnosticreport/SpecimenCompoundsMapper.java index d9f1988ea..483fc6979 100644 --- a/gp2gp-translator/src/main/java/uk/nhs/adaptors/pss/translator/mapper/diagnosticreport/SpecimenCompoundsMapper.java +++ b/gp2gp-translator/src/main/java/uk/nhs/adaptors/pss/translator/mapper/diagnosticreport/SpecimenCompoundsMapper.java @@ -10,6 +10,7 @@ import java.util.Objects; import java.util.Optional; +import lombok.extern.slf4j.Slf4j; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.DiagnosticReport; import org.hl7.fhir.dstu3.model.Encounter; @@ -26,6 +27,7 @@ import org.hl7.v3.RCMRMT030101UKEhrExtract; import org.hl7.v3.RCMRMT030101UKNarrativeStatement; import org.hl7.v3.RCMRMT030101UKObservationStatement; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -36,6 +38,7 @@ import static uk.nhs.adaptors.common.util.CodeableConceptUtils.createCodeableConcept; +@Slf4j @Service @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class SpecimenCompoundsMapper { @@ -49,45 +52,52 @@ public class SpecimenCompoundsMapper { private final SpecimenBatteryMapper batteryMapper; - public List handleSpecimenChildComponents(RCMRMT030101UKEhrExtract ehrExtract, List observations, - List observationComments, List diagnosticReports, - Patient patient, List encounters, String practiseCode) { - + public List handleSpecimenChildComponents( + RCMRMT030101UKEhrExtract ehrExtract, + List observations, + List observationComments, + List diagnosticReports, + Patient patient, + List encounters, + String practiseCode + ) { final List batteryObservations = new ArrayList<>(); for (var diagnosticReport : diagnosticReports) { - var diagnosticReportCompoundStatement = getCompoundStatementByDRId(ehrExtract, diagnosticReport.getId()); + if (diagnosticReportCompoundStatement.isEmpty()) { + continue; + } - if (diagnosticReportCompoundStatement.isPresent()) { - - for (var specimenCompoundStatement : getSpecimenCompoundStatements(diagnosticReportCompoundStatement.orElseThrow())) { + for (var specimenCompoundStatement : getSpecimenCompoundStatements(diagnosticReportCompoundStatement.get())) { + handleSpecimenObservationStatement(observations, diagnosticReport, specimenCompoundStatement); - for (var specimenObservationStatement : getObservationStatementsInCompound(specimenCompoundStatement)) { - getObservationById(observations, specimenObservationStatement.getId().getRoot()) - .ifPresent(observation -> { - handleObservationStatement(specimenCompoundStatement, specimenObservationStatement, observation); - DiagnosticReportMapper.addResultToDiagnosticReport(observation, diagnosticReport); - }); - } + var nestedSpecimenCompoundStatements = getNestedSpecimenCompoundStatements(specimenCompoundStatement); - for (var clusterCompoundStatement : getCompoundStatementsInSpecimenCompound(specimenCompoundStatement, - CLUSTER_CLASSCODE)) { + for (var nestedSpecimenCompoundStatement : nestedSpecimenCompoundStatements) { + if (CLUSTER_CLASSCODE.equals(nestedSpecimenCompoundStatement.getClassCode().get(0))) { handleClusterCompoundStatement( - specimenCompoundStatement, clusterCompoundStatement, observations, observationComments, diagnosticReport, + specimenCompoundStatement, + nestedSpecimenCompoundStatement, + observations, + observationComments, + diagnosticReport, false ); } - for (var batteryCompoundStatement : getCompoundStatementsInSpecimenCompound(specimenCompoundStatement, - BATTERY_CLASSCODE)) { + if (BATTERY_CLASSCODE.equals(nestedSpecimenCompoundStatement.getClassCode().get(0))) { handleBatteryCompoundStatement( - specimenCompoundStatement, batteryCompoundStatement, observations, observationComments, diagnosticReport + specimenCompoundStatement, + nestedSpecimenCompoundStatement, + observations, + observationComments, + diagnosticReport ); final SpecimenBatteryParameters batteryParameters = SpecimenBatteryParameters.builder() .ehrExtract(ehrExtract) - .batteryCompoundStatement(batteryCompoundStatement) + .batteryCompoundStatement(nestedSpecimenCompoundStatement) .specimenCompoundStatement(specimenCompoundStatement) .ehrComposition(getCurrentEhrComposition(ehrExtract, diagnosticReportCompoundStatement.orElseThrow())) .diagnosticReport(diagnosticReport) @@ -107,6 +117,30 @@ public List handleSpecimenChildComponents(RCMRMT030101UKEhrExtract return batteryObservations; } + private static @NotNull List getNestedSpecimenCompoundStatements( + RCMRMT030101UKCompoundStatement specimenCompoundStatement + ) { + return specimenCompoundStatement.getComponent() + .stream() + .filter(RCMRMT030101UKComponent02::hasCompoundStatement) + .map(RCMRMT030101UKComponent02::getCompoundStatement) + .filter(SpecimenCompoundsMapper::hasBatteryOrClusterClassCode) + .toList(); + } + + private void handleSpecimenObservationStatement( + List observations, + DiagnosticReport diagnosticReport, + RCMRMT030101UKCompoundStatement specimenCompoundStatement + ) { + getObservationStatementsInCompound(specimenCompoundStatement).forEach(specimenObservationStatement -> + getObservationById(observations, specimenObservationStatement.getId().getRoot()) + .ifPresent(observation -> { + handleObservationStatement(specimenCompoundStatement, specimenObservationStatement, observation); + DiagnosticReportMapper.addResultToDiagnosticReport(observation, diagnosticReport); + })); + } + private void handleObservationStatement(RCMRMT030101UKCompoundStatement specimenCompoundStatement, RCMRMT030101UKObservationStatement observationStatement, Observation observation) { final Reference specimenReference = new Reference(new IdType( @@ -282,17 +316,6 @@ private List getNarrativeStatementsInCompound( .toList(); } - private List getCompoundStatementsInSpecimenCompound( - RCMRMT030101UKCompoundStatement specimenCompoundStatement, String classCode) { - - return specimenCompoundStatement.getComponent() - .stream() - .filter(RCMRMT030101UKComponent02::hasCompoundStatement) - .map(RCMRMT030101UKComponent02::getCompoundStatement) - .filter(compoundStatement -> classCode.equals(compoundStatement.getClassCode().get(0))) - .toList(); - } - private RCMRMT030101UKEhrComposition getCurrentEhrComposition(RCMRMT030101UKEhrExtract ehrExtract, RCMRMT030101UKCompoundStatement parentCompoundStatement) { @@ -306,4 +329,9 @@ private RCMRMT030101UKEhrComposition getCurrentEhrComposition(RCMRMT030101UKEhrE .anyMatch(parentCompoundStatement::equals) ).findFirst().get(); } + + private static boolean hasBatteryOrClusterClassCode(RCMRMT030101UKCompoundStatement compoundStatement) { + return CLUSTER_CLASSCODE.equals(compoundStatement.getClassCode().get(0)) + || BATTERY_CLASSCODE.equals(compoundStatement.getClassCode().get(0)); + } } diff --git a/gp2gp-translator/src/test/java/uk/nhs/adaptors/pss/translator/mapper/diagnosticreport/SpecimenCompoundsMapperTest.java b/gp2gp-translator/src/test/java/uk/nhs/adaptors/pss/translator/mapper/diagnosticreport/SpecimenCompoundsMapperTest.java index 19f37de31..b2ec73596 100644 --- a/gp2gp-translator/src/test/java/uk/nhs/adaptors/pss/translator/mapper/diagnosticreport/SpecimenCompoundsMapperTest.java +++ b/gp2gp-translator/src/test/java/uk/nhs/adaptors/pss/translator/mapper/diagnosticreport/SpecimenCompoundsMapperTest.java @@ -1,30 +1,40 @@ package uk.nhs.adaptors.pss.translator.mapper.diagnosticreport; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.AdditionalAnswers.answer; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.springframework.util.ResourceUtils.getFile; +import static uk.nhs.adaptors.pss.translator.mapper.diagnosticreport.SpecimenBatteryMapper.SpecimenBatteryParameters; import static uk.nhs.adaptors.pss.translator.util.XmlUnmarshallUtil.unmarshallFile; import java.util.ArrayList; import java.util.List; import org.hl7.fhir.dstu3.model.DiagnosticReport; +import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.dstu3.model.Observation; import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Reference; +import org.hl7.fhir.dstu3.model.ResourceType; import org.hl7.v3.RCMRMT030101UKEhrExtract; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.jupiter.MockitoExtension; import lombok.SneakyThrows; +import org.mockito.stubbing.Answer1; @ExtendWith(MockitoExtension.class) +@RunWith(MockitoJUnitRunner.class) public class SpecimenCompoundsMapperTest { private static final String XML_RESOURCES_BASE = "xml/SpecimenComponents/"; @@ -159,6 +169,54 @@ public void testHandlingNonUserNarrativeStatement() { assertThat(observation.getIssuedElement().asStringValue()).isNull(); } + @Test public void testOrderingIsPreservedForDiagnosticReportResults() { + final RCMRMT030101UKEhrExtract ehrExtract = + unmarshallEhrExtract("specimen_with_three_test_group_headers.xml"); + + final var testObservations = List.of( + (Observation) new Observation().setId("TEST-GROUP-HEADER-1"), + (Observation) new Observation().setId("OBSERVATION-STATEMENT-ID-1"), + (Observation) new Observation().setId("TEST-GROUP-HEADER-2"), + (Observation) new Observation().setId("OBSERVATION-STATEMENT-ID-2"), + (Observation) new Observation().setId("TEST-GROUP-HEADER-3"), + (Observation) new Observation().setId("OBSERVATION-STATEMENT-ID-3") + ); + + doAnswer( + answer( + (Answer1) batteryParameters -> { + var id = batteryParameters.getBatteryCompoundStatement().getId().get(0).getRoot(); + var observation = new Observation(); + observation.setId(id); + + batteryParameters.getDiagnosticReport().addResult( + new Reference(new IdType(ResourceType.Observation.name(), id))); + return observation; + }) + ).when(specimenBatteryMapper).mapBatteryObservation(any(SpecimenBatteryMapper.SpecimenBatteryParameters.class)); + + specimenCompoundsMapper.handleSpecimenChildComponents( + ehrExtract, + testObservations, + observationComments, + diagnosticReports, + PATIENT, + List.of(), + TEST_PRACTISE_CODE + ); + + var diagnosticReport = diagnosticReports.get(0); + + assertAll( + () -> assertThat(getReferenceId(diagnosticReport.getResult().get(0))) + .isEqualTo("Observation/TEST-GROUP-HEADER-1"), + () -> assertThat(getReferenceId(diagnosticReport.getResult().get(1))) + .isEqualTo("OBSERVATION-STATEMENT-ID-2"), + () -> assertThat(getReferenceId(diagnosticReport.getResult().get(2))) + .isEqualTo("OBSERVATION-STATEMENT-ID-3") + ); + } + private void assertParentSpecimenIsReferenced(Observation observation) { assertThat(observation.hasSpecimen()).isTrue(); assertThat(observation.getSpecimen().hasReference()).isTrue(); @@ -203,6 +261,13 @@ private List createObservationComments() { return observationComments; } + private String getReferenceId(Reference reference) { + return reference.hasReference() + ? reference.getReference() + : reference.getResource().getIdElement().getValue(); + } + + @SneakyThrows private RCMRMT030101UKEhrExtract unmarshallEhrExtract(String fileName) { return unmarshallFile(getFile("classpath:" + XML_RESOURCES_BASE + fileName), RCMRMT030101UKEhrExtract.class); diff --git a/gp2gp-translator/src/test/resources/xml/SpecimenComponents/specimen_with_three_test_group_headers.xml b/gp2gp-translator/src/test/resources/xml/SpecimenComponents/specimen_with_three_test_group_headers.xml new file mode 100644 index 000000000..743dd37e3 --- /dev/null +++ b/gp2gp-translator/src/test/resources/xml/SpecimenComponents/specimen_with_three_test_group_headers.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + Filed Report + + + + + + + + + + + + + +
+ + + Blood + + + + + + + + Full blood count - FBC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file