From 440cf1284598f4b5bc61e674ee60abfcff938988 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 29 Nov 2024 09:04:27 -0500 Subject: [PATCH 1/4] Experimental changes for issues 600, 601, and 602. --- cqf-fhir-cr/pom.xml | 10 + .../cr/measure/common/MeasureEvaluator.java | 29 ++ .../cr/measure/constant/MeasureConstants.java | 4 + .../cr/measure/r4/R4MeasureReportBuilder.java | 8 + .../cr/measure/r4/R4MeasureReportScorer.java | 76 +++- .../cr/measure/r4/R4SubmitDataService.java | 1 + .../fhir/cr/measure/r4/MeasureScorerTest.java | 52 ++- .../measure/r4/R4MeasureEvaluationTest.java | 361 ++++++++++++++---- .../measure/r4/R4SubmitDataServiceTest.java | 76 ++++ 9 files changed, 531 insertions(+), 86 deletions(-) create mode 100644 cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java diff --git a/cqf-fhir-cr/pom.xml b/cqf-fhir-cr/pom.xml index fdc0dad1d..9f73417ae 100644 --- a/cqf-fhir-cr/pom.xml +++ b/cqf-fhir-cr/pom.xml @@ -76,6 +76,16 @@ slf4j-test test + + + org.jetbrains + annotations + 23.0.0 + test + diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 32e9759a3..3ea7538d3 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -19,6 +19,8 @@ import java.time.ZonedDateTime; import java.util.Collections; 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; @@ -303,6 +305,8 @@ protected MeasureDef evaluate( EvaluationResult result = libraryEngine.getEvaluationResult(id, subjectId, null, null, null, null, zonedDateTime, context); + someEvaluationValidationStuff(measureDef, result); + evaluateSubject(measureDef, subjectTypePart, subjectIdPart, subjectSize, type, result); } @@ -565,6 +569,7 @@ protected void evaluateGroup( int populationSize, MeasureReportType reportType, EvaluationResult evaluationResult) { + evaluateStratifiers(subjectId, groupDef.stratifiers(), evaluationResult); var scoring = groupDef.measureScoring(); @@ -582,6 +587,30 @@ protected void evaluateGroup( } } + private void someEvaluationValidationStuff(MeasureDef measureDef, EvaluationResult evaluationResult) { + measureDef.groups().forEach(groupDef -> someEvaluationValidationStuff(groupDef, evaluationResult)); + } + + private void someEvaluationValidationStuff(GroupDef groupDef, EvaluationResult evaluationResult) { + final Map expressionResults = evaluationResult.expressionResults; + final CodeDef groupDefPopulationBasis = groupDef.getPopulationBasis(); + final List stratifiers = groupDef.stratifiers(); + final List> criteriaResults = stratifiers.stream() + .map(StratifierDef::getResults).toList(); + + // LUKETODO: LEFT side of the comparison is the groupDefPopulationBasis + logger.info("expressionResults: {}, groupDefPopulationBasis: {}", expressionResults, + groupDefPopulationBasis); + + for (Map criteriaResult : criteriaResults) { + logger.info("criteriaResult: {}", criteriaResult); + + for (Entry criteriaResultEntry : criteriaResult.entrySet()) { + logger.info("criteriaResultEntry: {}", criteriaResultEntry); + } + } + } + protected Object evaluateDateOfCompliance(PopulationDef populationDef) { var ref = Libraries.resolveExpressionRef( populationDef.expression(), this.context.getState().getCurrentLibrary()); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java index e77860bd0..15cec4f9b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java @@ -43,8 +43,12 @@ private MeasureConstants() {} public static final String FHIR_ALL_TYPES_SYSTEM_URL = "http://hl7.org/fhir/fhir-types"; public static final String POPULATION_BASIS_URL = "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis"; + // LUKETODO: get rid of this: + @Deprecated public static final String EXT_TOTAL_DENOMINATOR_URL = "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-cqfm-denominator-membership"; + // LUKETODO: get rid of this: + @Deprecated public static final String EXT_TOTAL_NUMERATOR_URL = "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-cqfm-numerator-membership"; } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java index 35ab735ba..52985a500 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java @@ -346,11 +346,15 @@ protected void buildGroup( } if (groupDef.isBooleanBasis()) { + // LUKETODO: why doesn't this consider exclusions? + // LUKETODO: do we add this WITH or WITHOUT exclusions? my guess is WITHOUT addExtension( reportGroup, EXT_TOTAL_DENOMINATOR_URL, getReportPopulation(groupDef, TOTALDENOMINATOR), true); addExtension(reportGroup, EXT_TOTAL_NUMERATOR_URL, getReportPopulation(groupDef, TOTALNUMERATOR), true); } else { + // LUKETODO: why doesn't this consider exclusions? + // LUKETODO: do we add this WITH or WITHOUT exclusions? my guess is WITHOUT addExtension( reportGroup, EXT_TOTAL_DENOMINATOR_URL, getReportPopulation(groupDef, TOTALDENOMINATOR), false); addExtension( @@ -377,6 +381,8 @@ protected void addExtension( group.addExtension().setUrl(extUrl).setValue(new StringType(Integer.toString(count))); } + // LUKETODO: is there such a thing as validation for group population basis? + /** * * Resource result --> Patient Key, Resource result --> can intersect on patient for Boolean basis, can't for Resource @@ -390,6 +396,7 @@ protected void validateStratifierBasisType(Map subjectVa .filter(x -> x.rawValue() instanceof Resource) .collect(Collectors.toList()); if (list.size() != subjectValues.values().size()) { + // LUKETODO: this is the stratifier case throw new IllegalArgumentException( "stratifier expression criteria results must match the same type as population."); } @@ -497,6 +504,7 @@ protected void buildStratum( } // add totalDenominator and totalNumerator extensions + // LUKETODO: buildStratumExtPopulation(groupDef, TOTALDENOMINATOR, subjectIds, stratum, EXT_TOTAL_DENOMINATOR_URL); buildStratumExtPopulation(groupDef, TOTALNUMERATOR, subjectIds, stratum, EXT_TOTAL_NUMERATOR_URL); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index d86301dc7..5484ef1ae 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -1,17 +1,26 @@ package org.opencds.cqf.fhir.cr.measure.r4; +import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.DENOMINATOR; +import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.NUMERATOR; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.EXT_TOTAL_DENOMINATOR_URL; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.EXT_TOTAL_NUMERATOR_URL; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent; +import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupStratifierComponent; import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupComponent; import org.hl7.fhir.r4.model.Quantity; import org.opencds.cqf.fhir.cr.measure.common.BaseMeasureReportScorer; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; +import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType; import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; +import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.Objects; /** *

The R4 MeasureScorer takes population components from MeasureReport resources and scores each group population @@ -36,6 +45,8 @@ */ public class R4MeasureReportScorer extends BaseMeasureReportScorer { + private static final Logger logger = LoggerFactory.getLogger(R4MeasureReportScorer.class); + @Override public void score(MeasureDef measureDef, MeasureReport measureReport) { // Measure Def Check @@ -48,10 +59,18 @@ public void score(MeasureDef measureDef, MeasureReport measureReport) { } for (MeasureReportGroupComponent mrgc : measureReport.getGroup()) { + scoreGroup( getGroupMeasureScoring(mrgc, measureDef), mrgc, getGroupDef(measureDef, mrgc).isIncreaseImprovementNotation()); + + final GroupDef groupDef = getGroupDef(measureDef, mrgc); + + final PopulationDef numeratorPopulationDef = groupDef.getSingle(NUMERATOR); + final PopulationDef denominatorPopulationDef = groupDef.getSingle(DENOMINATOR); + + logger.info("numeratorPopulationDef: {}, denominatorPopulationDef: {} ", numeratorPopulationDef, denominatorPopulationDef); } } @@ -109,9 +128,61 @@ protected void scoreGroup( switch (measureScoring) { case PROPORTION: case RATIO: + // LUKETODO: should this be from the MeasureReportGroupComponent or the GroupDef? + /* + MeasureScorer should now look for Numerator/Denominator values to make the calculation instead of the extension values. + Any code that sets or maintains these populations can be removed + Testing classes that have assertions for these values should be deprecated + */ + final List populations = mrgc.getPopulation(); + + // LUKETODO: I think we need to do the depopulations.stream() + final List numerators = populations.stream() + .filter(population -> "numerator".equals( + population.getCode().getCodingFirstRep().getCode())) + .toList(); + + final List denominators = populations.stream() + .filter(population -> "numerator".equals( + population.getCode().getCodingFirstRep().getCode())) + .toList(); + + final Integer populationNumeratorCount = populations.stream() + .filter(population -> "numerator".equals(population.getCode().getCodingFirstRep().getCode())) + .map(MeasureReportGroupPopulationComponent::getCount) + .findAny() + .orElse(0); + + final Integer populationDenominatorCount = populations.stream() + .filter(population -> "denominator".equals(population.getCode().getCodingFirstRep().getCode())) + .map(MeasureReportGroupPopulationComponent::getCount) + .findAny() + .orElse(0); + logger.info("populationNumeratorCount: {}, populationDenominatorCount: {}", populationNumeratorCount, populationDenominatorCount); + System.out.println("populationNumeratorCount = " + populationNumeratorCount ); + System.out.println("populationDenominatorCount = " + populationDenominatorCount); + + final Integer extNumeratorCount = getGroupExtensionCount(mrgc, + EXT_TOTAL_NUMERATOR_URL); + final Integer extDenominatorCount = getGroupExtensionCount(mrgc, + EXT_TOTAL_DENOMINATOR_URL); + + logger.info("extNumeratorCount: {}, extDenominatorCount: {}", extNumeratorCount, extDenominatorCount); + System.out.println("extNumeratorCount= " + populationNumeratorCount); + System.out.println("extDenominatorCount= " + extDenominatorCount); + + + if (!Objects.equals(extNumeratorCount, populationNumeratorCount)) { + throw new IllegalStateException("numerator counts don't match: ext:" + extNumeratorCount + " != population:" + populationNumeratorCount); + } + + if (!Objects.equals(extDenominatorCount, populationDenominatorCount)) { + throw new IllegalStateException("denominator counts don't match: ext:" + extDenominatorCount + " != population:" + populationDenominatorCount); + } + Double score = this.calcProportionScore( - getGroupExtensionCount(mrgc, EXT_TOTAL_NUMERATOR_URL), - getGroupExtensionCount(mrgc, EXT_TOTAL_DENOMINATOR_URL)); + extNumeratorCount, + extDenominatorCount); if (score != null) { if (isIncreaseImprovementNotation) { mrgc.setMeasureScore(new Quantity(score)); @@ -133,6 +204,7 @@ protected void scoreStratum(MeasureScoring measureScoring, StratifierGroupCompon switch (measureScoring) { case PROPORTION: case RATIO: + // LUKETODO: Double score = this.calcProportionScore( getStratumPopulationCount(stratum, EXT_TOTAL_NUMERATOR_URL), getStratumPopulationCount(stratum, EXT_TOTAL_DENOMINATOR_URL)); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java index 7bed3d0d2..333ed200a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java @@ -10,6 +10,7 @@ public class R4SubmitDataService { + // LUKETODO: test this private final Repository repository; public R4SubmitDataService(Repository repository) { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java index 0e117c954..09b8b956f 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java @@ -18,14 +18,14 @@ class MeasureScorerTest { - List myMeasures = getMyMeasures(); - List myMeasureReports = getMyMeasureReports(); + List measures = getMeasures(); + List measureReports = getMeasureReports(); @Test void scoreOnlyPopulationIdMultiRateMeasure() { var measureUrl = "http://content.alphora.com/fhir/uv/mips-qm-content-r4/Measure/multirate-groupid"; var measureScoringDef = getMeasureScoringDef(measureUrl); - var measureReport = getMyMeasureReport(measureUrl); + var measureReport = getMeasureReport(measureUrl); try { R4MeasureReportScorer scorer = new R4MeasureReportScorer(); @@ -52,11 +52,29 @@ void scorerThrowsIfNoScoringSupplied() { } } + // LUKETODO: these counts are off each other: + // LUKETODO: is this just because of bad test data, or are the counts really different? + /* +populationNumeratorCount = 1 +populationDenominatorCount = 1 +extNumeratorCount= 1 +extDenominatorCount= 1 +populationNumeratorCount = 1 +populationDenominatorCount = 2 +extNumeratorCount= 1 +extDenominatorCount= 1 +populationNumeratorCount = 1 +populationDenominatorCount = 2 +extNumeratorCount= 1 +extDenominatorCount= 2 + + */ + // LUKETODO: fix counts in JSONs @Test void scorePopulationIdMultiRate() { var measureUrl = "http://ecqi.healthit.gov/ecqms/Measure/FHIR347"; var measureScoringDef = getMeasureScoringDef(measureUrl); - var measureReport = getMyMeasureReport(measureUrl); + var measureReport = getMeasureReport(measureUrl); R4MeasureReportScorer scorer = new R4MeasureReportScorer(); scorer.score(measureScoringDef, measureReport); @@ -76,7 +94,7 @@ void scorePopulationIdMultiRate() { void scoreErrorNoIds() { var measureUrl = "http://content.alphora.com/fhir/uv/mips-qm-content-r4/Measure/multirate-groupid-error"; var measureScoringDef = getMeasureScoringDef(measureUrl); - var measureReport = getMyMeasureReport(measureUrl); + var measureReport = getMeasureReport(measureUrl); try { R4MeasureReportScorer scorer = new R4MeasureReportScorer(); scorer.score(measureScoringDef, measureReport); @@ -92,7 +110,7 @@ void scoreErrorNoIds() { void scoreZeroDenominator() { var measureUrl = "http://content.alphora.com/fhir/uv/mips-qm-content-r4/Measure/multirate-zeroden"; var measureScoringDef = getMeasureScoringDef(measureUrl); - var measureReport = getMyMeasureReport(measureUrl); + var measureReport = getMeasureReport(measureUrl); R4MeasureReportScorer scorer = new R4MeasureReportScorer(); scorer.score(measureScoringDef, measureReport); @@ -100,11 +118,12 @@ void scoreZeroDenominator() { assertNull(group(measureReport, "DataCompleteness").getMeasureScore().getValue()); } + // LUKETODO: fix counts in JSONs @Test void scoreNoExtension() { var measureUrl = "http://content.alphora.com/fhir/uv/mips-qm-content-r4/Measure/multirate-noext"; var measureScoringDef = getMeasureScoringDef(measureUrl); - var measureReport = getMyMeasureReport(measureUrl); + var measureReport = getMeasureReport(measureUrl); R4MeasureReportScorer scorer = new R4MeasureReportScorer(); scorer.score(measureScoringDef, measureReport); @@ -112,12 +131,19 @@ void scoreNoExtension() { assertNull(group(measureReport, "PerformanceRate").getMeasureScore().getValue()); } + // LUKETODO: these counts are in sync: + /* + populationNumeratorCount = 5 + populationDenominatorCount = 10 + extNumeratorCount= 5 + extDenominatorCount= 10 + */ @Test void scoreGroupIdMultiStratum() { var measureUrl = "http://ecqi.healthit.gov/ecqms/Measure/PrimaryCariesPreventionasOfferedbyPCPsincludingDentistsFHIR"; var measureScoringDef = getMeasureScoringDef(measureUrl); - var measureReport = getMyMeasureReport(measureUrl); + var measureReport = getMeasureReport(measureUrl); R4MeasureReportScorer scorer = new R4MeasureReportScorer(); scorer.score(measureScoringDef, measureReport); @@ -169,7 +195,7 @@ public MeasureReport.MeasureReportGroupStratifierComponent getStratumById( .get(); } - public List getMyMeasures() { + public List getMeasures() { // Measures FhirResourceLoader measures = new FhirResourceLoader( FhirContext.forR4(), this.getClass(), List.of("MeasureScoring/Measures/"), false); @@ -181,7 +207,7 @@ public List getMyMeasures() { return measureList; } - public List getMyMeasureReports() { + public List getMeasureReports() { FhirResourceLoader measureReports = new FhirResourceLoader( FhirContext.forR4(), this.getClass(), List.of("MeasureScoring/MeasureReports/"), false); List measureReportList = new ArrayList<>(); @@ -193,7 +219,7 @@ public List getMyMeasureReports() { } public MeasureDef getMeasureScoringDef(String measureUrl) { - var measureRes = myMeasures.stream() + var measureRes = measures.stream() .filter(measure -> measureUrl.equals(measure.getUrl())) .findAny() .orElse(null); @@ -201,8 +227,8 @@ public MeasureDef getMeasureScoringDef(String measureUrl) { return measureDefBuilder.build(measureRes); } - public MeasureReport getMyMeasureReport(String measureUrl) { - return myMeasureReports.stream() + public MeasureReport getMeasureReport(String measureUrl) { + return measureReports.stream() .filter(measureReport -> measureUrl.equals(measureReport.getMeasure())) .findAny() .orElse(null); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java index dac5ac52e..9f8dc88f6 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java @@ -1,8 +1,11 @@ package org.opencds.cqf.fhir.cr.measure.r4; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -24,12 +27,14 @@ import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nonnull; import org.cqframework.cql.cql2elm.LibraryManager; import org.cqframework.cql.cql2elm.ModelManager; import org.cqframework.cql.cql2elm.StringLibrarySourceProvider; import org.hl7.elm.r1.VersionedIdentifier; import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; @@ -48,9 +53,12 @@ import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullSource; import org.opencds.cqf.cql.engine.data.CompositeDataProvider; import org.opencds.cqf.cql.engine.data.DataProvider; @@ -71,6 +79,9 @@ public class R4MeasureEvaluationTest extends BaseMeasureEvaluationTest { + private static final CodeType POPULATION_BASIS_BOOLEAN = new CodeType("boolean"); + private static final CodeType POPULATION_BASIS_ENCOUNTER = new CodeType("Encounter"); + public String getFhirVersion() { return "4.0.1"; } @@ -81,7 +92,7 @@ public String getFhirVersion() { private MeasureEvaluationOptions evaluationOptions = MeasureEvaluationOptions.defaultOptions(); @Test - void cohortMeasureEvaluation() throws Exception { + void cohortMeasureEvaluation() { Patient patient = john_doe(); RetrieveProvider retrieveProvider = mock(RetrieveProvider.class); @@ -102,15 +113,17 @@ void cohortMeasureEvaluation() throws Exception { String cql = cql_with_dateTime() + sde_race() + "define InitialPopulation: 'Doe' in Patient.name.family\n"; + System.out.println("cql = \n" + cql); + Measure measure = cohort_measure(); MeasureReport report = runTest(cql, Collections.singletonList(patient.getId()), measure, retrieveProvider, null); - checkEvidence(patient, report); + checkEvidence(report); } @Test - void sdeInMeasureEvaluation() throws Exception { + void sdeInMeasureEvaluation() { Patient patient = john_doe(); RetrieveProvider retrieveProvider = mock(RetrieveProvider.class); @@ -131,15 +144,17 @@ void sdeInMeasureEvaluation() throws Exception { String cql = cql_with_dateTime() + sde_race() + "define InitialPopulation: 'Doe' in Patient.name.family\n"; + System.out.println("cql = \n" + cql); + Measure measure = cohort_measure(); MeasureReport report = runTest(cql, Collections.singletonList(patient.getId()), measure, retrieveProvider, null); - checkEvidence(patient, report); + checkEvidence(report); } @Test - void proportionMeasureEvaluation() throws Exception { + void proportionMeasureEvaluation() { Patient patient = john_doe(); RetrieveProvider retrieveProvider = mock(RetrieveProvider.class); @@ -156,22 +171,24 @@ void proportionMeasureEvaluation() throws Exception { any(), any(), any())) - .thenReturn(Arrays.asList(patient)); + .thenReturn(List.of(patient)); String cql = cql_with_dateTime() + sde_race() + "define InitialPopulation: 'Doe' in Patient.name.family\n" + "define Denominator: 'John' in Patient.name.given\n" + "define Numerator: Patient.birthDate > @1970-01-01\n"; + System.out.println("cql = \n" + cql); + Measure measure = proportion_measure(); MeasureReport report = runTest(cql, Collections.singletonList(patient.getId()), measure, retrieveProvider, null); - checkEvidence(patient, report); + checkEvidence(report); } @Test - void proportionMeasureEvaluationWithDate() throws Exception { + void proportionMeasureEvaluationWithDate() { Patient patient = john_doe(); RetrieveProvider retrieveProvider = mock(RetrieveProvider.class); @@ -188,21 +205,23 @@ void proportionMeasureEvaluationWithDate() throws Exception { any(), any(), any())) - .thenReturn(Arrays.asList(patient)); + .thenReturn(List.of(patient)); String cql = cql_with_date() + sde_race() + "define InitialPopulation: 'Doe' in Patient.name.family\n" + "define Denominator: 'John' in Patient.name.given\n" + "define Numerator: AgeInYearsAt(start of \"Measurement Period\") > 18\n"; + System.out.println("cql = \n" + cql); + Measure measure = proportion_measure(); MeasureReport report = runTest(cql, Collections.singletonList(patient.getId()), measure, retrieveProvider, null); - checkEvidence(patient, report); + checkEvidence(report); } @Test - void continuousVariableMeasureEvaluation() throws Exception { + void continuousVariableMeasureEvaluation() { Patient patient = john_doe(); RetrieveProvider retrieveProvider = mock(RetrieveProvider.class); @@ -219,19 +238,96 @@ void continuousVariableMeasureEvaluation() throws Exception { any(), any(), any())) - .thenReturn(Arrays.asList(patient)); + .thenReturn(List.of(patient)); String cql = cql_with_dateTime() + sde_race() + "define InitialPopulation: 'Doe' in Patient.name.family\n" + "define MeasurePopulation: Patient.birthDate > @1970-01-01\n"; + System.out.println("cql = \n" + cql); + Measure measure = continuous_variable_measure(); + printResource(measure); + MeasureReport report = runTest(cql, Collections.singletonList(patient.getId()), measure, retrieveProvider, null); - checkEvidence(patient, report); + checkEvidence(report); } + // LUKETODO: cleanup: + @Language("JSON") + final String json = + """ + { + "resourceType": "Measure", + "id": "proportion", + "url": "http://test.com/fhir/Measure/Test", + "version": "1.0.0", + "name": "Test", + "scoring": { + "coding": [ { + "code": "proportion" + } ] + }, + "group": [ { + "population": [ { + "id": "initial-population", + "code": { + "coding": [ { + "code": "initial-population" + } ] + }, + "criteria": { + "expression": "InitialPopulation" + } + }, { + "id": "denominator", + "code": { + "coding": [ { + "code": "denominator" + } ] + }, + "criteria": { + "expression": "Denominator" + } + }, { + "id": "numerator", + "code": { + "coding": [ { + "code": "numerator" + } ] + }, + "criteria": { + "expression": "Numerator" + } + } ], + "stratifier": [ { + "id": "patient-gender", + "criteria": { + "expression": "Gender" + } + } ] + } ], + "supplementalData": [ { + "id": "sde-race", + "code": { + "text": "sde-race" + }, + "usage": [ { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/measure-data-usage", + "code": "supplemental-data" + } ] + } ], + "criteria": { + "language": "text/cql", + "expression": "SDE Race" + } + } ] + } + """; + // Prove we no longer error out for a SUBJECT report with multiple SDEs @ParameterizedTest @NullSource @@ -239,68 +335,107 @@ void continuousVariableMeasureEvaluation() throws Exception { value = MeasureEvalType.class, names = {"SUBJECT", "SUBJECTLIST", "POPULATION"}) void stratifiedMeasureEvaluation(@Nullable MeasureEvalType measureEvalTypeOverride) { - RetrieveProvider retrieveProvider = mock(RetrieveProvider.class); - when(retrieveProvider.retrieve( - isNull(), - isNull(), - isNull(), - eq("Patient"), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any())) - .thenReturn(Arrays.asList(jane_doe(), john_doe())); - when(retrieveProvider.retrieve( - eq("Patient"), - eq("id"), - eq("john-doe"), - eq("Patient"), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any())) - .thenReturn(Arrays.asList(john_doe())); - when(retrieveProvider.retrieve( - eq("Patient"), - eq("id"), - eq("jane-doe"), - eq("Patient"), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any())) - .thenReturn(Arrays.asList(jane_doe())); - String cql = cql_with_dateTime() + sde_race() + final String cql = cql_with_dateTime() + sde_race() + "define InitialPopulation: 'Doe' in Patient.name.family\n" + "define Denominator: 'John' in Patient.name.given\n" + "define Numerator: Patient.birthDate > @1970-01-01\n" + "define Gender: Patient.gender\n"; - Measure measure = stratified_measure(); + System.out.println("cql = \n" + cql); - MeasureReport report = runTest( + final MeasureReport report = runTest( cql, Arrays.asList(jane_doe().getId(), john_doe().getId()), - measure, - retrieveProvider, + stratified_measure(), + setupMockRetrieverProvider(), measureEvalTypeOverride); checkStratification(report); } + private static Stream stratifiedMeasureEvaluationByPopulationBasisHappyPathParams() { + return Stream.of( + Arguments.of(null, null), + Arguments.of(POPULATION_BASIS_BOOLEAN, POPULATION_BASIS_BOOLEAN), + Arguments.of(POPULATION_BASIS_BOOLEAN, POPULATION_BASIS_ENCOUNTER) + ); + } + + @ParameterizedTest + @MethodSource("stratifiedMeasureEvaluationByPopulationBasisHappyPathParams") + void stratifiedMeasureEvaluationByPopulationHappyPathBasis(@Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { + final String cql = cql_with_dateTime() + sde_race() + + "define InitialPopulation: 'Doe' in Patient.name.family\n" + + "define Denominator: 'John' in Patient.name.given\n" + + "define Numerator: Patient.birthDate > @1970-01-01\n" + "define Gender: Patient.gender\n"; + + System.out.println("cql = \n" + cql); + + final MeasureReport report = runTest( + cql, + Arrays.asList(jane_doe().getId(), john_doe().getId()), + stratified_measure(populationBasisTypeForMeasure, populationBasisTypeForGroup), + setupMockRetrieverProvider(), + null); + checkStratification(report); + } + + private static Stream stratifiedMeasureEvaluationByPopulationBasisErrorPathParams() { + return Stream.of( + Arguments.of(POPULATION_BASIS_ENCOUNTER, POPULATION_BASIS_BOOLEAN), + Arguments.of(POPULATION_BASIS_ENCOUNTER, POPULATION_BASIS_ENCOUNTER) + ); + } + + // LUKETODO: can I just shove "SDE Race" into the Numerator expression and call it a day? + @ParameterizedTest + @MethodSource("stratifiedMeasureEvaluationByPopulationBasisErrorPathParams") + void stratifiedMeasureEvaluationByPopulationErrorPathBasis(@Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { + final String cql = cql_with_dateTime() + sde_race() + + "define InitialPopulation: 'Doe' in Patient.name.family\n" + + "define Denominator: 'John' in Patient.name.given\n" + + "define Numerator: Patient.birthDate > @1970-01-01\n" + "define Gender: Patient.gender\n"; + + System.out.println("cql = \n" + cql); + + var x = """ + library Test version '1.0.0' + + using FHIR version '4.0.1' + include FHIRHelpers version '4.0.1' + + parameter "Measurement Period" Interval default Interval[@2019-01-01T00:00:00.0, @2020-01-01T00:00:00.0) + + context Patient + define "SDE Race": + (flatten ( + Patient.extension Extension + where Extension.url = 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race' + return Extension.extension + )) E + where E.url = 'ombCategory' + or E.url = 'detailed' + return E.value as Coding + + define InitialPopulation: 'Doe' in Patient.name.family + define Denominator: 'John' in Patient.name.given + define Numerator: Patient.birthDate > @1970-01-01 + """; + + try { + runTest( + cql, + Arrays.asList(jane_doe().getId(), john_doe().getId()), + stratified_measure(populationBasisTypeForMeasure, populationBasisTypeForGroup), + setupMockRetrieverProvider(), + null); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException exception) { + assertThat(exception.getMessage(), equalTo("stratifier expression criteria results must match the same type as population.")); + } + } + @Test - void evaluatePopulationCriteriaNullResult() throws Exception { + void evaluatePopulationCriteriaNullResult() { Patient patient = john_doe(); RetrieveProvider retrieveProvider = mock(RetrieveProvider.class); @@ -317,7 +452,7 @@ void evaluatePopulationCriteriaNullResult() throws Exception { any(), any(), any())) - .thenReturn(Arrays.asList(patient)); + .thenReturn(List.of(patient)); String cql = cql_with_dateTime() + sde_race() + "define InitialPopulation: null\n" + "define Denominator: null\n" + "define Numerator: null\n"; @@ -326,7 +461,7 @@ void evaluatePopulationCriteriaNullResult() throws Exception { MeasureReport report = runTest(cql, Collections.singletonList(patient.getId()), measure, retrieveProvider, null); - checkEvidence(patient, report); + checkEvidence(report); } private void checkStratification(MeasureReport report) { @@ -337,14 +472,14 @@ private void checkStratification(MeasureReport report) { StratifierGroupComponent sgc = mrgsc.getStratum().stream() .filter(x -> x.hasValue() && x.getValue().getText().equals("male")) .findFirst() - .get(); + .orElseThrow(); StratifierGroupPopulationComponent sgpc = sgc.getPopulation().stream() .filter(x -> x.getCode() .getCodingFirstRep() .getCode() .equals(MeasurePopulationType.INITIALPOPULATION.toCode())) .findFirst() - .get(); + .orElseThrow(); assertEquals(1, sgpc.getCount()); @@ -413,7 +548,7 @@ private MeasureEvalType getMeasureEvalType(List subjectIds, MeasureEvalT .orElse(subjectIds.size() == 1 ? MeasureEvalType.SUBJECT : MeasureEvalType.POPULATION); } - private void checkEvidence(Patient patient, MeasureReport report) { + private void checkEvidence(MeasureReport report) { Map contained = report.getContained().stream() .collect(Collectors.toMap(r -> r.getClass().getSimpleName(), Function.identity())); @@ -441,8 +576,12 @@ private Measure cohort_measure() { } private Measure proportion_measure() { + return proportion_measure(null, null); + } + + private Measure proportion_measure(@Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { - Measure measure = measure("proportion"); + Measure measure = measure("proportion", populationBasisTypeForMeasure, populationBasisTypeForGroup); addPopulation(measure, MeasurePopulationType.INITIALPOPULATION, "InitialPopulation"); addPopulation(measure, MeasurePopulationType.DENOMINATOR, "Denominator"); addPopulation(measure, MeasurePopulationType.NUMERATOR, "Numerator"); @@ -458,11 +597,19 @@ private Measure continuous_variable_measure() { addPopulation(measure, MeasurePopulationType.MEASUREPOPULATION, "MeasurePopulation"); addSDEComponent(measure); + printResource(measure); + return measure; } private Measure stratified_measure() { - Measure measure = proportion_measure(); + Measure measure = proportion_measure(null, null); + addStratifier(measure, "patient-gender", "Gender"); + return measure; + } + + private Measure stratified_measure(@Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { + Measure measure = proportion_measure(populationBasisTypeForMeasure, populationBasisTypeForGroup); addStratifier(measure, "patient-gender", "Gender"); return measure; } @@ -492,12 +639,27 @@ private void addSDEComponent(Measure measure) { } private Measure measure(String scoring) { + return measure(scoring, null, null); + } + + private Measure measure(String scoring, @Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { Measure measure = new Measure(); measure.setId(scoring); measure.setName("Test"); measure.setVersion("1.0.0"); measure.setUrl("http://test.com/fhir/Measure/Test"); measure.getScoring().getCodingFirstRep().setCode(scoring); + Optional.ofNullable(populationBasisTypeForMeasure) + .ifPresent(nonNullPopulationBasisType-> + measure.addExtension(new Extension() + .setUrl(MeasureConstants.POPULATION_BASIS_URL) + .setValue(populationBasisTypeForMeasure))); + Optional.ofNullable(populationBasisTypeForGroup) + .ifPresent(nonNullPopulationBasisType-> + measure.getGroupFirstRep() + .addExtension(new Extension() + .setUrl(MeasureConstants.POPULATION_BASIS_URL) + .setValue(populationBasisTypeForMeasure))); return measure; } @@ -516,7 +678,7 @@ private Patient john_doe() { Patient patient = new Patient(); patient.setId(new IdType("Patient", "john-doe")); patient.setName( - Arrays.asList(new HumanName().setFamily("Doe").setGiven(Arrays.asList(new StringType("John"))))); + List.of(new HumanName().setFamily("Doe").setGiven(List.of(new StringType("John"))))); patient.setBirthDate(new Date()); patient.setGender(AdministrativeGender.MALE); @@ -545,7 +707,7 @@ private Patient jane_doe() { Patient patient = new Patient(); patient.setId(new IdType("Patient", "jane-doe")); patient.setName( - Arrays.asList(new HumanName().setFamily("Doe").setGiven(Arrays.asList(new StringType("Jane"))))); + List.of(new HumanName().setFamily("Doe").setGiven(List.of(new StringType("Jane"))))); patient.setBirthDate(new Date()); patient.setGender(AdministrativeGender.FEMALE); @@ -561,4 +723,61 @@ private Patient jane_doe() { patient.getExtension().add(usCoreRace); return patient; } + + @Nonnull + private RetrieveProvider setupMockRetrieverProvider() { + RetrieveProvider retrieveProvider = mock(RetrieveProvider.class); + when(retrieveProvider.retrieve( + isNull(), + isNull(), + isNull(), + eq("Patient"), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any())) + .thenReturn(Arrays.asList(jane_doe(), john_doe())); + when(retrieveProvider.retrieve( + eq("Patient"), + eq("id"), + eq("john-doe"), + eq("Patient"), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any())) + .thenReturn(List.of(john_doe())); + when(retrieveProvider.retrieve( + eq("Patient"), + eq("id"), + eq("jane-doe"), + eq("Patient"), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any())) + .thenReturn(List.of(jane_doe())); + return retrieveProvider; + } + + private void printResource(Resource resource) { + final String json = FhirContext.forR4Cached() + .newJsonParser() + .setPrettyPrint(true) + .encodeResourceToString(resource); + + System.out.println("json = \n" + json); + } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java new file mode 100644 index 000000000..f62db30d7 --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java @@ -0,0 +1,76 @@ +package org.opencds.cqf.fhir.cr.measure.r4; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import ca.uhn.fhir.context.FhirContext; +import com.google.common.collect.Lists; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.StringType; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.api.Repository; +import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; +import org.opencds.cqf.fhir.utility.search.Searches; + +class R4SubmitDataServiceTest { + + private static final FhirContext FHIR_CONTEXT = FhirContext.forR4(); + + private final Repository repository = new InMemoryFhirRepository(FhirContext.forR4Cached()); + private final R4SubmitDataService testSubject = new R4SubmitDataService(repository); + + @Test + public void submitDataTest(){ + + //create resources + MeasureReport mr = newResource(MeasureReport.class).setMeasure("Measure/A123"); + Observation obs = newResource(Observation.class).setValue(new StringType("ABC")); + + //submit-data operation + var res = testSubject + .submitData(new IdType("Measure", "A123"), mr, + Lists.newArrayList(obs)); + + var resultMr = repository.search(Bundle.class, MeasureReport.class, Searches.ALL); + var mrSize = resultMr.getEntry().size(); + MeasureReport report = null; + for (int i = 0; i < mrSize; i++){ + var getEntry = resultMr.getEntry(); + var mrResource = (MeasureReport) getEntry.get(i).getResource(); + var measure = mrResource.getMeasure(); + if (measure.equals("Measure/A123")){ + report = mrResource; + break; + } + } + //found submitted MeasureReport! + assertNotNull(report); + + var resultOb = repository.search(Bundle.class, Observation.class, Searches.ALL); + var obSize = resultOb.getEntry().size(); + Observation observation = null; + for (int i = 0; i < obSize; i++){ + var getEntry = resultOb.getEntry(); + var obResource = (Observation) getEntry.get(i).getResource(); + var val = obResource.getValue().primitiveValue(); + if (val.equals("ABC")){ + observation = obResource; + break; + } + } + //found submitted Observation! + assertNotNull(observation); + + } + + @SuppressWarnings("unchecked") + private T newResource(Class theResourceClass) { + checkNotNull(theResourceClass); + + return (T) FHIR_CONTEXT.getResourceDefinition(theResourceClass).newInstance(); + } +} \ No newline at end of file From bed51591fad8171276fbe44cba20b344fa902e9f Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 29 Nov 2024 13:00:54 -0500 Subject: [PATCH 2/4] Spotless, test, and animal sniffer. --- .../cr/measure/common/MeasureEvaluator.java | 7 +- .../cr/measure/r4/R4MeasureReportScorer.java | 91 ++++----- .../fhir/cr/measure/r4/MeasureScorerTest.java | 28 +-- .../measure/r4/R4MeasureEvaluationTest.java | 182 +++++++++--------- .../measure/r4/R4SubmitDataServiceTest.java | 77 +++++--- 5 files changed, 204 insertions(+), 181 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 3ea7538d3..f44a84ab2 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -595,12 +595,11 @@ private void someEvaluationValidationStuff(GroupDef groupDef, EvaluationResult e final Map expressionResults = evaluationResult.expressionResults; final CodeDef groupDefPopulationBasis = groupDef.getPopulationBasis(); final List stratifiers = groupDef.stratifiers(); - final List> criteriaResults = stratifiers.stream() - .map(StratifierDef::getResults).toList(); + final List> criteriaResults = + stratifiers.stream().map(StratifierDef::getResults).collect(Collectors.toList()); // LUKETODO: LEFT side of the comparison is the groupDefPopulationBasis - logger.info("expressionResults: {}, groupDefPopulationBasis: {}", expressionResults, - groupDefPopulationBasis); + logger.info("expressionResults: {}, groupDefPopulationBasis: {}", expressionResults, groupDefPopulationBasis); for (Map criteriaResult : criteriaResults) { logger.info("criteriaResult: {}", criteriaResult); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index 5484ef1ae..fc645897b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -1,10 +1,10 @@ package org.opencds.cqf.fhir.cr.measure.r4; -import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.DENOMINATOR; -import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.NUMERATOR; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.EXT_TOTAL_DENOMINATOR_URL; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.EXT_TOTAL_NUMERATOR_URL; +import java.util.List; +import java.util.stream.Collectors; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; @@ -14,13 +14,9 @@ import org.opencds.cqf.fhir.cr.measure.common.BaseMeasureReportScorer; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; -import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType; import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; -import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; -import java.util.Objects; /** *

The R4 MeasureScorer takes population components from MeasureReport resources and scores each group population @@ -64,13 +60,6 @@ public void score(MeasureDef measureDef, MeasureReport measureReport) { getGroupMeasureScoring(mrgc, measureDef), mrgc, getGroupDef(measureDef, mrgc).isIncreaseImprovementNotation()); - - final GroupDef groupDef = getGroupDef(measureDef, mrgc); - - final PopulationDef numeratorPopulationDef = groupDef.getSingle(NUMERATOR); - final PopulationDef denominatorPopulationDef = groupDef.getSingle(DENOMINATOR); - - logger.info("numeratorPopulationDef: {}, denominatorPopulationDef: {} ", numeratorPopulationDef, denominatorPopulationDef); } } @@ -130,59 +119,63 @@ protected void scoreGroup( case RATIO: // LUKETODO: should this be from the MeasureReportGroupComponent or the GroupDef? /* - MeasureScorer should now look for Numerator/Denominator values to make the calculation instead of the extension values. - Any code that sets or maintains these populations can be removed - Testing classes that have assertions for these values should be deprecated - */ + MeasureScorer should now look for Numerator/Denominator values to make the calculation instead of the extension values. + Any code that sets or maintains these populations can be removed + Testing classes that have assertions for these values should be deprecated + */ final List populations = mrgc.getPopulation(); // LUKETODO: I think we need to do the depopulations.stream() final List numerators = populations.stream() - .filter(population -> "numerator".equals( - population.getCode().getCodingFirstRep().getCode())) - .toList(); + .filter(population -> "numerator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .collect(Collectors.toList()); final List denominators = populations.stream() - .filter(population -> "numerator".equals( - population.getCode().getCodingFirstRep().getCode())) - .toList(); + .filter(population -> "numerator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .collect(Collectors.toList()); final Integer populationNumeratorCount = populations.stream() - .filter(population -> "numerator".equals(population.getCode().getCodingFirstRep().getCode())) - .map(MeasureReportGroupPopulationComponent::getCount) - .findAny() - .orElse(0); + .filter(population -> "numerator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .map(MeasureReportGroupPopulationComponent::getCount) + .findAny() + .orElse(0); final Integer populationDenominatorCount = populations.stream() - .filter(population -> "denominator".equals(population.getCode().getCodingFirstRep().getCode())) - .map(MeasureReportGroupPopulationComponent::getCount) - .findAny() - .orElse(0); - logger.info("populationNumeratorCount: {}, populationDenominatorCount: {}", populationNumeratorCount, populationDenominatorCount); - System.out.println("populationNumeratorCount = " + populationNumeratorCount ); + .filter(population -> "denominator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .map(MeasureReportGroupPopulationComponent::getCount) + .findAny() + .orElse(0); + logger.info( + "populationNumeratorCount: {}, populationDenominatorCount: {}", + populationNumeratorCount, + populationDenominatorCount); + System.out.println("populationNumeratorCount = " + populationNumeratorCount); System.out.println("populationDenominatorCount = " + populationDenominatorCount); - final Integer extNumeratorCount = getGroupExtensionCount(mrgc, - EXT_TOTAL_NUMERATOR_URL); - final Integer extDenominatorCount = getGroupExtensionCount(mrgc, - EXT_TOTAL_DENOMINATOR_URL); + final Integer extNumeratorCount = getGroupExtensionCount(mrgc, EXT_TOTAL_NUMERATOR_URL); + final Integer extDenominatorCount = getGroupExtensionCount(mrgc, EXT_TOTAL_DENOMINATOR_URL); logger.info("extNumeratorCount: {}, extDenominatorCount: {}", extNumeratorCount, extDenominatorCount); System.out.println("extNumeratorCount= " + populationNumeratorCount); System.out.println("extDenominatorCount= " + extDenominatorCount); - - if (!Objects.equals(extNumeratorCount, populationNumeratorCount)) { - throw new IllegalStateException("numerator counts don't match: ext:" + extNumeratorCount + " != population:" + populationNumeratorCount); - } - - if (!Objects.equals(extDenominatorCount, populationDenominatorCount)) { - throw new IllegalStateException("denominator counts don't match: ext:" + extDenominatorCount + " != population:" + populationDenominatorCount); - } - - Double score = this.calcProportionScore( - extNumeratorCount, - extDenominatorCount); + // LUKETODO: + // if (!Objects.equals(extNumeratorCount, populationNumeratorCount)) { + // throw new IllegalStateException("numerator counts don't match: ext:" + + // extNumeratorCount + " != population:" + populationNumeratorCount); + // } + // + // if (!Objects.equals(extDenominatorCount, populationDenominatorCount)) { + // throw new IllegalStateException("denominator counts don't match: ext:" + + // extDenominatorCount + " != population:" + populationDenominatorCount); + // } + + Double score = this.calcProportionScore(extNumeratorCount, extDenominatorCount); + // LUKETODO: if (score != null) { if (isIncreaseImprovementNotation) { mrgc.setMeasureScore(new Quantity(score)); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java index 09b8b956f..58eac4fca 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java @@ -55,20 +55,20 @@ void scorerThrowsIfNoScoringSupplied() { // LUKETODO: these counts are off each other: // LUKETODO: is this just because of bad test data, or are the counts really different? /* -populationNumeratorCount = 1 -populationDenominatorCount = 1 -extNumeratorCount= 1 -extDenominatorCount= 1 -populationNumeratorCount = 1 -populationDenominatorCount = 2 -extNumeratorCount= 1 -extDenominatorCount= 1 -populationNumeratorCount = 1 -populationDenominatorCount = 2 -extNumeratorCount= 1 -extDenominatorCount= 2 - - */ + populationNumeratorCount = 1 + populationDenominatorCount = 1 + extNumeratorCount= 1 + extDenominatorCount= 1 + populationNumeratorCount = 1 + populationDenominatorCount = 2 + extNumeratorCount= 1 + extDenominatorCount= 1 + populationNumeratorCount = 1 + populationDenominatorCount = 2 + extNumeratorCount= 1 + extDenominatorCount= 2 + + */ // LUKETODO: fix counts in JSONs @Test void scorePopulationIdMultiRate() { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java index 9f8dc88f6..44cdff342 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java @@ -258,7 +258,7 @@ void continuousVariableMeasureEvaluation() { // LUKETODO: cleanup: @Language("JSON") final String json = - """ + """ { "resourceType": "Measure", "id": "proportion", @@ -347,64 +347,65 @@ void stratifiedMeasureEvaluation(@Nullable MeasureEvalType measureEvalTypeOverri cql, Arrays.asList(jane_doe().getId(), john_doe().getId()), stratified_measure(), - setupMockRetrieverProvider(), + setupMockRetrieverProvider(), measureEvalTypeOverride); checkStratification(report); } private static Stream stratifiedMeasureEvaluationByPopulationBasisHappyPathParams() { return Stream.of( - Arguments.of(null, null), - Arguments.of(POPULATION_BASIS_BOOLEAN, POPULATION_BASIS_BOOLEAN), - Arguments.of(POPULATION_BASIS_BOOLEAN, POPULATION_BASIS_ENCOUNTER) - ); + Arguments.of(null, null), + Arguments.of(POPULATION_BASIS_BOOLEAN, POPULATION_BASIS_BOOLEAN), + Arguments.of(POPULATION_BASIS_BOOLEAN, POPULATION_BASIS_ENCOUNTER)); } @ParameterizedTest @MethodSource("stratifiedMeasureEvaluationByPopulationBasisHappyPathParams") - void stratifiedMeasureEvaluationByPopulationHappyPathBasis(@Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { + void stratifiedMeasureEvaluationByPopulationHappyPathBasis( + @Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { final String cql = cql_with_dateTime() + sde_race() - + "define InitialPopulation: 'Doe' in Patient.name.family\n" - + "define Denominator: 'John' in Patient.name.given\n" - + "define Numerator: Patient.birthDate > @1970-01-01\n" + "define Gender: Patient.gender\n"; + + "define InitialPopulation: 'Doe' in Patient.name.family\n" + + "define Denominator: 'John' in Patient.name.given\n" + + "define Numerator: Patient.birthDate > @1970-01-01\n" + "define Gender: Patient.gender\n"; System.out.println("cql = \n" + cql); final MeasureReport report = runTest( - cql, - Arrays.asList(jane_doe().getId(), john_doe().getId()), - stratified_measure(populationBasisTypeForMeasure, populationBasisTypeForGroup), - setupMockRetrieverProvider(), - null); + cql, + Arrays.asList(jane_doe().getId(), john_doe().getId()), + stratified_measure(populationBasisTypeForMeasure, populationBasisTypeForGroup), + setupMockRetrieverProvider(), + null); checkStratification(report); } private static Stream stratifiedMeasureEvaluationByPopulationBasisErrorPathParams() { return Stream.of( - Arguments.of(POPULATION_BASIS_ENCOUNTER, POPULATION_BASIS_BOOLEAN), - Arguments.of(POPULATION_BASIS_ENCOUNTER, POPULATION_BASIS_ENCOUNTER) - ); + Arguments.of(POPULATION_BASIS_ENCOUNTER, POPULATION_BASIS_BOOLEAN), + Arguments.of(POPULATION_BASIS_ENCOUNTER, POPULATION_BASIS_ENCOUNTER)); } // LUKETODO: can I just shove "SDE Race" into the Numerator expression and call it a day? @ParameterizedTest @MethodSource("stratifiedMeasureEvaluationByPopulationBasisErrorPathParams") - void stratifiedMeasureEvaluationByPopulationErrorPathBasis(@Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { + void stratifiedMeasureEvaluationByPopulationErrorPathBasis( + @Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { final String cql = cql_with_dateTime() + sde_race() - + "define InitialPopulation: 'Doe' in Patient.name.family\n" - + "define Denominator: 'John' in Patient.name.given\n" - + "define Numerator: Patient.birthDate > @1970-01-01\n" + "define Gender: Patient.gender\n"; + + "define InitialPopulation: 'Doe' in Patient.name.family\n" + + "define Denominator: 'John' in Patient.name.given\n" + + "define Numerator: Patient.birthDate > @1970-01-01\n" + "define Gender: Patient.gender\n"; System.out.println("cql = \n" + cql); - var x = """ + var x = + """ library Test version '1.0.0' - + using FHIR version '4.0.1' include FHIRHelpers version '4.0.1' - + parameter "Measurement Period" Interval default Interval[@2019-01-01T00:00:00.0, @2020-01-01T00:00:00.0) - + context Patient define "SDE Race": (flatten ( @@ -415,7 +416,7 @@ void stratifiedMeasureEvaluationByPopulationErrorPathBasis(@Nullable CodeType po where E.url = 'ombCategory' or E.url = 'detailed' return E.value as Coding - + define InitialPopulation: 'Doe' in Patient.name.family define Denominator: 'John' in Patient.name.given define Numerator: Patient.birthDate > @1970-01-01 @@ -423,14 +424,16 @@ void stratifiedMeasureEvaluationByPopulationErrorPathBasis(@Nullable CodeType po try { runTest( - cql, - Arrays.asList(jane_doe().getId(), john_doe().getId()), - stratified_measure(populationBasisTypeForMeasure, populationBasisTypeForGroup), - setupMockRetrieverProvider(), - null); + cql, + Arrays.asList(jane_doe().getId(), john_doe().getId()), + stratified_measure(populationBasisTypeForMeasure, populationBasisTypeForGroup), + setupMockRetrieverProvider(), + null); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException exception) { - assertThat(exception.getMessage(), equalTo("stratifier expression criteria results must match the same type as population.")); + assertThat( + exception.getMessage(), + equalTo("stratifier expression criteria results must match the same type as population.")); } } @@ -579,7 +582,8 @@ private Measure proportion_measure() { return proportion_measure(null, null); } - private Measure proportion_measure(@Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { + private Measure proportion_measure( + @Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { Measure measure = measure("proportion", populationBasisTypeForMeasure, populationBasisTypeForGroup); addPopulation(measure, MeasurePopulationType.INITIALPOPULATION, "InitialPopulation"); @@ -608,7 +612,8 @@ private Measure stratified_measure() { return measure; } - private Measure stratified_measure(@Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { + private Measure stratified_measure( + @Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { Measure measure = proportion_measure(populationBasisTypeForMeasure, populationBasisTypeForGroup); addStratifier(measure, "patient-gender", "Gender"); return measure; @@ -642,7 +647,10 @@ private Measure measure(String scoring) { return measure(scoring, null, null); } - private Measure measure(String scoring, @Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { + private Measure measure( + String scoring, + @Nullable CodeType populationBasisTypeForMeasure, + @Nullable CodeType populationBasisTypeForGroup) { Measure measure = new Measure(); measure.setId(scoring); measure.setName("Test"); @@ -650,16 +658,14 @@ private Measure measure(String scoring, @Nullable CodeType populationBasisTypeFo measure.setUrl("http://test.com/fhir/Measure/Test"); measure.getScoring().getCodingFirstRep().setCode(scoring); Optional.ofNullable(populationBasisTypeForMeasure) - .ifPresent(nonNullPopulationBasisType-> - measure.addExtension(new Extension() + .ifPresent(nonNullPopulationBasisType -> measure.addExtension(new Extension() .setUrl(MeasureConstants.POPULATION_BASIS_URL) .setValue(populationBasisTypeForMeasure))); Optional.ofNullable(populationBasisTypeForGroup) - .ifPresent(nonNullPopulationBasisType-> - measure.getGroupFirstRep() - .addExtension(new Extension() - .setUrl(MeasureConstants.POPULATION_BASIS_URL) - .setValue(populationBasisTypeForMeasure))); + .ifPresent(nonNullPopulationBasisType -> measure.getGroupFirstRep() + .addExtension(new Extension() + .setUrl(MeasureConstants.POPULATION_BASIS_URL) + .setValue(populationBasisTypeForMeasure))); return measure; } @@ -677,8 +683,7 @@ private Library library(String cql) { private Patient john_doe() { Patient patient = new Patient(); patient.setId(new IdType("Patient", "john-doe")); - patient.setName( - List.of(new HumanName().setFamily("Doe").setGiven(List.of(new StringType("John"))))); + patient.setName(List.of(new HumanName().setFamily("Doe").setGiven(List.of(new StringType("John"))))); patient.setBirthDate(new Date()); patient.setGender(AdministrativeGender.MALE); @@ -706,8 +711,7 @@ private Patient john_doe() { private Patient jane_doe() { Patient patient = new Patient(); patient.setId(new IdType("Patient", "jane-doe")); - patient.setName( - List.of(new HumanName().setFamily("Doe").setGiven(List.of(new StringType("Jane"))))); + patient.setName(List.of(new HumanName().setFamily("Doe").setGiven(List.of(new StringType("Jane"))))); patient.setBirthDate(new Date()); patient.setGender(AdministrativeGender.FEMALE); @@ -728,55 +732,53 @@ private Patient jane_doe() { private RetrieveProvider setupMockRetrieverProvider() { RetrieveProvider retrieveProvider = mock(RetrieveProvider.class); when(retrieveProvider.retrieve( - isNull(), - isNull(), - isNull(), - eq("Patient"), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any())) - .thenReturn(Arrays.asList(jane_doe(), john_doe())); + isNull(), + isNull(), + isNull(), + eq("Patient"), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any())) + .thenReturn(Arrays.asList(jane_doe(), john_doe())); when(retrieveProvider.retrieve( - eq("Patient"), - eq("id"), - eq("john-doe"), - eq("Patient"), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any())) - .thenReturn(List.of(john_doe())); + eq("Patient"), + eq("id"), + eq("john-doe"), + eq("Patient"), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any())) + .thenReturn(List.of(john_doe())); when(retrieveProvider.retrieve( - eq("Patient"), - eq("id"), - eq("jane-doe"), - eq("Patient"), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any())) - .thenReturn(List.of(jane_doe())); + eq("Patient"), + eq("id"), + eq("jane-doe"), + eq("Patient"), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any())) + .thenReturn(List.of(jane_doe())); return retrieveProvider; } private void printResource(Resource resource) { - final String json = FhirContext.forR4Cached() - .newJsonParser() - .setPrettyPrint(true) - .encodeResourceToString(resource); + final String json = + FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true).encodeResourceToString(resource); System.out.println("json = \n" + json); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java index f62db30d7..64dd51a60 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java @@ -6,65 +6,94 @@ import ca.uhn.fhir.context.FhirContext; import com.google.common.collect.Lists; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.MeasureReport; -import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.*; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.api.Repository; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.opencds.cqf.fhir.utility.search.Searches; +// LUKETODO: https://www.hl7.org/fhir/R4/measure-operation-submit-data.html + +/* +The official URL for this operation definition is + + http://hl7.org/fhir/OperationDefinition/Measure-submit-data +Formal Definition (as a OperationDefinition). + +URL: [base]/Measure/$submit-data + +URL: [base]/Measure/[id]/$submit-data + +This is an idempotent operation + +In Parameters: + +| Name | Cardinality | Type | Binding | Profile | Documentation | +| --------------| ----------- | ------------- | ------- | ------- | -------------------------------------------------------------------------- | +| measureReport | 1..1 | MeasureReport | | | The measure report being submitted | +| resource | 0..* | Resource | | | The individual resources that make up the data-of-interest being submitted | + +The effect of invoking this operation is that the submitted data is posted to the receiving system and can be used for +subsequent calculation of the relevant quality measure. The data-of-interest for a measure can be determined by +examining the measure definition, or by invoking the $data-requirements operation + + */ class R4SubmitDataServiceTest { private static final FhirContext FHIR_CONTEXT = FhirContext.forR4(); + private static final String OBSERVATION_VALUE = "ABC"; + private static final String MEASURE_ID_COMPONENT = "A123"; + private static final String MEASURE_FULL_ID = "Measure/A123"; + private final Repository repository = new InMemoryFhirRepository(FhirContext.forR4Cached()); private final R4SubmitDataService testSubject = new R4SubmitDataService(repository); @Test - public void submitDataTest(){ + public void submitDataTest() { - //create resources - MeasureReport mr = newResource(MeasureReport.class).setMeasure("Measure/A123"); - Observation obs = newResource(Observation.class).setValue(new StringType("ABC")); + // create resources + var measureReport = newResource(MeasureReport.class).setMeasure(MEASURE_FULL_ID); + var observation = newResource(Observation.class).setValue(new StringType(OBSERVATION_VALUE)); - //submit-data operation - var res = testSubject - .submitData(new IdType("Measure", "A123"), mr, - Lists.newArrayList(obs)); + // submit-data operation + var result = testSubject.submitData( + new IdType(ResourceType.Measure.toString(), MEASURE_ID_COMPONENT), + measureReport, + Lists.newArrayList(observation)); + + assertNotNull(result); var resultMr = repository.search(Bundle.class, MeasureReport.class, Searches.ALL); var mrSize = resultMr.getEntry().size(); MeasureReport report = null; - for (int i = 0; i < mrSize; i++){ + for (int i = 0; i < mrSize; i++) { var getEntry = resultMr.getEntry(); var mrResource = (MeasureReport) getEntry.get(i).getResource(); var measure = mrResource.getMeasure(); - if (measure.equals("Measure/A123")){ + if (MEASURE_FULL_ID.equals(measure)) { report = mrResource; break; } } - //found submitted MeasureReport! + + // found submitted MeasureReport! assertNotNull(report); var resultOb = repository.search(Bundle.class, Observation.class, Searches.ALL); var obSize = resultOb.getEntry().size(); - Observation observation = null; - for (int i = 0; i < obSize; i++){ + Observation observationFromRepo = null; + for (int i = 0; i < obSize; i++) { var getEntry = resultOb.getEntry(); var obResource = (Observation) getEntry.get(i).getResource(); var val = obResource.getValue().primitiveValue(); - if (val.equals("ABC")){ - observation = obResource; + if (OBSERVATION_VALUE.equals(val)) { + observationFromRepo = obResource; break; } } - //found submitted Observation! - assertNotNull(observation); - + // found submitted Observation! + assertNotNull(observationFromRepo); } @SuppressWarnings("unchecked") @@ -73,4 +102,4 @@ private T newResource(Class theResourceClass) { return (T) FHIR_CONTEXT.getResourceDefinition(theResourceClass).newInstance(); } -} \ No newline at end of file +} From 673a5e7a4115a83c91fa24a186b55fe07cccfbc3 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Fri, 29 Nov 2024 17:01:47 -0500 Subject: [PATCH 3/4] Organize TODOs and more test code. --- .../cr/measure/common/MeasureEvaluator.java | 2 +- .../cr/measure/constant/MeasureConstants.java | 4 +- .../cr/measure/r4/R4MeasureReportBuilder.java | 14 +- .../cr/measure/r4/R4MeasureReportScorer.java | 142 +++++++++------- .../cr/measure/r4/R4SubmitDataService.java | 11 +- .../fhir/cr/measure/r4/MeasureScorerTest.java | 10 +- .../measure/r4/R4MeasureEvaluationTest.java | 4 +- .../measure/r4/R4SubmitDataServiceTest.java | 152 +++++++++++------- 8 files changed, 203 insertions(+), 136 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index f44a84ab2..0a805d618 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -598,7 +598,7 @@ private void someEvaluationValidationStuff(GroupDef groupDef, EvaluationResult e final List> criteriaResults = stratifiers.stream().map(StratifierDef::getResults).collect(Collectors.toList()); - // LUKETODO: LEFT side of the comparison is the groupDefPopulationBasis + // LUKETODO: 600 LEFT side of the comparison is the groupDefPopulationBasis logger.info("expressionResults: {}, groupDefPopulationBasis: {}", expressionResults, groupDefPopulationBasis); for (Map criteriaResult : criteriaResults) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java index 15cec4f9b..7b5abf97d 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/constant/MeasureConstants.java @@ -43,11 +43,11 @@ private MeasureConstants() {} public static final String FHIR_ALL_TYPES_SYSTEM_URL = "http://hl7.org/fhir/fhir-types"; public static final String POPULATION_BASIS_URL = "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis"; - // LUKETODO: get rid of this: + // LUKETODO: 602 get rid of this: @Deprecated public static final String EXT_TOTAL_DENOMINATOR_URL = "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-cqfm-denominator-membership"; - // LUKETODO: get rid of this: + // LUKETODO: 602 get rid of this: @Deprecated public static final String EXT_TOTAL_NUMERATOR_URL = "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-cqfm-numerator-membership"; diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java index 52985a500..f4caa4681 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java @@ -346,15 +346,15 @@ protected void buildGroup( } if (groupDef.isBooleanBasis()) { - // LUKETODO: why doesn't this consider exclusions? - // LUKETODO: do we add this WITH or WITHOUT exclusions? my guess is WITHOUT + // LUKETODO: 602 why doesn't this consider exclusions? + // LUKETODO: 602 do we add this WITH or WITHOUT exclusions? my guess is WITHOUT addExtension( reportGroup, EXT_TOTAL_DENOMINATOR_URL, getReportPopulation(groupDef, TOTALDENOMINATOR), true); addExtension(reportGroup, EXT_TOTAL_NUMERATOR_URL, getReportPopulation(groupDef, TOTALNUMERATOR), true); } else { - // LUKETODO: why doesn't this consider exclusions? - // LUKETODO: do we add this WITH or WITHOUT exclusions? my guess is WITHOUT + // LUKETODO: 602 why doesn't this consider exclusions? + // LUKETODO: 602 do we add this WITH or WITHOUT exclusions? my guess is WITHOUT addExtension( reportGroup, EXT_TOTAL_DENOMINATOR_URL, getReportPopulation(groupDef, TOTALDENOMINATOR), false); addExtension( @@ -381,7 +381,7 @@ protected void addExtension( group.addExtension().setUrl(extUrl).setValue(new StringType(Integer.toString(count))); } - // LUKETODO: is there such a thing as validation for group population basis? + // LUKETODO: 602 is there such a thing as validation for group population basis? /** * @@ -396,7 +396,7 @@ protected void validateStratifierBasisType(Map subjectVa .filter(x -> x.rawValue() instanceof Resource) .collect(Collectors.toList()); if (list.size() != subjectValues.values().size()) { - // LUKETODO: this is the stratifier case + // LUKETODO: 602 this is the stratifier case throw new IllegalArgumentException( "stratifier expression criteria results must match the same type as population."); } @@ -504,7 +504,7 @@ protected void buildStratum( } // add totalDenominator and totalNumerator extensions - // LUKETODO: + // LUKETODO: 602 buildStratumExtPopulation(groupDef, TOTALDENOMINATOR, subjectIds, stratum, EXT_TOTAL_DENOMINATOR_URL); buildStratumExtPopulation(groupDef, TOTALNUMERATOR, subjectIds, stratum, EXT_TOTAL_NUMERATOR_URL); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java index fc645897b..a0c64cb4c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java @@ -10,6 +10,7 @@ import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupStratifierComponent; import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupComponent; +import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupPopulationComponent; import org.hl7.fhir.r4.model.Quantity; import org.opencds.cqf.fhir.cr.measure.common.BaseMeasureReportScorer; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; @@ -114,68 +115,70 @@ protected MeasureScoring getGroupMeasureScoring(MeasureReportGroupComponent mrgc protected void scoreGroup( MeasureScoring measureScoring, MeasureReportGroupComponent mrgc, boolean isIncreaseImprovementNotation) { - switch (measureScoring) { - case PROPORTION: - case RATIO: - // LUKETODO: should this be from the MeasureReportGroupComponent or the GroupDef? + // LUKETODO: 602 should this be from the MeasureReportGroupComponent or the GroupDef? /* MeasureScorer should now look for Numerator/Denominator values to make the calculation instead of the extension values. Any code that sets or maintains these populations can be removed Testing classes that have assertions for these values should be deprecated */ - final List populations = mrgc.getPopulation(); - - // LUKETODO: I think we need to do the depopulations.stream() - final List numerators = populations.stream() - .filter(population -> "numerator" - .equals(population.getCode().getCodingFirstRep().getCode())) - .collect(Collectors.toList()); - - final List denominators = populations.stream() - .filter(population -> "numerator" - .equals(population.getCode().getCodingFirstRep().getCode())) - .collect(Collectors.toList()); - - final Integer populationNumeratorCount = populations.stream() - .filter(population -> "numerator" - .equals(population.getCode().getCodingFirstRep().getCode())) - .map(MeasureReportGroupPopulationComponent::getCount) - .findAny() - .orElse(0); - - final Integer populationDenominatorCount = populations.stream() - .filter(population -> "denominator" - .equals(population.getCode().getCodingFirstRep().getCode())) - .map(MeasureReportGroupPopulationComponent::getCount) - .findAny() - .orElse(0); - logger.info( - "populationNumeratorCount: {}, populationDenominatorCount: {}", - populationNumeratorCount, - populationDenominatorCount); - System.out.println("populationNumeratorCount = " + populationNumeratorCount); - System.out.println("populationDenominatorCount = " + populationDenominatorCount); - - final Integer extNumeratorCount = getGroupExtensionCount(mrgc, EXT_TOTAL_NUMERATOR_URL); - final Integer extDenominatorCount = getGroupExtensionCount(mrgc, EXT_TOTAL_DENOMINATOR_URL); - - logger.info("extNumeratorCount: {}, extDenominatorCount: {}", extNumeratorCount, extDenominatorCount); - System.out.println("extNumeratorCount= " + populationNumeratorCount); - System.out.println("extDenominatorCount= " + extDenominatorCount); - - // LUKETODO: - // if (!Objects.equals(extNumeratorCount, populationNumeratorCount)) { - // throw new IllegalStateException("numerator counts don't match: ext:" + - // extNumeratorCount + " != population:" + populationNumeratorCount); - // } - // - // if (!Objects.equals(extDenominatorCount, populationDenominatorCount)) { - // throw new IllegalStateException("denominator counts don't match: ext:" + - // extDenominatorCount + " != population:" + populationDenominatorCount); - // } - - Double score = this.calcProportionScore(extNumeratorCount, extDenominatorCount); - // LUKETODO: + final List populations = mrgc.getPopulation(); + + // LUKETODO: 602 I think we need to do the populations.stream() + final List numerators = populations.stream() + .filter(population -> "numerator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .collect(Collectors.toList()); + + final List denominators = populations.stream() + .filter(population -> "numerator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .collect(Collectors.toList()); + + final Integer populationNumeratorCount = populations.stream() + .filter(population -> "numerator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .map(MeasureReportGroupPopulationComponent::getCount) + .findAny() + .orElse(0); + + final Integer populationDenominatorCount = populations.stream() + .filter(population -> "denominator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .map(MeasureReportGroupPopulationComponent::getCount) + .findAny() + .orElse(0); + logger.info( + "populationNumeratorCount: {}, populationDenominatorCount: {}", + populationNumeratorCount, + populationDenominatorCount); + System.out.println("populationNumeratorCount = " + populationNumeratorCount); + System.out.println("populationDenominatorCount = " + populationDenominatorCount); + + final Integer extNumeratorCount = getGroupExtensionCount(mrgc, EXT_TOTAL_NUMERATOR_URL); + final Integer extDenominatorCount = getGroupExtensionCount(mrgc, EXT_TOTAL_DENOMINATOR_URL); + + logger.info("extNumeratorCount: {}, extDenominatorCount: {}", extNumeratorCount, extDenominatorCount); + System.out.println("extNumeratorCount= " + populationNumeratorCount); + System.out.println("extDenominatorCount= " + extDenominatorCount); + + // LUKETODO: 602 + // if (!Objects.equals(extNumeratorCount, populationNumeratorCount)) { + // throw new IllegalStateException("numerator counts don't match: ext:" + + // extNumeratorCount + " != population:" + populationNumeratorCount); + // } + // + // if (!Objects.equals(extDenominatorCount, populationDenominatorCount)) { + // throw new IllegalStateException("denominator counts don't match: ext:" + + // extDenominatorCount + " != population:" + populationDenominatorCount); + // } + switch (measureScoring) { + case PROPORTION: + case RATIO: + + // LUKETODO: 602: do we need a fix for the PRODUCTION WRITE???? + // LUKETODO: 602: this is the actual fix for the READ +// Double score = this.calcProportionScore(extNumeratorCount, extDenominatorCount); + Double score = this.calcProportionScore(populationNumeratorCount, populationDenominatorCount); if (score != null) { if (isIncreaseImprovementNotation) { mrgc.setMeasureScore(new Quantity(score)); @@ -188,6 +191,7 @@ protected void scoreGroup( break; } + // LUKETODO: 602: pass down populationNumerator and denominator to scoreStratifier() ??? for (MeasureReportGroupStratifierComponent stratifierComponent : mrgc.getStratifier()) { scoreStratifier(measureScoring, stratifierComponent); } @@ -197,7 +201,29 @@ protected void scoreStratum(MeasureScoring measureScoring, StratifierGroupCompon switch (measureScoring) { case PROPORTION: case RATIO: - // LUKETODO: + // LUKETODO: 602: use this instead of the MeasureGroup Population???????? + final List populations = stratum.getPopulation(); + + final Integer stratumPopulationNumeratorCount = populations.stream() + .filter(population -> "numerator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .map(StratifierGroupPopulationComponent::getCount) + .findAny() + .orElse(0); + + final Integer stratumPopulationDenominatorCount = populations.stream() + .filter(population -> "denominator" + .equals(population.getCode().getCodingFirstRep().getCode())) + .map(StratifierGroupPopulationComponent::getCount) + .findAny() + .orElse(0); + logger.info( + "stratumPopulationNumeratorCount: {}, stratumPopulationDenominatorCount: {}", + stratumPopulationNumeratorCount, + stratumPopulationDenominatorCount); + System.out.println("stratumPopulationNumeratorCount = " + stratumPopulationNumeratorCount); + System.out.println("stratumPopulationDenominatorCount = " + stratumPopulationDenominatorCount); + Double score = this.calcProportionScore( getStratumPopulationCount(stratum, EXT_TOTAL_NUMERATOR_URL), getStratumPopulationCount(stratum, EXT_TOTAL_DENOMINATOR_URL)); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java index 333ed200a..b69c08fb8 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java @@ -10,13 +10,16 @@ public class R4SubmitDataService { - // LUKETODO: test this + // LUKETODO: 601 test this private final Repository repository; public R4SubmitDataService(Repository repository) { this.repository = repository; } + // LUKETODO: 601 what's the point of the id in the GET String? Is this a measure ID? If so, what to do with it? + // URL: [base]/Measure/[id]/$submit-data + /** * Save measure report and resources to the local repository * @@ -26,10 +29,14 @@ public R4SubmitDataService(Repository repository) { * @return Bundle transaction result */ public Bundle submitData(IdType id, MeasureReport report, List resources) { + // LUKETODO: 601 /* * TODO - resource validation using $data-requirements operation (params are the provided id and * the measurement period from the MeasureReport) - * + */ + + // LUKETODO: 601 + /* * TODO - profile validation ... not sure how that would work ... (get StructureDefinition from * URL or must it be stored in Ruler?) */ diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java index 58eac4fca..fd800635d 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScorerTest.java @@ -52,8 +52,8 @@ void scorerThrowsIfNoScoringSupplied() { } } - // LUKETODO: these counts are off each other: - // LUKETODO: is this just because of bad test data, or are the counts really different? + // LUKETODO: 602 these counts are off each other: + // LUKETODO: 602 is this just because of bad test data, or are the counts really different? /* populationNumeratorCount = 1 populationDenominatorCount = 1 @@ -69,7 +69,7 @@ void scorerThrowsIfNoScoringSupplied() { extDenominatorCount= 2 */ - // LUKETODO: fix counts in JSONs + // LUKETODO: 602 fix counts in JSONs @Test void scorePopulationIdMultiRate() { var measureUrl = "http://ecqi.healthit.gov/ecqms/Measure/FHIR347"; @@ -118,7 +118,7 @@ void scoreZeroDenominator() { assertNull(group(measureReport, "DataCompleteness").getMeasureScore().getValue()); } - // LUKETODO: fix counts in JSONs + // LUKETODO: 602 fix counts in JSONs @Test void scoreNoExtension() { var measureUrl = "http://content.alphora.com/fhir/uv/mips-qm-content-r4/Measure/multirate-noext"; @@ -131,7 +131,7 @@ void scoreNoExtension() { assertNull(group(measureReport, "PerformanceRate").getMeasureScore().getValue()); } - // LUKETODO: these counts are in sync: + // LUKETODO: 602 these counts are in sync: /* populationNumeratorCount = 5 populationDenominatorCount = 10 diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java index 44cdff342..c7ce0556a 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java @@ -255,7 +255,7 @@ void continuousVariableMeasureEvaluation() { checkEvidence(report); } - // LUKETODO: cleanup: + // LUKETODO: 600 cleanup: @Language("JSON") final String json = """ @@ -385,7 +385,7 @@ private static Stream stratifiedMeasureEvaluationByPopulationBasisErr Arguments.of(POPULATION_BASIS_ENCOUNTER, POPULATION_BASIS_ENCOUNTER)); } - // LUKETODO: can I just shove "SDE Race" into the Numerator expression and call it a day? + // LUKETODO: 600 can I just shove "SDE Race" into the Numerator expression and call it a day? @ParameterizedTest @MethodSource("stratifiedMeasureEvaluationByPopulationBasisErrorPathParams") void stratifiedMeasureEvaluationByPopulationErrorPathBasis( diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java index 64dd51a60..951326e63 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataServiceTest.java @@ -1,10 +1,15 @@ package org.opencds.cqf.fhir.cr.measure.r4; import static com.google.common.base.Preconditions.checkNotNull; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertNotNull; import ca.uhn.fhir.context.FhirContext; -import com.google.common.collect.Lists; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.List; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.*; import org.junit.jupiter.api.Test; @@ -12,32 +17,6 @@ import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.opencds.cqf.fhir.utility.search.Searches; -// LUKETODO: https://www.hl7.org/fhir/R4/measure-operation-submit-data.html - -/* -The official URL for this operation definition is - - http://hl7.org/fhir/OperationDefinition/Measure-submit-data -Formal Definition (as a OperationDefinition). - -URL: [base]/Measure/$submit-data - -URL: [base]/Measure/[id]/$submit-data - -This is an idempotent operation - -In Parameters: - -| Name | Cardinality | Type | Binding | Profile | Documentation | -| --------------| ----------- | ------------- | ------- | ------- | -------------------------------------------------------------------------- | -| measureReport | 1..1 | MeasureReport | | | The measure report being submitted | -| resource | 0..* | Resource | | | The individual resources that make up the data-of-interest being submitted | - -The effect of invoking this operation is that the submitted data is posted to the receiving system and can be used for -subsequent calculation of the relevant quality measure. The data-of-interest for a measure can be determined by -examining the measure definition, or by invoking the $data-requirements operation - - */ class R4SubmitDataServiceTest { private static final FhirContext FHIR_CONTEXT = FhirContext.forR4(); @@ -46,54 +25,109 @@ class R4SubmitDataServiceTest { private static final String MEASURE_ID_COMPONENT = "A123"; private static final String MEASURE_FULL_ID = "Measure/A123"; + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); + private static final String PATIENT_ID = "Practitioner-2178"; + private static final String ENCOUNTER_ID = "Encounter-62912"; + private static final String PROCEDURE_ID = "Procedure-89972"; + private final Repository repository = new InMemoryFhirRepository(FhirContext.forR4Cached()); private final R4SubmitDataService testSubject = new R4SubmitDataService(repository); @Test - public void submitDataTest() { + public void submitDataSimple() { // create resources var measureReport = newResource(MeasureReport.class).setMeasure(MEASURE_FULL_ID); var observation = newResource(Observation.class).setValue(new StringType(OBSERVATION_VALUE)); + // submit-data operation + var result = testSubject.submitData( + new IdType(ResourceType.Measure.toString(), MEASURE_ID_COMPONENT), measureReport, List.of(observation)); + + assertNotNull(result); + + var resultMeasureReport = getOnlyResourceFromSearch(MeasureReport.class); + assertThat(resultMeasureReport.getMeasure(), equalTo(MEASURE_FULL_ID)); + + var resultObservation = getOnlyResourceFromSearch(Observation.class); + assertThat(resultObservation.getValue().primitiveValue(), equalTo(OBSERVATION_VALUE)); + } + + @Test + public void submitDataMedium() throws ParseException { + + // create resources + var patient = newResource(Patient.class).setId(PATIENT_ID); + + var encounter = newResource(Encounter.class) + .setPeriod(new Period() + .setStart(DATE_FORMAT.parse("2018-05-29T11:00:00-04:00")) + .setEnd(DATE_FORMAT.parse("2018-05-29T11:00:00-04:00"))) + .setSubject(new Reference(patient.getId())) + .setId(ENCOUNTER_ID); + + var procedure = newResource(Procedure.class) + .setSubject(new Reference(patient.getId())) + .setPerformed(new Period() + .setStart(DATE_FORMAT.parse("2018-06-02T14:00:00-05:00")) + .setEnd(DATE_FORMAT.parse("2018-06-02T14:00:00-05:00"))) + .setId(PROCEDURE_ID); + + var measureReport = newResource(MeasureReport.class) + .setMeasure(MEASURE_FULL_ID) + .setPeriod(new Period() + .setStart(DATE_FORMAT.parse("2017-01-01T00:00:00+00:00")) + .setEnd(DATE_FORMAT.parse("2017-12-31T00:00:00+00:00"))) + .addEvaluatedResource(new Reference(patient.getId())) + .addEvaluatedResource(new Reference(encounter.getId())) + .addEvaluatedResource(new Reference(procedure.getId())); + // submit-data operation var result = testSubject.submitData( new IdType(ResourceType.Measure.toString(), MEASURE_ID_COMPONENT), measureReport, - Lists.newArrayList(observation)); + List.of(patient, encounter, procedure)); assertNotNull(result); - var resultMr = repository.search(Bundle.class, MeasureReport.class, Searches.ALL); - var mrSize = resultMr.getEntry().size(); - MeasureReport report = null; - for (int i = 0; i < mrSize; i++) { - var getEntry = resultMr.getEntry(); - var mrResource = (MeasureReport) getEntry.get(i).getResource(); - var measure = mrResource.getMeasure(); - if (MEASURE_FULL_ID.equals(measure)) { - report = mrResource; - break; - } - } - - // found submitted MeasureReport! - assertNotNull(report); - - var resultOb = repository.search(Bundle.class, Observation.class, Searches.ALL); - var obSize = resultOb.getEntry().size(); - Observation observationFromRepo = null; - for (int i = 0; i < obSize; i++) { - var getEntry = resultOb.getEntry(); - var obResource = (Observation) getEntry.get(i).getResource(); - var val = obResource.getValue().primitiveValue(); - if (OBSERVATION_VALUE.equals(val)) { - observationFromRepo = obResource; - break; - } - } - // found submitted Observation! - assertNotNull(observationFromRepo); + var resultMeasureReport = getOnlyResourceFromSearch(MeasureReport.class); + assertThat(resultMeasureReport.getMeasure(), equalTo(MEASURE_FULL_ID)); + assertThat( + resultMeasureReport.getEvaluatedResource().stream() + .map(Reference::getReference) + .toList(), + containsInAnyOrder(PATIENT_ID, ENCOUNTER_ID, PROCEDURE_ID)); + + var resultPatient = getOnlyResourceFromSearch(Patient.class); + assertThat(resultPatient.getId(), equalTo(PATIENT_ID)); + + var resultEncounter = getOnlyResourceFromSearch(Encounter.class); + assertThat(resultEncounter.getSubject().getReference(), equalTo(PATIENT_ID)); + + var resultProcedure = getOnlyResourceFromSearch(Procedure.class); + assertThat(resultProcedure.getSubject().getReference(), equalTo(PATIENT_ID)); + } + + private T getOnlyResourceFromSearch(Class clazz) { + final List resourcesFromSearch = getResourcesFromSearch(clazz); + + assertThat(resourcesFromSearch.size(), equalTo(1)); + + final T resource = resourcesFromSearch.get(0); + + assertThat(resource, notNullValue()); + + return resource; + } + + private List getResourcesFromSearch(Class clazz) { + var bundle = repository.search(Bundle.class, clazz, Searches.ALL); + + return bundle.getEntry().stream() + .map(Bundle.BundleEntryComponent::getResource) + .filter(clazz::isInstance) + .map(clazz::cast) + .toList(); } @SuppressWarnings("unchecked") From a03d6be936600e35970ebfec0a0bedf075205595 Mon Sep 17 00:00:00 2001 From: Luke deGruchy Date: Mon, 2 Dec 2024 13:56:06 -0500 Subject: [PATCH 4/4] More changes. --- .../cr/measure/r4/R4SubmitDataService.java | 28 ++++++++++++------- .../measure/r4/R4MeasureEvaluationTest.java | 19 +++++++++---- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java index b69c08fb8..2644bacf1 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4SubmitDataService.java @@ -4,37 +4,39 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; +import org.hl7.fhir.r4.model.Period; import org.hl7.fhir.r4.model.Resource; import org.opencds.cqf.fhir.api.Repository; public class R4SubmitDataService { - // LUKETODO: 601 test this private final Repository repository; public R4SubmitDataService(Repository repository) { this.repository = repository; } - // LUKETODO: 601 what's the point of the id in the GET String? Is this a measure ID? If so, what to do with it? - // URL: [base]/Measure/[id]/$submit-data - /** * Save measure report and resources to the local repository * - * @param id - * @param report - * @param resources + * @param measureId ??? + * @param report The measure report being submitted + * @param resources The individual resources that make up the data-of-interest being submitted * @return Bundle transaction result */ - public Bundle submitData(IdType id, MeasureReport report, List resources) { - // LUKETODO: 601 + public Bundle submitData(IdType measureId, MeasureReport report, List resources) { /* - * TODO - resource validation using $data-requirements operation (params are the provided id and + * TODO - resource validation using $data-requirements operation (params are the provided measureId and * the measurement period from the MeasureReport) */ + var measureFromDb = repository.read(Measure.class, measureId); + var measureReportPeriod = report.getPeriod(); + + validateResource(measureFromDb, measureReportPeriod); + // LUKETODO: 601 /* * TODO - profile validation ... not sure how that would work ... (get StructureDefinition from @@ -47,6 +49,7 @@ public Bundle submitData(IdType id, MeasureReport report, List re if (resources != null) { for (IBaseResource res : resources) { // Unpack nested Bundles + // TODO: LD: replace with pattern variable once animal sniffer allows it if (res instanceof Bundle) { Bundle nestedBundle = (Bundle) res; for (Bundle.BundleEntryComponent entry : nestedBundle.getEntry()) { @@ -60,6 +63,11 @@ public Bundle submitData(IdType id, MeasureReport report, List re return repository.transaction(transactionBundle); } + private void validateResource(Measure measureFromDb, Period measureReportPeriod) { + // LUKETODO: 601 ??? + // LUKETODO: 601 unit tests for any cases that fail validation + } + private Bundle.BundleEntryComponent createEntry(IBaseResource resource) { return new Bundle.BundleEntryComponent() .setResource((Resource) resource) diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java index c7ce0556a..8b7672c3d 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureEvaluationTest.java @@ -385,18 +385,24 @@ private static Stream stratifiedMeasureEvaluationByPopulationBasisErr Arguments.of(POPULATION_BASIS_ENCOUNTER, POPULATION_BASIS_ENCOUNTER)); } + // LUKETODO: 600 how do I set up this test to prove that GroupDef criteria expressions are properly validated? // LUKETODO: 600 can I just shove "SDE Race" into the Numerator expression and call it a day? @ParameterizedTest @MethodSource("stratifiedMeasureEvaluationByPopulationBasisErrorPathParams") void stratifiedMeasureEvaluationByPopulationErrorPathBasis( @Nullable CodeType populationBasisTypeForMeasure, @Nullable CodeType populationBasisTypeForGroup) { - final String cql = cql_with_dateTime() + sde_race() - + "define InitialPopulation: 'Doe' in Patient.name.family\n" - + "define Denominator: 'John' in Patient.name.given\n" - + "define Numerator: Patient.birthDate > @1970-01-01\n" + "define Gender: Patient.gender\n"; + final String cql = cql_with_dateTime() + sde_race() + + """ + define InitialPopulation: 'Doe' in Patient.name.family + define Denominator: 'John' in Patient.name.given + define Numerator: Patient.birthDate > @1970-01-01 + define Gender: Patient.gender + """; + // LUKETODO: cleanup System.out.println("cql = \n" + cql); + // LUKETODO: cleanup var x = """ library Test version '1.0.0' @@ -425,7 +431,7 @@ void stratifiedMeasureEvaluationByPopulationErrorPathBasis( try { runTest( cql, - Arrays.asList(jane_doe().getId(), john_doe().getId()), + List.of(jane_doe().getId(), john_doe().getId()), stratified_measure(populationBasisTypeForMeasure, populationBasisTypeForGroup), setupMockRetrieverProvider(), null); @@ -588,7 +594,8 @@ private Measure proportion_measure( Measure measure = measure("proportion", populationBasisTypeForMeasure, populationBasisTypeForGroup); addPopulation(measure, MeasurePopulationType.INITIALPOPULATION, "InitialPopulation"); addPopulation(measure, MeasurePopulationType.DENOMINATOR, "Denominator"); - addPopulation(measure, MeasurePopulationType.NUMERATOR, "Numerator"); +// addPopulation(measure, MeasurePopulationType.NUMERATOR, "Numerator"); + addPopulation(measure, MeasurePopulationType.NUMERATOR, "SDE Race"); addSDEComponent(measure); return measure;