Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#543: $risk-adjustment operation POC #550

Merged
merged 13 commits into from
Jul 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.ruler.behavior.DaoRegistryUser;
import org.opencds.cqf.ruler.behavior.IdCreator;
Expand All @@ -13,20 +15,20 @@
import org.slf4j.LoggerFactory;

public interface MeasureReportUser extends DaoRegistryUser, IdCreator {
static final Logger ourLog = LoggerFactory.getLogger(ParameterUser.class);
Logger ourLog = LoggerFactory.getLogger(ParameterUser.class);

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

public default Map<String, Resource> getEvaluatedResources(org.hl7.fhir.r4.model.MeasureReport report) {
default Map<String, Resource> getEvaluatedResources(org.hl7.fhir.r4.model.MeasureReport report) {
Map<String, Resource> resources = new HashMap<>();
getEvaluatedResources(report, resources);

return resources;
}

public default MeasureReportUser getEvaluatedResources(org.hl7.fhir.r4.model.MeasureReport report,
Map<String, Resource> resources) {
default MeasureReportUser getEvaluatedResources(org.hl7.fhir.r4.model.MeasureReport report,
Map<String, Resource> resources) {
report.getEvaluatedResource().forEach(evaluatedResource -> {
IIdType resourceId = evaluatedResource.getReferenceElement();
if (resourceId.getResourceType() == null || resources.containsKey(Ids.simple(resourceId))) {
Expand All @@ -41,4 +43,13 @@ public default MeasureReportUser getEvaluatedResources(org.hl7.fhir.r4.model.Mea

return this;
}

default OperationOutcome generateIssue(String severity, String issue) {
OperationOutcome error = new OperationOutcome();
error.addIssue()
.setSeverity(OperationOutcome.IssueSeverity.fromCode(severity))
.setCode(OperationOutcome.IssueType.PROCESSING)
.setDetails(new CodeableConcept().setText(issue));
return error;
}
}
6 changes: 5 additions & 1 deletion plugin/ra/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

<artifactId>cqf-ruler-ra</artifactId>
<dependencies>

<dependency>
<groupId>org.opencds.cqf.ruler</groupId>
<artifactId>cqf-ruler-cr</artifactId>
<version>0.5.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.opencds.cqf.ruler.api.OperationProvider;
import org.opencds.cqf.ruler.external.annotations.OnR4Condition;
import org.opencds.cqf.ruler.ra.r4.RiskAdjustmentProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
Expand All @@ -21,4 +22,10 @@ public RAProperties RAProperties() {
public OperationProvider r4ReportProvider() {
return new org.opencds.cqf.ruler.ra.r4.ReportProvider();
}

@Bean
@Conditional(OnR4Condition.class)
public OperationProvider r4RiskAdjustmentProvider() {
return new RiskAdjustmentProvider();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package org.opencds.cqf.ruler.ra.r4;

import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Extension;
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.Meta;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.ruler.behavior.r4.MeasureReportUser;
import org.opencds.cqf.ruler.cr.r4.provider.MeasureEvaluateProvider;
import org.opencds.cqf.ruler.provider.DaoRegistryOperationProvider;
import org.opencds.cqf.ruler.utility.Operations;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Collections;
import java.util.Map;

public class RiskAdjustmentProvider extends DaoRegistryOperationProvider implements MeasureReportUser {

@Autowired
MeasureEvaluateProvider measureEvaluateProvider;

private static String suspectTypeUrl = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-suspectType";
private static String evidenceStatusUrl = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-evidenceStatus";
private static String evidenceStatusDateUrl = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-evidenceStatusDate";

private String visited;

@Operation(name = "$risk-adjustment", idempotent = true, type = Measure.class)
public Parameters riskAdjustment(
RequestDetails requestDetails,
@IdParam IdType theId,
@OperationParam(name = "type") String type,
@OperationParam(name = "periodStart") String periodStart,
@OperationParam(name = "periodEnd") String periodEnd,
@OperationParam(name = "subject") String subject) {

if (requestDetails.getRequestType() == RequestTypeEnum.GET) {
try {
Operations.validateCardinality(requestDetails, "type", 1);
Operations.validateCardinality(requestDetails, "periodStart", 1);
Operations.validateCardinality(requestDetails, "periodEnd", 1);
Operations.validateCardinality(requestDetails, "subject", 1);
} catch (Exception e) {
return org.opencds.cqf.ruler.utility.r4.Parameters.newParameters(
org.opencds.cqf.ruler.utility.r4.Parameters.newPart("Invalid parameters", generateIssue("error", e.getMessage()))
);
}
}

if (!type.equalsIgnoreCase("report")) {
org.opencds.cqf.ruler.utility.r4.Parameters.newParameters(
org.opencds.cqf.ruler.utility.r4.Parameters.newPart(
subject, generateIssue("error", String.format("The $risk-adjustment operation is not implemented for %s type parameter on this server", type))
)
);
}

MeasureReport unprocessedReport = measureEvaluateProvider.evaluateMeasure(
requestDetails, theId, periodStart, periodEnd, null, subject, null,
null, null, null, null
);

Parameters riskAdjustmentParameters = new Parameters();

RiskAdjustmentReturnElement riskAdjustmentReturnElement = new RiskAdjustmentReturnElement(unprocessedReport.getSubject().getReference(), unprocessedReport);
resolveRiskAdjustmentReport(riskAdjustmentReturnElement);
riskAdjustmentParameters.addParameter()
.setName(riskAdjustmentReturnElement.reference)
.setResource(riskAdjustmentReturnElement.getRiskAdjustmentOutcome());

return riskAdjustmentParameters;
}

private void resolveRiskAdjustmentReport(RiskAdjustmentReturnElement riskAdjustmentReturnElement) {
for (MeasureReport.MeasureReportGroupComponent group : riskAdjustmentReturnElement.unprocessedReport.getGroup()) {
CodeableConcept hccCode = group.getCode();
visited = null;
for (MeasureReport.MeasureReportGroupStratifierComponent stratifier : group.getStratifier()) {
CodeableConcept stratifierPopCode = stratifier.getCodeFirstRep();
for (MeasureReport.StratifierGroupComponent stratum : stratifier.getStratum()) {
CodeableConcept value = stratum.getValue();
Quantity score = stratum.getMeasureScore();
if (stratifierPopCode.hasCoding() && stratifierPopCode.getCodingFirstRep().getCode().equals("historic")) {
resolveGroup(riskAdjustmentReturnElement, new Historic(hccCode, value, score, resolveEvidenceStatusDate(riskAdjustmentReturnElement)));
}
else if (stratifierPopCode.hasCoding() && stratifierPopCode.getCodingFirstRep().getCode().equals("suspected")) {
resolveGroup(riskAdjustmentReturnElement, new Suspected(hccCode, value, score, resolveEvidenceStatusDate(riskAdjustmentReturnElement)));
}
else if (stratifierPopCode.hasCoding() && stratifierPopCode.getCodingFirstRep().getCode().equals("net-new")) {
resolveGroup(riskAdjustmentReturnElement, new NetNew(hccCode, value, score, resolveEvidenceStatusDate(riskAdjustmentReturnElement)));
}
}
}
}
}

private void resolveGroup(RiskAdjustmentReturnElement riskAdjustmentReturnElement, RiskAdjustmentGroup riskAdjustmentGroup) {
if (riskAdjustmentGroup.value != null && riskAdjustmentGroup.value.hasText() && riskAdjustmentGroup.value.getText().equalsIgnoreCase("true")) {
if (visited != null) {
riskAdjustmentReturnElement.createIssue(
String.format(
"Disjoint populations found. The %s and %s populations cannot be included in the same group",
visited, riskAdjustmentGroup.name)
);
}
else if (riskAdjustmentGroup instanceof NetNew && riskAdjustmentGroup.score.hasValue() && riskAdjustmentGroup.score.getValue().intValue() == 0) {
riskAdjustmentReturnElement.createIssue("Invalid open gap detected for net-new population");
}
else {
riskAdjustmentReturnElement.processedReport.addGroup(riskAdjustmentGroup.resolveGroup());
visited = riskAdjustmentGroup.name;
}
}
}

private Extension resolveEvidenceStatusDate(RiskAdjustmentReturnElement riskAdjustmentReturnElement) {
for (Resource contained : riskAdjustmentReturnElement.unprocessedReport.getContained()) {
if (contained instanceof Observation && ((Observation) contained).hasCode() && ((Observation) contained).getCode().hasText()
&& ((Observation) contained).getCode().getText().equalsIgnoreCase("evidence-status-date") && ((Observation) contained).hasValueCodeableConcept()
&& ((Observation) contained).getValueCodeableConcept().hasCoding() && ((Observation) contained).getValueCodeableConcept().getCodingFirstRep().hasCode()) {
return new Extension().setUrl(evidenceStatusDateUrl).setValue(
new CodeableConcept().setCoding(
Collections.singletonList(new Coding().setCode(((Observation) contained).getValueCodeableConcept().getCodingFirstRep().getCode()).setSystem(evidenceStatusDateUrl))
)
);
}
}
return null;
}

private static class RiskAdjustmentGroup {
private Extension closedGapExtension = new Extension().setUrl(evidenceStatusUrl).setValue(
new CodeableConcept().setCoding(
Collections.singletonList(new Coding().setCode("closed-gap").setSystem(evidenceStatusUrl))
)
);
private Extension openGapExtension = new Extension().setUrl(evidenceStatusUrl).setValue(
new CodeableConcept().setCoding(
Collections.singletonList(new Coding().setCode("open-gap").setSystem(evidenceStatusUrl))
)
);

String name;
CodeableConcept hccCode;
CodeableConcept value;
Quantity score;
Extension supectTypeExtension;
Extension evidenceStatusExtension;
Extension evidenceStatusDateExtension;

RiskAdjustmentGroup(CodeableConcept hccCode, CodeableConcept value, Quantity score, Extension evidenceStatusDateExtension) {
this.hccCode = hccCode;
this.value = value;
this.score = score;
this.evidenceStatusDateExtension = evidenceStatusDateExtension;
}

MeasureReport.MeasureReportGroupComponent resolveGroup() {
MeasureReport.MeasureReportGroupComponent group = new MeasureReport.MeasureReportGroupComponent();
evidenceStatusExtension = score.hasValue() && score.getValue().intValue() == 1 ? closedGapExtension : openGapExtension;
group.setCode(hccCode)
.addExtension(supectTypeExtension)
.addExtension(evidenceStatusExtension)
.addExtension(evidenceStatusDateExtension);
return group;
}
}

private static class Historic extends RiskAdjustmentGroup {

Historic(CodeableConcept hccCode, CodeableConcept value, Quantity score, Extension evidenceStatusDateExtension) {
super(hccCode, value, score, evidenceStatusDateExtension);
this.supectTypeExtension = new Extension().setUrl(suspectTypeUrl).setValue(
new CodeableConcept().setCoding(
Collections.singletonList(new Coding().setCode("historic").setSystem(suspectTypeUrl))
)
);
this.name = "historic";
}
}

private static class Suspected extends RiskAdjustmentGroup {

Suspected(CodeableConcept hccCode, CodeableConcept value, Quantity score, Extension evidenceStatusDateExtension) {
super(hccCode, value, score, evidenceStatusDateExtension);
this.supectTypeExtension = new Extension().setUrl(suspectTypeUrl).setValue(
new CodeableConcept().setCoding(
Collections.singletonList(new Coding().setCode("suspected").setSystem(suspectTypeUrl))
)
);
this.name = "suspected";
}
}

private static class NetNew extends RiskAdjustmentGroup {

NetNew(CodeableConcept hccCode, CodeableConcept value, Quantity score, Extension evidenceStatusDateExtension) {
super(hccCode, value, score, evidenceStatusDateExtension);
this.supectTypeExtension = new Extension().setUrl(suspectTypeUrl).setValue(
new CodeableConcept().setCoding(
Collections.singletonList(new Coding().setCode("net-new").setSystem(suspectTypeUrl))
)
);
this.name = "net-new";
}
}

private class RiskAdjustmentReturnElement {
String reference;
MeasureReport unprocessedReport;
MeasureReport processedReport;
OperationOutcome error;

RiskAdjustmentReturnElement(String reference, MeasureReport unprocessedReport) {
this.reference = reference;
this.unprocessedReport = unprocessedReport;
this.processedReport = new MeasureReport();
this.unprocessedReport.copyValues(this.processedReport);
this.processedReport.getGroup().clear();
this.processedReport.setMeta(new Meta().addProfile("http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-measurereport"));
}

void createIssue(String issue) {
this.error = generateIssue("error" ,issue);
}

Resource getRiskAdjustmentOutcome() {
return this.error == null ? bundleReport() : this.error;
}

private Bundle bundleReport() {
Bundle raBundle = new Bundle().setType(Bundle.BundleType.COLLECTION);
raBundle.setMeta(new Meta().addProfile("http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-measurereport-bundle"));
raBundle.addEntry().setResource(processedReport);
for (Map.Entry<String, Resource> evaluatedResources : getEvaluatedResources(processedReport).entrySet()) {
raBundle.addEntry().setResource(evaluatedResources.getValue());
}
return raBundle;
}
}

}
Loading