Skip to content

Commit

Permalink
Risk adjustment connectathon updates (#601)
Browse files Browse the repository at this point in the history
* Updated report operation arguments and tests ... added assisted servlet ... stubbed resolve operation

* First pass at resolve operation

* Added test for resolve operation
  • Loading branch information
c-schuler authored Sep 14, 2022
1 parent c08c924 commit 15ebd40
Show file tree
Hide file tree
Showing 9 changed files with 1,290 additions and 232 deletions.
26 changes: 26 additions & 0 deletions plugin/ra/src/main/java/org/opencds/cqf/ruler/ra/RAConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

import org.opencds.cqf.ruler.api.OperationProvider;
import org.opencds.cqf.ruler.external.annotations.OnR4Condition;
import org.opencds.cqf.ruler.ra.r4.AssistedServlet;
import org.opencds.cqf.ruler.ra.r4.ResolveProvider;
import org.opencds.cqf.ruler.ra.r4.RiskAdjustmentProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
Expand All @@ -12,6 +17,8 @@
@ConditionalOnProperty(prefix = "hapi.fhir.ra", name = "enabled", havingValue = "true", matchIfMissing = true)
public class RAConfig {

@Autowired
AutowireCapableBeanFactory beanFactory;
@Bean
public RAProperties RAProperties() {
return new RAProperties();
Expand All @@ -28,4 +35,23 @@ public OperationProvider r4ReportProvider() {
public OperationProvider r4RiskAdjustmentProvider() {
return new RiskAdjustmentProvider();
}

@Bean
@Conditional(OnR4Condition.class)
public OperationProvider r4ResolveProvider() {
return new ResolveProvider();
}

@Bean
@Conditional(OnR4Condition.class)
public ServletRegistrationBean<AssistedServlet> assistedServletServletRegistrationBeanR4() {
AssistedServlet assistedServlet = new AssistedServlet();
beanFactory.autowireBean(assistedServlet);
ServletRegistrationBean<AssistedServlet> registrationBean = new ServletRegistrationBean<>();
registrationBean.setName("davinci-ra assisted servlet");
registrationBean.setServlet(assistedServlet);
registrationBean.addUrlMappings("/assisted");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.opencds.cqf.ruler.ra.r4;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class AssistedServlet extends HttpServlet {

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getContentType() == null || !request.getContentType().startsWith("text/csv")) {
response.setStatus(400);
response.getWriter().println(String.format(
"Invalid content type %s. Please use text/csv.",
request.getContentType()));
return;
}

response.setStatus(200);
response.getWriter().println("Success!");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Map;
import java.util.UUID;

import ca.uhn.fhir.rest.api.RequestTypeEnum;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
Expand All @@ -30,6 +31,9 @@
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ReferenceParam;

import static org.opencds.cqf.ruler.utility.r4.Parameters.newParameters;
import static org.opencds.cqf.ruler.utility.r4.Parameters.newPart;

public class ReportProvider extends DaoRegistryOperationProvider
implements ParameterUser, ResourceCreator, MeasureReportUser {
/**
Expand All @@ -53,11 +57,24 @@ public class ReportProvider extends DaoRegistryOperationProvider
@Operation(name = "$report", idempotent = true, type = MeasureReport.class)
public Parameters report(
RequestDetails requestDetails,
@OperationParam(name = "periodStart", min = 1, max = 1) String periodStart,
@OperationParam(name = "periodEnd", min = 1, max = 1) String periodEnd,
@OperationParam(name = "subject", min = 1, max = 1) String subject) throws FHIRException {
@OperationParam(name = "periodStart") String periodStart,
@OperationParam(name = "periodEnd") String periodEnd,
@OperationParam(name = "subject") String subject,
@OperationParam(name = "measureId") List<String> measureId,
@OperationParam(name = "measureIdentifier") List<String> measureIdentifier,
@OperationParam(name = "measureUrl") List<String> measureUrl) throws FHIRException {

if (requestDetails.getRequestType() == RequestTypeEnum.GET) {
try {
validateParameters(requestDetails);
Operations.validatePattern("subject", subject, Operations.PATIENT_OR_GROUP_REFERENCE);
Operations.validatePeriod(requestDetails, "periodStart", "periodEnd");
} catch (Exception e) {
return newParameters(newPart("Invalid parameters",
generateIssue("error", e.getMessage())));
}
}

validateParameters(periodStart, periodEnd, subject);
ensureSupplementalDataElementSearchParameter(requestDetails);

Parameters result = newResource(Parameters.class, subject.replace("/", "-") + "-report");
Expand All @@ -77,33 +94,11 @@ public Parameters report(
return result;
}

// TODO: implement this correctly
public void validateParameters(RequestDetails requestDetails) {

}

private Period validateParameters(String periodStart, String periodEnd, String subject) {
if (periodStart == null) {
throw new IllegalArgumentException("Parameter 'periodStart' is required.");
}
if (periodEnd == null) {
throw new IllegalArgumentException("Parameter 'periodEnd' is required.");
}
Date periodStartDate = Operations.resolveRequestDate(periodStart, true);
Date periodEndDate = Operations.resolveRequestDate(periodEnd, false);
if (periodStartDate.after(periodEndDate)) {
throw new IllegalArgumentException("Parameter 'periodStart' must be before 'periodEnd'.");
}

if (subject == null) {
throw new IllegalArgumentException("Parameter 'subject' is required.");
}
if (!subject.startsWith("Patient/") && !subject.startsWith("Group/")) {
throw new IllegalArgumentException(
"Parameter 'subject' must be in the format 'Patient/[id]' or 'Group/[id]'.");
}

return new Period().setStart(periodStartDate).setEnd(periodEndDate);
Operations.validateCardinality(requestDetails, "periodStart", 1);
Operations.validateCardinality(requestDetails, "periodEnd", 1);
Operations.validateCardinality(requestDetails, "subject", 1);
Operations.validateAtLeastOne(requestDetails, "measureId", "measureIdentifier", "measureUrl");
}

private static final String PATIENT_REPORT_PROFILE_URL = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-measurereport-bundle";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package org.opencds.cqf.ruler.ra.r4;

import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
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 ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
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.Composition;
import org.hl7.fhir.r4.model.DetectedIssue;
import org.hl7.fhir.r4.model.Group;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.ruler.behavior.r4.MeasureReportUser;
import org.opencds.cqf.ruler.provider.DaoRegistryOperationProvider;
import org.opencds.cqf.ruler.utility.Ids;
import org.opencds.cqf.ruler.utility.Operations;
import org.opencds.cqf.ruler.utility.TypedBundleProvider;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static org.opencds.cqf.ruler.utility.r4.Parameters.newParameters;
import static org.opencds.cqf.ruler.utility.r4.Parameters.newPart;

public class ResolveProvider extends DaoRegistryOperationProvider implements MeasureReportUser {

private static final Meta COMPOSITION_META = new Meta().addProfile("http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-composition");
private static final CodeableConcept COMPOSITION_TYPE = new CodeableConcept().addCoding(
new Coding().setSystem("http://loinc.org").setCode("96315-7").setDisplay("Gaps in care report"));

@Operation(name = "$davinci-ra.resolve", idempotent = true, type = MeasureReport.class)
public Parameters resolve(
RequestDetails requestDetails,
@OperationParam(name = "periodStart") String periodStart,
@OperationParam(name = "periodEnd") String periodEnd,
@OperationParam(name = "subject") String subject,
@OperationParam(name = "measureId") List<String> measureId,
@OperationParam(name = "measureIdentifier") List<String> measureIdentifier,
@OperationParam(name = "measureUrl") List<String> measureUrl) {

if (requestDetails.getRequestType() == RequestTypeEnum.GET) {
try {
Operations.validateCardinality(requestDetails, "periodStart", 1);
Operations.validateCardinality(requestDetails, "periodEnd", 1);
Operations.validateCardinality(requestDetails, "subject", 1);
Operations.validatePeriod(requestDetails, "periodStart", "periodEnd");
Operations.validatePattern("subject", subject, Operations.PATIENT_OR_GROUP_REFERENCE);
Operations.validateAtLeastOne(requestDetails, "measureId", "measureIdentifier", "measureUrl");
} catch (Exception e) {
return newParameters(newPart("Invalid parameters",
generateIssue("error", e.getMessage())));
}
}

List<MeasureReport> reports = new ArrayList<>();
if (subject.startsWith("Group/")) {
Group group = read(new IdType(subject));
for (Group.GroupMemberComponent groupMembers: group.getMember()) {
if (groupMembers.hasEntity() && groupMembers.getEntity().getReference().startsWith("Patient/")) {
reports.addAll(fetchReports(groupMembers.getEntity().getReference(), periodStart,
periodEnd, measureId, measureIdentifier, measureUrl));
}
}
}
else {
reports.addAll(fetchReports(subject, periodStart, periodEnd,
measureId, measureIdentifier, measureUrl));
}
Map<MeasureReport, List<DetectedIssue>> issues = fetchIssues(reports);
List<Bundle> raBundles = generateCompositionsAndBundles(subject, issues);

Parameters result = new Parameters();
for (Bundle raBundle : raBundles) {
result.addParameter(newPart("return", raBundle));
}

return result;
}

private List<MeasureReport> fetchReports(String subject, String periodStart, String periodEnd,
List<String> measureId, List<String> measureIdentifier,
List<String> measureUrl) {
List<MeasureReport> reports = new ArrayList<>();
TypedBundleProvider<MeasureReport> searchResults = search(MeasureReport.class,
SearchParameterMap.newSynchronous()
.add(MeasureReport.SP_SUBJECT, new ReferenceParam(subject))
.add(MeasureReport.SP_PERIOD, new DateRangeParam(periodStart, periodEnd)));

for (MeasureReport report : searchResults.getAllResourcesTyped()) {
if (!report.hasMeasure()) continue;
if (measureId != null && !measureId.isEmpty()) {
for (String id : measureId) {
if (report.getMeasure().endsWith(id)) {
reports.add(report);
}
}
}
else if (measureIdentifier != null && !measureIdentifier.isEmpty()) {
for (String identifier : measureIdentifier) {
if (report.getMeasure().contains(identifier)) {
reports.add(report);
}
}
}
else {
for (String url : measureUrl) {
if (report.getMeasure().equals(url)) {
reports.add(report);
}
}
}
}

return reports;
}

private Map<MeasureReport, List<DetectedIssue>> fetchIssues(List<MeasureReport> reports) {
Map<MeasureReport, List<DetectedIssue>> issues = new HashMap<>();
for (MeasureReport report : reports) {
issues.put(report, search(DetectedIssue.class, SearchParameterMap.newSynchronous()
.add(DetectedIssue.SP_IMPLICATED, new ReferenceParam(report.getIdElement().getValue()))
).getAllResourcesTyped());
}

return issues;
}

private List<Bundle> generateCompositionsAndBundles(String subject, Map<MeasureReport, List<DetectedIssue>> issues) {
List<Bundle> raBundles = new ArrayList<>();
for (Map.Entry<MeasureReport, List<DetectedIssue>> issuesSet : issues.entrySet()) {
Composition composition = new Composition();
composition.setMeta(COMPOSITION_META);
composition.setStatus(Composition.CompositionStatus.PRELIMINARY)
.setType(COMPOSITION_TYPE).setSubject(new Reference(subject))
.setDate(Date.from(Instant.now()));
composition.addSection().setFocus(new Reference(issuesSet.getKey().getIdElement().getValue()))
.setEntry(issuesSet.getValue().stream().map(
issue -> new Reference(issue.getIdElement().getValue())).collect(Collectors.toList()));

raBundles.add(generateRaBundle(composition, issuesSet.getValue(), issuesSet.getKey()));
}

return raBundles;
}

private Bundle generateRaBundle(Composition composition, List<DetectedIssue> issues, MeasureReport report) {
Bundle raBundle = new Bundle().setType(Bundle.BundleType.DOCUMENT);
raBundle.addEntry().setResource(composition);
Map<String, Resource> evaluatedResources = new HashMap<>();
for (DetectedIssue issue : issues) {
raBundle.addEntry().setResource(issue);
evaluatedResources.putAll(getEvidenceResources(issue));
}
raBundle.addEntry().setResource(report);
getEvaluatedResources(report, evaluatedResources);

for (Map.Entry<String, Resource> evaluatedResourcesSet : evaluatedResources.entrySet()) {
raBundle.addEntry().setResource(evaluatedResourcesSet.getValue());
}

return raBundle;
}

private Map<String, Resource> getEvidenceResources(DetectedIssue issue) {
Map<String, Resource> evidenceResources = new HashMap<>();
for (DetectedIssue.DetectedIssueEvidenceComponent evidence : issue.getEvidence()) {
if (evidence.hasDetail()) {
for (Reference detail : evidence.getDetail()) {
if (detail.getReference().startsWith("MeasureReport/")) continue;
evidenceResources.put(Ids.simple(new IdType(detail.getReference())), read(new IdType(detail.getReference())));
}
}
}

return evidenceResources;
}
}
Loading

0 comments on commit 15ebd40

Please sign in to comment.