Skip to content

Commit

Permalink
Rel 3.13.1 mergeback (#577)
Browse files Browse the repository at this point in the history
* Fix $populate issue with missing responseItems for a null population context (#573)

* Allow for an empty context response

* add comment

* fix not-applicable status for care-gaps (#574)

* fix na status for care-gaps

* remove duplicate identifier

---------

Co-authored-by: Brenin Rhodes <brenin@alphora.com>
Co-authored-by: Justin McKelvy <60718638+Capt-Mac@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 4, 2024
1 parent fc64604 commit 6533d6a
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,25 @@
*/
public class R4CareGapStatusEvaluator {
/**
* <p>
* GapStatus is determined by interpreting a MeasureReport resource of Type Ratio or Proportion that contain the populations: Numerator & Denominator
* </p>
*<p>
* <ul>
* <li>'not-applicable': When a subject does not meet the criteria for the Measure scenario, whether by exclusion or exception criteria, or just by not meeting any required criteria, they will not show membership results in the 'Denominator'.</li>
* <li> subject is applicable (not a status): When a subject meets the criteria for a Measure they will have membership results in the 'Denominator', indicating they are of the appropriate criteria for the Measure scenario.</li>
* If in membership of 'Denominator', the subject will be assigned a 'closed-gap' or 'open-gap' status based on membership in 'Numerator' and the 'improvement notation'.
*</ul>
* </p>
* <p>
* 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.
* </p>
* <ul>
* <li>ex: 1/10 with improvementNotation "decrease" means that the measureScore is 90%, therefore absence from 'Numerator' means criteria for care was met</li>
* <li>ex: 1/10 with improvementNotation "increase" means that the measureScore is 10%, therefore absence from 'Numerator' means criteria for care was NOT met.</li>
* </ul>
* <ul>
* <li>'open-gap': if in 'Denominator' & NOT in 'Numerator', where 'improvement notation' = increase. Then the subject is 'open-gap'</li>
* <li>'open-gap': if in 'Denominator' & in 'Numerator', where 'improvement notation' = decrease. Then the subject is 'open-gap'</li>
* <li>'closed-gap': if in 'Denominator' & NOT in 'Numerator', where 'improvement notation' = decrease. Then the subject is 'closed-gap'</li>
* <li>'closed-gap': if in 'Denominator' & in 'Numerator', where 'improvement notation' = increase. Then the subject is 'closed-gap'</li>
* </ul>
* <p>'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.</p>
*<br/>
* <p>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.</p>
* 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 |
*
* <p></p>
* *`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<String, CareGapsStatusCode> getGroupGapStatus(Measure measure, MeasureReport measureReport) {
Map<String, CareGapsStatusCode> groupStatus = new HashMap<>();
Expand All @@ -67,10 +58,18 @@ public Map<String, CareGapsStatusCode> getGroupGapStatus(Measure measure, Measur

private CareGapsStatusCode getGapStatus(
Measure measure, MeasureReportGroupComponent measureReportGroup, DateTimeType reportDate) {
Pair<String, Boolean> inInitialPopulation = new MutablePair<>("initial-population", false);
Pair<String, Boolean> inNumerator = new MutablePair<>("numerator", false);
Pair<String, Boolean> 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) {
Expand All @@ -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;

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ protected void initApply(ApplyRequest request) {
var version = request.resolvePathString(request.getPlanDefinition(), "version");
if (version != null) {
var subject = request.getSubjectId().getIdPart();
var formatter = new SimpleDateFormat("yyyy-MM-dd-hh.mm.ssZ");
var formatter = new SimpleDateFormat("yyyy-MM-dd-hh.mm.ss");
request.getModelResolver()
.setValue(
questionnaire,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,13 @@ List<IBaseBackboneElement> processContextItem(PopulateRequest request, IBaseBack
return null;
}
})
// filtering nulls here to prevent unnecessary duplicate responseItems
.filter(r -> nonNull(r))
.collect(Collectors.toList());
if (populationContext.isEmpty()) {
// We always want to return a responseItem even if we have nothing to populate
populationContext.add(null);
}
return populationContext.stream()
.map(context ->
processPopulationContext(request, item, contextExpression.getName(), context, profileAdapter))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Questionnaire;
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent;
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -74,4 +76,28 @@ void testMissingProfileLogsException() {
assertTrue(operationOutcome.hasIssue());
assertEquals(2, operationOutcome.getIssue().size());
}

@Test
void testNoContextStillReturnsResponseItem() {
var questionnaire = new Questionnaire();
doReturn(FhirContext.forR4Cached()).when(repository).fhirContext();
var populateRequest = newPopulateRequestForVersion(FhirVersionEnum.R4, libraryEngine, questionnaire);
var questionnaireItem = new QuestionnaireItemComponent()
.setLinkId("1")
.setDefinition("http://hl7.org/fhir/Patient#Patient.name.given");
var extensions = Arrays.asList(new Extension(Constants.SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT));
questionnaireItem.setExtension(extensions);
var expression = new CqfExpression().setLanguage("text/cql").setExpression("%subject.name.given[0]");
List<IBase> expressionResults = new ArrayList<>();
doReturn(expression)
.when(expressionProcessor)
.getCqfExpression(populateRequest, extensions, Constants.SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT);
doReturn(expressionResults)
.when(expressionProcessor)
.getExpressionResultForItem(populateRequest, expression, "1");
var actual = processItemWithContext.processContextItem(populateRequest, questionnaireItem);
assertEquals(1, actual.size());
assertTrue(
((QuestionnaireResponseItemComponent) actual.get(0)).getAnswer().isEmpty());
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
]
}

0 comments on commit 6533d6a

Please sign in to comment.