From e99737ecba18dc831951b07797e448d83dd14749 Mon Sep 17 00:00:00 2001 From: Justin McKelvy <60718638+Capt-Mac@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:41:37 -0600 Subject: [PATCH] fix not-applicable status for care-gaps (#574) * fix na status for care-gaps * remove duplicate identifier --- .../measure/r4/R4CareGapStatusEvaluator.java | 62 +++++---- .../fhir/cr/measure/r4/R4CareGapsTest.java | 24 ++++ ...MinimalProportionDenominatorExclusion.json | 118 ++++++++++++++++++ 3 files changed, 172 insertions(+), 32 deletions(-) create mode 100644 cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/resources/measure/MinimalProportionDenominatorExclusion.json diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapStatusEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapStatusEvaluator.java index d19d09a8a..c4b0ae8d9 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapStatusEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapStatusEvaluator.java @@ -23,34 +23,25 @@ */ public class R4CareGapStatusEvaluator { /** - *

- * GapStatus is determined by interpreting a MeasureReport resource of Type Ratio or Proportion that contain the populations: Numerator & Denominator - *

- *

- *

- *

- *

- * Improvement Notation of Scoring Algorithm indicates whether the ratio of Numerator over Denominator populations represents a scenario to increase the Numerator to improve outcomes, or to decrease the Numerator count. If this value is not set on a Measure resource, then it is defaulted to 'Increase' under the IsPositive variable. - *

- * - * - *

'prospective-gap' is a concept that represents a period of time where a 'care-gap' measure has opportunity to address recommended care in a specific window of time. This 'window of time' we call the 'Date of Compliance' to indicate a range of time that optimally represents when care is meant to be provided.

- *
- *

If care has not been provided ('open-gap'), and the date (reportDate) of evaluating for the Measure is before or within the 'Date of Compliance' interval, then the Measure is considered a 'prospective-gap' for the subject evaluated.

+ * Below table is a mapping of expected behavior of the care-gap-status to Measure Report score + * | Gap-Status | initial-population | denominator | denominator-exclusion | denominator-exception\*\* | numerator-exclusion | numerator | Improvement Notation\*\*\* | + * | ----------------- | ------------------ | ----------- | --------------------- | ------------------------- | ------------------- | --------- | -------------------------- | + * | not-applicable | FALSE | N/A | N/A | N/A | N/A | N/A | N/A | + * | closed-gap | TRUE | FALSE | FALSE | FALSE | FALSE | FALSE | N/A | + * | closed-gap | TRUE | FALSE | TRUE | FALSE | FALSE | FALSE | N/A | + * | closed-gap | TRUE | FALSE | FALSE | TRUE | FALSE | FALSE | N/A | + * | prospective-gap\* | TRUE | TRUE | FALSE | FALSE | FALSE | FALSE | N/A | + * | prospective-gap\* | TRUE | TRUE | FALSE | FALSE | TRUE | FALSE | N/A | + * | open-gap | TRUE | TRUE | FALSE | FALSE | TRUE | FALSE | increase | + * | open-gap | TRUE | TRUE | FALSE | FALSE | FALSE | FALSE | increase | + * | open-gap | TRUE | TRUE | FALSE | FALSE | FALSE | TRUE | decrease | + * | closed-gap | TRUE | TRUE | FALSE | FALSE | TRUE | FALSE | decrease | + * | closed-gap | TRUE | TRUE | FALSE | FALSE | FALSE | TRUE | increase | + * | closed-gap | TRUE | TRUE | FALSE | FALSE | FALSE | FALSE | decrease | * - *

+ * *`prospective-gap` status requires additional data points than just population-code values within a MeasureReport in order to determine if ‘prospective-gap’ or just ‘open-gap’. + * **denominator-exception: is only for ‘proportion’ scoring type. + * ***improvement Notation: is a Measure defined value that tells users how to view results of the Measure Report. */ public Map getGroupGapStatus(Measure measure, MeasureReport measureReport) { Map groupStatus = new HashMap<>(); @@ -67,10 +58,18 @@ public Map getGroupGapStatus(Measure measure, Measur private CareGapsStatusCode getGapStatus( Measure measure, MeasureReportGroupComponent measureReportGroup, DateTimeType reportDate) { + Pair inInitialPopulation = new MutablePair<>("initial-population", false); Pair inNumerator = new MutablePair<>("numerator", false); Pair inDenominator = new MutablePair<>("denominator", false); // get Numerator and Denominator membership measureReportGroup.getPopulation().forEach(population -> { + if (population.hasCode() + && population + .getCode() + .hasCoding(MEASUREREPORT_MEASURE_POPULATION_SYSTEM, inInitialPopulation.getKey()) + && population.getCount() == 1) { + inInitialPopulation.setValue(true); + } if (population.hasCode() && population.getCode().hasCoding(MEASUREREPORT_MEASURE_POPULATION_SYSTEM, inNumerator.getKey()) && population.getCount() == 1) { @@ -83,6 +82,10 @@ private CareGapsStatusCode getGapStatus( } }); + // is subject eligible for measure? + if (Boolean.FALSE.equals(inInitialPopulation.getValue())) { + return CareGapsStatusCode.NOT_APPLICABLE; + } // default improvementNotation boolean isPositive = true; @@ -94,11 +97,6 @@ private CareGapsStatusCode getGapStatus( .hasCoding(MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM, IMPROVEMENT_NOTATION_SYSTEM_INCREASE); } - if (Boolean.FALSE.equals(inDenominator.getValue())) { - // patient is not in eligible population - return CareGapsStatusCode.NOT_APPLICABLE; - } - if (Boolean.TRUE.equals(inDenominator.getValue()) && ((isPositive && !inNumerator.getValue()) || (!isPositive && inNumerator.getValue()))) { return getOpenOrProspectiveStatus(measureReportGroup, reportDate); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsTest.java index dc92e1f2d..6b559fce5 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsTest.java @@ -85,6 +85,7 @@ void exm125_careGaps_openGap() { @Test void exm125_careGaps_NA() { + // validates initial-population=false returns 'not-applicable' status given.when() .subject("Patient/neg-denom-EXM125") .periodStart(LocalDate.of(2019, Month.JANUARY, 1).atStartOfDay().atZone(ZoneId.systemDefault())) @@ -759,4 +760,27 @@ void noMeasureSpecified() { assertTrue(e.getMessage().contains("no measure resolving parameter was specified")); } } + // MinimalProportionDenominatorExclusion + @Test + void InInitalPopulationAndDenominatorExclusion() { + // when subject is in initial population + // not in Denominator + // they should produce 'closed-gap' + GIVEN_REPO + .when() + .subject("Patient/female-1988") + .periodStart(LocalDate.of(2019, Month.JANUARY, 1).atStartOfDay().atZone(ZoneId.systemDefault())) + .periodEnd(LocalDate.of(2019, Month.DECEMBER, 31).atStartOfDay().atZone(ZoneId.systemDefault())) + .measureId("MinimalProportionDenominatorExclusion") + .measureIdentifier(null) + .measureUrl(null) + .status("closed-gap") + .getCareGapsReport() + .then() + .hasBundleCount(1) + .firstParameter() + .detectedIssueCount(1) + .detectedIssue() + .hasCareGapStatus("closed-gap"); + } } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/resources/measure/MinimalProportionDenominatorExclusion.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/resources/measure/MinimalProportionDenominatorExclusion.json new file mode 100644 index 000000000..114d3b216 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MinimalMeasureEvaluation/resources/measure/MinimalProportionDenominatorExclusion.json @@ -0,0 +1,118 @@ +{ + "id": "MinimalProportionDenominatorExclusion", + "resourceType": "Measure", + "url": "http://example.com/Measure/MinimalProportionDenominatorExclusion", + "library": [ + "http://example.com/Library/MinimalProportionBooleanBasisSingleGroup" + ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "proportion" + } + ] + }, + "group": [ + { + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "always true" + } + }, + { + "id": "denominator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator", + "display": "Denominator" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "always false" + } + }, + { + "id": "denominator-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator-exclusion", + "display": "Denominator-Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "always true" + } + }, + { + "id": "denominator-exception", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator-exception", + "display": "Denominator-Exception" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "always false" + } + }, + { + "id": "numerator-exclusion", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "numerator-exclusion", + "display": "Numerator-Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "always false" + } + }, + { + "id": "numerator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "numerator", + "display": "Numerator" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "always false" + } + } + ] + } + ] +} \ No newline at end of file