Skip to content

Commit

Permalink
Merge branch 'master' into br-fix-dupliate-namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
JPercival authored Sep 27, 2024
2 parents be89809 + 6e14c44 commit f3d2dba
Show file tree
Hide file tree
Showing 33 changed files with 26,353 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ private CareGapsConstants() {
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/gaps-composition-deqm";
public static final String CARE_GAPS_DETECTED_ISSUE_PROFILE =
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/gaps-detectedissue-deqm";
public static final String CARE_GAPS_DETECTED_ISSUE_MR_GROUP_ID =
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/gaps-detectedissue-mr-group-id-deqm";
public static final String CARE_GAPS_GAP_STATUS_EXTENSION =
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-gapStatus";
public static final String CARE_GAPS_GAP_STATUS_SYSTEM =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ private MeasureReportConstants() {}

public static final String MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM =
"http://terminology.hl7.org/CodeSystem/measure-improvement-notation";

public static final String MEASUREREPORT_IMPROVEMENT_NOTATION_EXTENSION =
"http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-improvementNotation";
public static final String MEASUREREPORT_MEASURE_POPULATION_SYSTEM =
"http://terminology.hl7.org/CodeSystem/measure-population";
public static final String MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM;
import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.MEASUREREPORT_MEASURE_POPULATION_SYSTEM;

import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent;
import org.opencds.cqf.fhir.cr.measure.enumeration.CareGapsStatusCode;

/**
Expand All @@ -28,8 +31,8 @@ public class R4CareGapStatusEvaluator {
* 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 absense from 'Numerator' means criteria for care was met</li>
* <li>ex: 1/10 with improvementNotation "increase" means that the measureScore is 10%, therefore absense from 'Numerator' means criteria for care was NOT met.</li>
* <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>
Expand All @@ -38,10 +41,23 @@ public class R4CareGapStatusEvaluator {
* <li>'closed-gap': if in 'Denominator' & in 'Numerator', where 'improvement notation' = increase. Then the subject is 'closed-gap'</li>
* </ul>
*/
public CareGapsStatusCode getGapStatus(Measure measure, MeasureReport measureReport) {
public Map<String, CareGapsStatusCode> getGroupGapStatus(Measure measure, MeasureReport measureReport) {
Map<String, CareGapsStatusCode> groupStatus = new HashMap<>();

for (MeasureReportGroupComponent group : measureReport.getGroup()) {
var groupId = group.getId();
var gapStatus = getGapStatus(measure, group);

groupStatus.put(groupId, gapStatus);
}
return groupStatus;
}

private CareGapsStatusCode getGapStatus(Measure measure, MeasureReportGroupComponent measureReportGroup) {
Pair<String, Boolean> inNumerator = new MutablePair<>("numerator", false);
Pair<String, Boolean> inDenominator = new MutablePair<>("denominator", false);
measureReport.getGroup().forEach(group -> group.getPopulation().forEach(population -> {
// get Numerator and Denominator membership
measureReportGroup.getPopulation().forEach(population -> {
if (population.hasCode()
&& population.getCode().hasCoding(MEASUREREPORT_MEASURE_POPULATION_SYSTEM, inNumerator.getKey())
&& population.getCount() == 1) {
Expand All @@ -52,12 +68,15 @@ public CareGapsStatusCode getGapStatus(Measure measure, MeasureReport measureRep
&& population.getCount() == 1) {
inDenominator.setValue(true);
}
}));
});

// default improvementNotation
boolean isPositive = true;

// if value is present, set value from measure if populated
// TODO: look for group specified 'improvement notation', if missing, then look on measure
/*if (groupHasImprovementNotation(measureReportGroup)) {
isPositive = groupImprovementNotationIsPositive(measureReportGroup);
} else if (measure.hasImprovementNotation()) {*/
if (measure.hasImprovementNotation()) {
isPositive =
measure.getImprovementNotation().hasCoding(MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM, "increase");
Expand All @@ -75,4 +94,17 @@ public CareGapsStatusCode getGapStatus(Measure measure, MeasureReport measureRep

return CareGapsStatusCode.CLOSED_GAP;
}

/*
// TODO implement Measure Group Level improvement notation extension
private boolean groupHasImprovementNotation(MeasureReportGroupComponent groupComponent) {
return groupComponent.getExtensionByUrl(MEASUREREPORT_IMPROVEMENT_NOTATION_EXTENSION) != null;
}
private boolean groupImprovementNotationIsPositive(MeasureReportGroupComponent groupComponent) {
var code = (CodeableConcept) groupComponent
.getExtensionByUrl(MEASUREREPORT_IMPROVEMENT_NOTATION_EXTENSION)
.getValue();
return code.hasCoding(MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM, "increase");
}*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.opencds.cqf.fhir.cr.measure.common.MeasureConstants.EXT_SDE_REFERENCE_URL;
import static org.opencds.cqf.fhir.cr.measure.constant.CareGapsConstants.CARE_GAPS_BUNDLE_PROFILE;
import static org.opencds.cqf.fhir.cr.measure.constant.CareGapsConstants.CARE_GAPS_COMPOSITION_PROFILE;
import static org.opencds.cqf.fhir.cr.measure.constant.CareGapsConstants.CARE_GAPS_DETECTED_ISSUE_MR_GROUP_ID;
import static org.opencds.cqf.fhir.cr.measure.constant.CareGapsConstants.CARE_GAPS_DETECTED_ISSUE_PROFILE;
import static org.opencds.cqf.fhir.cr.measure.constant.CareGapsConstants.CARE_GAPS_GAP_STATUS_EXTENSION;
import static org.opencds.cqf.fhir.cr.measure.constant.CareGapsConstants.CARE_GAPS_GAP_STATUS_SYSTEM;
Expand All @@ -17,6 +18,7 @@
import com.google.common.collect.ImmutableMap;
import jakarta.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
Expand All @@ -40,6 +42,7 @@
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.cr.measure.CareGapsProperties;
import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions;
Expand Down Expand Up @@ -146,15 +149,22 @@ public Bundle makePatientBundle(Bundle bundle, List<String> statuses, Patient pa
addResourceId(mr);
Measure measure = r4MeasureServiceUtils.resolveByUrl(mr.getMeasure());
// Applicable Reports per Gap-Status
var gapStatus = gapEvaluator.getGapStatus(measure, mr);
boolean keepResult = statuses.contains(gapStatus.toString());
if (keepResult) {
var gapStatus = gapEvaluator.getGroupGapStatus(measure, mr);
var filteredGapStatus = filteredGapStatus(gapStatus, statuses);
if (!filteredGapStatus.isEmpty()) {
// add Report to final Care-gap report
measureReports.add(mr);
// Issue Detected for Report
DetectedIssue detectedIssue = getDetectedIssue(patient, mr, gapStatus);
detectedIssues.add(getDetectedIssue(patient, mr, gapStatus));
composition.addSection(getSection(measure, mr, detectedIssue, gapStatus));
// Issue(s) Detected from MeasureReport
for (Map.Entry<String, CareGapsStatusCode> item : filteredGapStatus.entrySet()) {
String groupId = item.getKey();
CareGapsStatusCode careGapsStatusCode = item.getValue();
// create DetectedIssue per gap-status and MeasureReport.groupId
DetectedIssue issue = getDetectedIssue(patient, mr, groupId, careGapsStatusCode);
// add DetectedIssue list to set on Bundle
detectedIssues.add(issue);
// add sections for DetectedIssues created
composition.addSection(getSection(measure, mr, issue, careGapsStatusCode));
}
// Track evaluated Resources
populateEvaluatedResources(mr, evalPlusSDE);
populateSDEResources(mr, evalPlusSDE);
Expand All @@ -170,6 +180,20 @@ public Bundle makePatientBundle(Bundle bundle, List<String> statuses, Patient pa
}
}

private Map<String, CareGapsStatusCode> filteredGapStatus(
Map<String, CareGapsStatusCode> careGapStatusPerGroupId, List<String> statuses) {
Map<String, CareGapsStatusCode> filtered = new HashMap<>();
for (Map.Entry<String, CareGapsStatusCode> entry : careGapStatusPerGroupId.entrySet()) {
String groupId = entry.getKey();
CareGapsStatusCode careGapsStatusCode = entry.getValue();
// check resulting status for report groups is in operation request 'statuses'
if (statuses.contains(careGapsStatusCode.toString())) {
filtered.put(groupId, careGapsStatusCode);
}
}
return filtered;
}

private Bundle.BundleEntryComponent getBundleEntry(String serverBase, Resource resource) {
return new Bundle.BundleEntryComponent().setResource(resource).setFullUrl(getFullUrl(serverBase, resource));
}
Expand Down Expand Up @@ -203,9 +227,17 @@ private Composition getComposition(Patient patient) {
.build();
}

private boolean isMultiRateMeasure(MeasureReport measureReport) {
return measureReport.getGroup().size() > 1;
}

private DetectedIssue getDetectedIssue(
Patient patient, MeasureReport measureReport, CareGapsStatusCode careGapStatusCode) {
return new DetectedIssueBuilder<>(DetectedIssue.class)
Patient patient,
MeasureReport measureReport,
String measureReportGroupId,
CareGapsStatusCode careGapsStatusCode) {

var detectedIssue = new DetectedIssueBuilder<>(DetectedIssue.class)
.withProfile(CARE_GAPS_DETECTED_ISSUE_PROFILE)
.withStatus(DetectedIssue.DetectedIssueStatus.FINAL.toString())
.withCode(CARE_GAPS_CODES.get("http://terminology.hl7.org/CodeSystem/v3-ActCode/CAREGAP"))
Expand All @@ -216,9 +248,19 @@ private DetectedIssue getDetectedIssue(
new CodeableConceptSettings()
.add(
CARE_GAPS_GAP_STATUS_SYSTEM,
careGapStatusCode.toString(),
careGapStatusCode.toDisplayString())))
careGapsStatusCode.toString(),
careGapsStatusCode.toDisplayString())))
.build();

if (measureReportGroupId != null && isMultiRateMeasure(measureReport)) {
// MeasureReportGroupComponent.id value set here to differentiate between DetectedIssue resources for the
// same MeasureReport
Extension groupIdExt = new Extension();
groupIdExt.setUrl(CARE_GAPS_DETECTED_ISSUE_MR_GROUP_ID);
groupIdExt.setValue(new StringType(measureReportGroupId));
detectedIssue.setExtension(Collections.singletonList(groupIdExt));
}
return detectedIssue;
}

private void populateEvaluatedResources(MeasureReport measureReport, Map<String, Resource> resources) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.Measure.MeasureGroupComponent;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.PrimitiveType;
Expand Down Expand Up @@ -154,6 +155,34 @@ private void measureCompatibilityCheck(List<Measure> measures) {
for (Measure measure : measures) {
checkMeasureScoringType(measure);
checkMeasureImprovementNotation(measure);
checkMeasureBasis(measure);
checkMeasureGroupComponents(measure);
}
}

private void checkMeasureBasis(Measure measure) {
R4MeasureBasisDef measureDef = new R4MeasureBasisDef();
if (!measureDef.isBooleanBasis(measure)) {
throw new IllegalArgumentException(
String.format("CareGaps can't process Measure: %s, it is not Boolean basis.", measure.getIdPart()));
}
}

/**
* MultiRate Measures require a unique 'id' per GroupComponent to uniquely identify results in Measure Report.
* This is helpful when creating DetectedIssues per GroupComponent so endUsers can attribute evidence of a Care-Gap to the specific MeasureReport result
* @param measure Measure resource
*/
private void checkMeasureGroupComponents(Measure measure) {
// if a Multi-rate Measure, enforce groupId to be populated
if (measure.getGroup().size() > 1) {
for (MeasureGroupComponent group : measure.getGroup()) {
if (measure.getGroup().size() > 1
&& (group.getId() == null || group.getId().isEmpty())) {
throw new IllegalArgumentException(
"Multi-rate Measure resources require unique 'id' for GroupComponents to be populated.");
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ void dataRequirements_booleanBasisMeasure_noPeriod() {
.DataRequirements()
.then()
.hasDataRequirementCount(1)
.hasParameterDefCount(11)
.hasParameterDefCount(13)
.hasRelatedArtifactCount(1)
.report();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,107 @@ void ContinuousVariable_ScoringTypeError() {
"MeasureScoring type: Continuous Variable, is not an accepted Type for care-gaps service"));
}
}

// MinimalProportionResourceBasisSingleGroup
@Test
void MinimalProportionResourceBasisSingleGroup_Subject() {
try {
GIVEN_REPO
.when()
.subject("Patient/female-1988")
.periodStart("2024-01-01")
.periodEnd("2024-12-31")
.measureIds("MinimalProportionResourceBasisSingleGroup")
.statuses("closed-gap")
.statuses("open-gap")
.getCareGapsReport()
.then()
.hasBundleCount(1);
fail("resource based measures should fail");
} catch (IllegalArgumentException e) {
Assertions.assertTrue(
e.getMessage()
.contains(
"CareGaps can't process Measure: MinimalProportionResourceBasisSingleGroup, it is not Boolean basis"));
}
}

@Test
void MinimalProportionBooleanBasisMultiGroup() {
GIVEN_REPO
.when()
.subject("Patient/female-1988")
.periodStart("2019-01-01")
.periodEnd("2019-12-31")
.measureIds("MinimalProportionBooleanBasisMultiGroup")
.statuses("closed-gap")
.statuses("open-gap")
.statuses("not-applicable")
.getCareGapsReport()
.then()
.hasBundleCount(1)
.firstParameter()
.detectedIssueCount(2); // 1 Detected issue per groupId
}

@Test
void MinimalProportionBooleanBasisMultiGroupDifferentStatus() {
GIVEN_REPO
.when()
.subject("Patient/female-1988")
.periodStart("2019-01-01")
.periodEnd("2019-12-31")
.measureIds("MinimalProportionBooleanBasisMultiGroupDifferentStatus")
.statuses("closed-gap")
.getCareGapsReport()
.then()
.hasBundleCount(1)
.firstParameter()
.detectedIssueCount(
1); // 2 Detected issue per groupId, 1 open-gap, 1 closed-gap. Only "closed-gap" should show
}

@Test
void MinimalProportionBooleanBasisMultiGroup_NoId() {
try {
GIVEN_REPO
.when()
.subject("Patient/female-1988")
.periodStart("2019-01-01")
.periodEnd("2019-12-31")
.measureIds("MinimalProportionBooleanBasisMultiGroupNoGroupId")
.statuses("closed-gap")
.statuses("open-gap")
.statuses("not-applicable")
.getCareGapsReport()
.then()
.hasBundleCount(1)
.firstParameter()
.detectedIssueCount(2); // 1 Detected issue per groupId
fail("this should fail without a groupId");
} catch (IllegalArgumentException e) {
Assertions.assertTrue(e.getMessage()
.contains("Multi-rate Measure resources require unique 'id' for GroupComponents to be populated."));
}
}

/*
// TODO implement Measure Group Level improvement notation extension
@Test
void MinimalProportionBooleanBasisMultiGroupGroupImpNotation() {
GIVEN_REPO
.when()
.subject("Patient/female-1988")
.periodStart("2019-01-01")
.periodEnd("2019-12-31")
.measureIds("MinimalProportionBooleanBasisMultiGroupGroupImpNotation")
.statuses("closed-gap")
.getCareGapsReport()
.then()
.hasBundleCount(1)
.firstParameter()
.detectedIssueCount(
2); // 2 Detected issue per groupId, one is decrease, the other increase improvement Notation. Both should be closed-gap
}
*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ define "Measure Population":
"Denominator"

define "date of compliance":
"Measurement Period"
"Measurement Period"

define "always false":
false

define "always true":
true
Loading

0 comments on commit f3d2dba

Please sign in to comment.