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

Assisted MeasureReport (CSV) operation implementation #674

Merged
merged 8 commits into from
Dec 2, 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
5 changes: 5 additions & 0 deletions plugin/ra/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@
<artifactId>cqf-ruler-cr</artifactId>
<version>0.10.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.7.1</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ private RAConstants() {
public static final String APPROVE_ID_PREFIX = "approve-coding-gaps-";
public static final String PATIENT_REPORT_URL = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-measurereport";
public static final String CODING_GAP_BUNDLE_URL = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-coding-gap-bundle";
public static final String MEASURE_REPORT_PROFILE_URL = "https://build.fhir.org/ig/HL7/davinci-ra/StructureDefinition-ra-measurereport.html";
public static final String HCC_CODESYSTEM_URL = "http://terminology.hl7.org/CodeSystem/cmshcc";
public static final String SUSPECT_TYPE_URL = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-suspectType";
public static final String EVIDENCE_STATUS_URL = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-evidenceStatus";
public static final String EVIDENCE_STATUS_DATE_URL = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-evidenceStatusDate";
public static final String HIERARCHICAL_STATUS_URL = "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-hierarchicalStatus";
public static final String HIERARCHICAL_STATUS_SYSTEM = "http://hl7.org/fhir/us/davinci-ra/CodeSystem/hierarchical-status";
// Suspect Type
public static final String SUSPECT_TYPE_SYSTEM = "http://hl7.org/fhir/us/davinci-ra/CodeSystem/suspect-type";
public static final String HISTORIC_CODE = "historic";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.opencds.cqf.ruler.ra.r4;

import com.opencsv.bean.CsvBindByName;

@SuppressWarnings("unused")
public class AssistedRowData {
@CsvBindByName(column = "periodStart")
private String periodStart;
@CsvBindByName(column = "periodEnd")
private String periodEnd;
@CsvBindByName(column = "modelId")
private String modelId;
@CsvBindByName(column = "modelVersion")
private String modelVersion;
@CsvBindByName(column = "patientId")
private String patientId;
@CsvBindByName(column = "ccCode")
private String ccCode;
@CsvBindByName(column = "suspectType")
private String suspectType;
@CsvBindByName(column = "evidenceStatus")
private String evidenceStatus;
@CsvBindByName(column = "evidenceStatusDate")
private String evidenceStatusDate;
@CsvBindByName(column = "hiearchicalStatus")
private String hiearchicalStatus;

public String getPeriodStart() {
return periodStart;
}

public String getPeriodEnd() {
return periodEnd;
}

public String getModelId() {
return modelId;
}

public String getModelVersion() {
return modelVersion;
}

public String getPatientId() {
return patientId;
}

public String getCcCode() {
return ccCode;
}

public String getSuspectType() {
return suspectType;
}

public String getEvidenceStatus() {
return evidenceStatus;
}

public String getEvidenceStatusDate() {
return evidenceStatusDate;
}

public String getHiearchicalStatus() {
return hiearchicalStatus;
}
}
128 changes: 115 additions & 13 deletions plugin/ra/src/main/java/org/opencds/cqf/ruler/ra/r4/AssistedServlet.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,126 @@
package org.opencds.cqf.ruler.ra.r4;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

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

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.DateType;
import org.hl7.fhir.r4.model.Extension;
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.Period;
import org.hl7.fhir.r4.model.Reference;
import org.opencds.cqf.ruler.ra.RAConstants;

import com.opencsv.bean.CsvToBeanBuilder;

import ca.uhn.fhir.context.FhirContext;

@SuppressWarnings({ "unchecked", "squid:S1989", "squid:S112", "rawtypes" })
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;
}

List<AssistedRowData> data = new CsvToBeanBuilder(request.getReader()).withType(AssistedRowData.class).build()
.parse();
Map<String, MeasureReport> mrMap = new HashMap<>();
for (AssistedRowData row : data) {
String hash = getHash(row);
if (mrMap.containsKey(hash)) {
addGroup(row, mrMap.get(hash));
} else {
mrMap.put(hash, createMeasureReport(row));
}
}

Bundle transaction = new Bundle();
transaction.setType(Bundle.BundleType.TRANSACTION);
for (Map.Entry<String, MeasureReport> entry : mrMap.entrySet()) {
transaction.addEntry().setResource(entry.getValue()).setRequest(new Bundle.BundleEntryRequestComponent()
.setMethod(Bundle.HTTPVerb.PUT).setUrl(entry.getValue().getIdElement().getValue()));
}

response.setStatus(200);
response.getWriter().println(FhirContext.forR4Cached()
.newJsonParser().setPrettyPrint(true).encodeResourceToString(transaction));
}

private String getHash(AssistedRowData data) {
return data.getPeriodStart() + data.getPeriodEnd() + data.getModelId() + data.getModelVersion()
+ data.getPatientId();
}

@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;
}
private MeasureReport createMeasureReport(AssistedRowData data) {
MeasureReport mr = new MeasureReport();
mr.setIdElement(new IdType("MeasureReport", "assisted-" + UUID.randomUUID()));
mr.setMeta(new Meta().addProfile(RAConstants.MEASURE_REPORT_PROFILE_URL));
mr.setStatus(MeasureReport.MeasureReportStatus.COMPLETE);
mr.setType(MeasureReport.MeasureReportType.INDIVIDUAL);
mr.setMeasure(data.getModelId());
mr.setSubject(new Reference(data.getPatientId().startsWith("Patient/")
? data.getPatientId()
: "Patient/" + data.getPatientId()));
mr.setDate(new Date());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
Date start;
Date end;
try {
start = formatter.parse(data.getPeriodStart());
end = formatter.parse(data.getPeriodEnd());
} catch (ParseException e) {
throw new RuntimeException(e);
}
mr.setPeriod(new Period().setStart(start).setEnd(end));
addGroup(data, mr);
return mr;
}

response.setStatus(200);
response.getWriter().println("Success!");
}
private void addGroup(AssistedRowData data, MeasureReport mr) {
MeasureReport.MeasureReportGroupComponent group = new MeasureReport.MeasureReportGroupComponent();
group.setId("group-" + data.getCcCode());
group.setCode(new CodeableConcept(new Coding().setCode(data.getCcCode())
.setSystem(RAConstants.HCC_CODESYSTEM_URL).setVersion(data.getModelVersion())));
if (data.getSuspectType() != null && !data.getSuspectType().isBlank()) {
group.addExtension(createCodeableConceptExtension(RAConstants.SUSPECT_TYPE_URL,
RAConstants.SUSPECT_TYPE_SYSTEM, data.getSuspectType()));
}
if (data.getEvidenceStatus() != null && !data.getEvidenceStatus().isBlank()) {
group.addExtension(createCodeableConceptExtension(RAConstants.EVIDENCE_STATUS_URL,
RAConstants.EVIDENCE_STATUS_SYSTEM, data.getEvidenceStatus()));
}
if (data.getEvidenceStatusDate() != null && !data.getEvidenceStatusDate().isBlank()) {
group.addExtension(new Extension().setUrl(RAConstants.EVIDENCE_STATUS_DATE_URL)
.setValue(new DateType(data.getEvidenceStatusDate())));
}
if (data.getHiearchicalStatus() != null && !data.getHiearchicalStatus().isBlank()) {
group.addExtension(createCodeableConceptExtension(RAConstants.HIERARCHICAL_STATUS_URL,
RAConstants.HIERARCHICAL_STATUS_SYSTEM, data.getHiearchicalStatus()));
}
mr.addGroup(group);
}

private Extension createCodeableConceptExtension(String url, String system, String code) {
return new Extension().setUrl(url).setValue(new CodeableConcept(new Coding().setCode(code).setSystem(system)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.cql.evaluator.fhir.util.Ids;
import org.opencds.cqf.ruler.behavior.r4.ParameterUser;
import org.opencds.cqf.ruler.provider.DaoRegistryOperationProvider;
import org.opencds.cqf.ruler.ra.RAConstants;
Expand Down Expand Up @@ -49,7 +50,7 @@ public Parameters remediate(
Composition composition = getCompositionFromBundle(b);
List<DetectedIssue> issues = getIssuesFromBundle(b);
Resource author = getAuthorFromBundle(b, composition);
issues.addAll(getAssociatedIssues(mr.getIdElement().getValue()));
issues.addAll(getAssociatedIssues(Ids.simple(mr)));
updateComposition(composition, mr, issues);
codingGapReportBundles.add(
buildCodingGapReportBundle(requestDetails.getFhirServerBase(), composition, issues, mr, author));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.opencds.cqf.ruler.ra.r4;

import ca.uhn.fhir.context.FhirContext;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.MeasureReport;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.opencds.cqf.ruler.Application;
import org.opencds.cqf.ruler.test.RestIntegrationTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { Application.class,
AssistedProviderIT.class }, properties = { "hapi.fhir.fhir_version=r4" })
class AssistedProviderIT extends RestIntegrationTest {
private String serverBase;

@BeforeEach
void beforeEach() {
serverBase = "http://localhost:" + getPort() + "/assisted";
}

@Test
void testAssistedServerStringRequest() {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost request = new HttpPost(serverBase);
request.setEntity(new StringEntity("periodStart,periodEnd,modelId,modelVersion,patientId,ccCode,suspectType,evidenceStatus,evidenceStatusDate,hiearchicalStatus\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,18,historic,closed-gap,2021-04-01,applied-not-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,111,historic,pending,2021-09-29,applied-not-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,24,historic,open-gap,2020-07-15,applied-not-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,112,historic,closed-gap,2021-04-27,applied-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,19,historic,pending,2021-09-27,applied-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,84,historic,open-gap,2020-12-15,applied-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,22,suspected,closed-gap,2021-03-15,applied-not-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,96,suspected,pending,2021-09-27,applied-not-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,110,suspected,open-gap,2020-07-15,applied-not-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,83,net-new,pending,2021-09-28,applied-not-superseded\n" +
"2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,59,historic,open-gap,2020-07-15,applied-not-superseded"));
request.addHeader("Content-Type", "text/csv");

CloseableHttpResponse response = httpClient.execute(request);
String result = EntityUtils.toString(response.getEntity());
validateResult(result);
} catch (IOException ioe) {
fail(ioe.getMessage());
}
}

@Test
void testAssistedServerFileRequest() {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost request = new HttpPost(serverBase);
request.setEntity(new FileEntity(new File(Objects.requireNonNull(this.getClass().getResource("test.csv")).toURI())));
request.addHeader("Content-Type", "text/csv");

CloseableHttpResponse response = httpClient.execute(request);
String result = EntityUtils.toString(response.getEntity());
validateResult(result);
} catch (IOException ioe) {
fail(ioe.getMessage());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

void validateResult(String result) {
IBaseResource resource = FhirContext.forR4Cached().newJsonParser().parseResource(result);
assertTrue(resource instanceof Bundle);
Bundle bundle = (Bundle) resource;
assertTrue(bundle.hasType());
assertEquals(Bundle.BundleType.TRANSACTION, bundle.getType());
assertTrue(bundle.hasEntry());
assertTrue(bundle.getEntryFirstRep().hasResource());
assertTrue(bundle.getEntryFirstRep().getResource() instanceof MeasureReport);
MeasureReport mr = (MeasureReport) bundle.getEntryFirstRep().getResource();
assertTrue(mr.hasStatus() && mr.getStatus() == MeasureReport.MeasureReportStatus.COMPLETE);
assertTrue(mr.hasType() && mr.getType() == MeasureReport.MeasureReportType.INDIVIDUAL);
assertTrue(mr.hasPeriod());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
assertEquals("2021-01-01", formatter.format(mr.getPeriod().getStart()));
assertEquals("2021-09-30", formatter.format(mr.getPeriod().getEnd()));
assertTrue(mr.hasGroup());
assertEquals(11, mr.getGroup().size());
Bundle transaction = transaction(bundle);
assertNotNull(transaction);
}
}
12 changes: 12 additions & 0 deletions plugin/ra/src/test/resources/org/opencds/cqf/ruler/ra/r4/test.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
periodStart,periodEnd,modelId,modelVersion,patientId,ccCode,suspectType,evidenceStatus,evidenceStatusDate,hiearchicalStatus
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,18,historic,closed-gap,2021-04-01,applied-not-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,111,historic,pending,2021-09-29,applied-not-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,24,historic,open-gap,2020-07-15,applied-not-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,112,historic,closed-gap,2021-04-27,applied-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,19,historic,pending,2021-09-27,applied-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,84,historic,open-gap,2020-12-15,applied-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,22,suspected,closed-gap,2021-03-15,applied-not-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,96,suspected,pending,2021-09-27,applied-not-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,110,suspected,open-gap,2020-07-15,applied-not-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,83,net-new,pending,2021-09-28,applied-not-superseded
2021-01-01,2021-09-30,https://build.fhir.org/ig/HL7/davinci-ra/Measure-RAModelExample01,24,ra-patient01,59,historic,open-gap,2020-07-15,applied-not-superseded