$care-gaps functionality integration (#4561)

* care gaps integration

* fix review comments.

* Clean up for HAPI Conventions

* More cleanup to share constants

* More cleanup of tests

* More cleanup

* Update CQL versions

* Added changelog

* fix failing test cases for care gaps.

* WIP care-gaps tests

* implementation of end to end care gaps test case.

* addressing the code review comments.

* addressing comments to bring to hapi-fhir standards.

* added the docs required.

* Adding hapi-fhir-storage-cr module for test coverage inclusion.

* Addressing the comments.

* addressing comments and updated with master.

* Addressing comments for minor changes requested.

---------

Co-authored-by: Jonathan Percival <jonathan.i.percival@gmail.com>
Co-authored-by: Chalma Maadaadi <chalma@alphora.com>
This commit is contained in:
chalmarm 2023-04-11 12:28:14 -04:00 committed by GitHub
parent 3a2b722093
commit 450ccb5599
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 350144 additions and 321 deletions

View File

@ -0,0 +1,4 @@
---
type: add
issue: 4562
title: "Added support for the $care-gaps operation defined by the DaVinci DEQM IG"

View File

@ -197,6 +197,11 @@
<artifactId>hapi-fhir-storage</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-storage-cr</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</profile>
</profiles>

View File

@ -71,6 +71,11 @@
<artifactId>hapi-fhir-structures-dstu3</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -69,7 +69,6 @@ import org.opencds.cqf.cql.evaluator.engine.model.CachingModelResolverDecorator;
import org.opencds.cqf.cql.evaluator.engine.retrieve.BundleRetrieveProvider;
import org.opencds.cqf.cql.evaluator.fhir.Constants;
import org.opencds.cqf.cql.evaluator.fhir.adapter.AdapterFactory;
import org.opencds.cqf.cql.evaluator.fhir.Constants;
import org.opencds.cqf.cql.evaluator.measure.MeasureEvaluationOptions;
import org.opencds.cqf.cql.evaluator.spring.fhir.adapter.AdapterConfiguration;
import org.slf4j.Logger;
@ -111,26 +110,22 @@ public abstract class BaseClinicalReasoningConfig {
@Bean
public CrProperties.CqlProperties cqlProperties(CrProperties theCrProperties) {
return theCrProperties.getCql();
return theCrProperties.getCqlProperties();
}
@Bean
public CrProperties.MeasureProperties measureProperties(CrProperties theCrProperties) {
return theCrProperties.getMeasure();
return theCrProperties.getMeasureProperties();
}
@Bean
public MeasureEvaluationOptions measureEvaluationOptions(CrProperties theCrProperties) {
theCrProperties.getMeasure();
MeasureEvaluationOptions measureEvaluation = theCrProperties.getMeasure().getMeasureEvaluation();
return measureEvaluation;
return theCrProperties.getMeasureProperties().getMeasureEvaluationOptions();
}
@Bean
public CqlOptions cqlOptions(CrProperties theCrProperties) {
return theCrProperties.getCql().getOptions();
return theCrProperties.getCqlProperties().getCqlOptions();
}
@Bean
@ -140,7 +135,7 @@ public abstract class BaseClinicalReasoningConfig {
@Bean
public CqlTranslatorOptions cqlTranslatorOptions(FhirContext theFhirContext, CrProperties.CqlProperties theCqlProperties) {
CqlTranslatorOptions options = theCqlProperties.getOptions().getCqlTranslatorOptions();
CqlTranslatorOptions options = theCqlProperties.getCqlOptions().getCqlTranslatorOptions();
if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.R4)
&& (options.getCompatibilityLevel().equals("1.5") || options.getCompatibilityLevel().equals("1.4"))) {
@ -239,7 +234,7 @@ public abstract class BaseClinicalReasoningConfig {
ModelManager theModelManager, CqlTranslatorOptions theCqlTranslatorOptions, CrProperties.CqlProperties theCqlProperties) {
return lcp -> {
if (theCqlProperties.getOptions().useEmbeddedLibraries()) {
if (theCqlProperties.getCqlOptions().useEmbeddedLibraries()) {
lcp.add(new FhirLibrarySourceProvider());
}

View File

@ -26,80 +26,87 @@ import org.opencds.cqf.cql.evaluator.measure.MeasureEvaluationOptions;
public class CrProperties {
private boolean enabled = true;
private MeasureProperties measureProperties;
private CqlProperties cqlProperties = new CqlProperties();
private boolean myCqlEnabled = true;
private MeasureProperties myMeasureProperties;
private CqlProperties myCqlProperties = new CqlProperties();
public CrProperties () {
this.measureProperties = new MeasureProperties();
this.myMeasureProperties = new MeasureProperties();
};
public boolean isEnabled() {
return enabled;
public boolean isCqlEnabled() {
return myCqlEnabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
public void setCqlEnabled(boolean theCqlEnabled) {
this.myCqlEnabled = theCqlEnabled;
}
public MeasureProperties getMeasure() {
return measureProperties;
public MeasureProperties getMeasureProperties() {
return myMeasureProperties;
}
public void setMeasure(MeasureProperties measureProperties) {
this.measureProperties = measureProperties;
public void setMeasureProperties(MeasureProperties theMeasureProperties) {
this.myMeasureProperties = theMeasureProperties;
}
public CqlProperties getCql() {
return cqlProperties;
public CqlProperties getCqlProperties() {
return myCqlProperties;
}
public void setCql(CqlProperties cqlProperties) {
this.cqlProperties = cqlProperties;
public void setCqlProperties(CqlProperties theCqlProperties) {
this.myCqlProperties = theCqlProperties;
}
public static class MeasureProperties {
private boolean threadedCareGapsEnabled = true;
private MeasureReportConfiguration measureReportConfiguration;
private MeasureEvaluationOptions measureEvaluationOptions;
private boolean myThreadedCareGapsEnabled = true;
private MeasureReportConfiguration myMeasureReportConfiguration;
private MeasureEvaluationOptions myMeasureEvaluationOptions;
public static final int DEFAULT_THREADS_FOR_MEASURE_EVAL = 4;
public static final int DEFAULT_THREADS_BATCH_SIZE = 250;
public static final boolean DEFAULT_THREADS_ENABLED_FOR_MEASURE_EVAL = true;
public MeasureProperties() {
measureEvaluationOptions = MeasureEvaluationOptions.defaultOptions();
measureEvaluationOptions.setNumThreads(4);
measureEvaluationOptions.setThreadedBatchSize(250);
measureEvaluationOptions.setThreadedEnabled(true);
myMeasureEvaluationOptions = MeasureEvaluationOptions.defaultOptions();
myMeasureEvaluationOptions.setNumThreads(DEFAULT_THREADS_FOR_MEASURE_EVAL);
myMeasureEvaluationOptions.setThreadedBatchSize(DEFAULT_THREADS_BATCH_SIZE);
myMeasureEvaluationOptions.setThreadedEnabled(DEFAULT_THREADS_ENABLED_FOR_MEASURE_EVAL);
};
//eval options
public MeasureEvaluationOptions getMeasureEvaluation() {
return this.measureEvaluationOptions;
}
public void setMeasureEvaluation(MeasureEvaluationOptions measureEvaluation) {
this.measureEvaluationOptions = measureEvaluation;
}
//care gaps
public boolean getThreadedCareGapsEnabled() {
return threadedCareGapsEnabled;
return myThreadedCareGapsEnabled;
}
public void setThreadedCareGapsEnabled(boolean enabled) {
this.threadedCareGapsEnabled = enabled;
public void setThreadedCareGapsEnabled(boolean theThreadedCareGapsEnabled) {
myThreadedCareGapsEnabled = theThreadedCareGapsEnabled;
}
public boolean isThreadedCareGapsEnabled() {
return myThreadedCareGapsEnabled;
}
//report configuration
public MeasureReportConfiguration getMeasureReport() {
return this.measureReportConfiguration;
public MeasureReportConfiguration getMeasureReportConfiguration() {
return myMeasureReportConfiguration;
}
public void setMeasureReport(MeasureReportConfiguration measureReport) {
this.measureReportConfiguration = measureReport;
public void setMeasureReportConfiguration(MeasureReportConfiguration theMeasureReport) {
myMeasureReportConfiguration = theMeasureReport;
}
//measure evaluations
public void setMeasureEvaluationOptions(MeasureEvaluationOptions theMeasureEvaluation) {
myMeasureEvaluationOptions = theMeasureEvaluation;
}
public MeasureEvaluationOptions getMeasureEvaluationOptions() {
return myMeasureEvaluationOptions;
}
public static class MeasureReportConfiguration {
/**
@ -112,7 +119,7 @@ public class CrProperties {
* <a href="http://build.fhir.org/ig/HL7/davinci-deqm/index.html">Da Vinci DEQM
* FHIR Implementation Guide</a>.
**/
private String careGapsReporter;
private String myCareGapsReporter;
/**
* Implements the author element of the <a href=
* "http://www.hl7.org/fhir/composition.html">Composition</a> FHIR
@ -123,22 +130,22 @@ public class CrProperties {
* <a href="http://build.fhir.org/ig/HL7/davinci-deqm/index.html">Da Vinci DEQM
* FHIR Implementation Guide</a>.
**/
private String careGapsCompositionSectionAuthor;
private String myCareGapsCompositionSectionAuthor;
public String getReporter() {
return careGapsReporter;
public String getCareGapsReporter() {
return myCareGapsReporter;
}
public void setCareGapsReporter(String careGapsReporter) {
this.careGapsReporter = null;// ResourceBuilder.ensureOrganizationReference(careGapsReporter);
public void setCareGapsReporter(String theCareGapsReporter) {
myCareGapsReporter = theCareGapsReporter;
}
public String getCompositionAuthor() {
return careGapsCompositionSectionAuthor;
public String getCareGapsCompositionSectionAuthor() {
return myCareGapsCompositionSectionAuthor;
}
public void setCareGapsCompositionSectionAuthor(String careGapsCompositionSectionAuthor) {
this.careGapsCompositionSectionAuthor = careGapsCompositionSectionAuthor;
public void setCareGapsCompositionSectionAuthor(String theCareGapsCompositionSectionAuthor) {
myCareGapsCompositionSectionAuthor = theCareGapsCompositionSectionAuthor;
}
}
@ -147,41 +154,41 @@ public class CrProperties {
public static class CqlProperties {
private boolean useEmbeddedLibraries = true;
private boolean myCqlUseOfEmbeddedLibraries = true;
private CqlEngineOptions runtimeOptions = CqlEngineOptions.defaultOptions();
private CqlTranslatorOptions compilerOptions = CqlTranslatorOptions.defaultOptions();
private CqlEngineOptions myCqlRuntimeOptions = CqlEngineOptions.defaultOptions();
private CqlTranslatorOptions myCqlTranslatorOptions = CqlTranslatorOptions.defaultOptions();
public boolean useEmbeddedLibraries() {
return this.useEmbeddedLibraries;
public boolean isCqlUseOfEmbeddedLibraries() {
return myCqlUseOfEmbeddedLibraries;
}
public void setUseEmbeddedLibraries(boolean useEmbeddedLibraries) {
this.useEmbeddedLibraries = useEmbeddedLibraries;
public void setCqlUseOfEmbeddedLibraries(boolean theCqlUseOfEmbeddedLibraries) {
myCqlUseOfEmbeddedLibraries = theCqlUseOfEmbeddedLibraries;
}
public CqlEngineOptions getRuntime() {
return this.runtimeOptions;
public CqlEngineOptions getCqlRuntimeOptions() {
return myCqlRuntimeOptions;
}
public void setRuntime(CqlEngineOptions runtime) {
this.runtimeOptions = runtime;
public void setCqlRuntimeOptions(CqlEngineOptions theRuntime) {
myCqlRuntimeOptions = theRuntime;
}
public CqlTranslatorOptions getCompiler() {
return this.compilerOptions;
public CqlTranslatorOptions getCqlTranslatorOptions() {
return myCqlTranslatorOptions;
}
public void setCompiler(CqlTranslatorOptions compiler) {
this.compilerOptions = compiler;
public void setCqlTranslatorOptions(CqlTranslatorOptions theCqlTranslatorOptions) {
myCqlTranslatorOptions = theCqlTranslatorOptions;
}
public CqlOptions getOptions() {
public CqlOptions getCqlOptions() {
CqlOptions cqlOptions = new CqlOptions();
cqlOptions.setUseEmbeddedLibraries(this.useEmbeddedLibraries());
cqlOptions.setCqlEngineOptions(this.getRuntime());
cqlOptions.setCqlTranslatorOptions(this.getCompiler());
cqlOptions.setUseEmbeddedLibraries(isCqlUseOfEmbeddedLibraries());
cqlOptions.setCqlEngineOptions(getCqlRuntimeOptions());
cqlOptions.setCqlTranslatorOptions(getCqlTranslatorOptions());
return cqlOptions;
}
}

View File

@ -19,8 +19,14 @@
*/
package ca.uhn.fhir.cr.config;
import ca.uhn.fhir.cr.r4.measure.CareGapsOperationProvider;
import ca.uhn.fhir.cr.r4.measure.CareGapsService;
import ca.uhn.fhir.cr.r4.measure.ISubmitDataService;
import ca.uhn.fhir.cr.r4.measure.MeasureOperationsProvider;
import ca.uhn.fhir.cr.r4.measure.MeasureService;
import ca.uhn.fhir.cr.r4.measure.SubmitDataProvider;
import ca.uhn.fhir.cr.r4.measure.SubmitDataService;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
@ -51,4 +57,31 @@ public class CrR4Config extends BaseClinicalReasoningConfig {
public MeasureOperationsProvider r4measureOperationsProvider() {
return new MeasureOperationsProvider();
}
@Bean
public Function<RequestDetails, CareGapsService> r4CareGapsServiceFactory(Function<RequestDetails, MeasureService> theR4MeasureServiceFactory,
CrProperties theCrProperties,
DaoRegistry theDaoRegistry) {
return r -> {
var ms = theR4MeasureServiceFactory.apply(r);
var cs = new CareGapsService(theCrProperties, ms, theDaoRegistry, cqlExecutor(), r);
return cs;
};
}
@Bean
public CareGapsOperationProvider r4CareGapsProvider(Function<RequestDetails, CareGapsService> theCareGapsServiceFunction){
return new CareGapsOperationProvider(theCareGapsServiceFunction);
}
@Bean
public ISubmitDataService r4SubmitDataService(DaoRegistry theDaoRegistry){
return requestDetails -> new SubmitDataService(theDaoRegistry, requestDetails);
}
@Bean
public SubmitDataProvider r4SubmitDataProvider(ISubmitDataService theSubmitDataService){
return new SubmitDataProvider(theSubmitDataService);
}
}

View File

@ -0,0 +1,12 @@
package ca.uhn.fhir.cr.constant;
public class CareCapsConstants {
private CareCapsConstants(){}
public static final String CARE_GAPS_REPORT_PROFILE = "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/indv-measurereport-deqm";
public static final String CARE_GAPS_BUNDLE_PROFILE = "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/gaps-bundle-deqm";
public static final String CARE_GAPS_COMPOSITION_PROFILE = "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_GAP_STATUS_EXTENSION = "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-gapStatus";
public static final String CARE_GAPS_GAP_STATUS_SYSTEM = "http://hl7.org/fhir/us/davinci-deqm/CodeSystem/gaps-status";
}

View File

@ -0,0 +1,9 @@
package ca.uhn.fhir.cr.constant;
public class HtmlConstants {
private HtmlConstants(){}
public static final String HTML_DIV_CONTENT = "<div xmlns=\"http://www.w3.org/1999/xhtml\">%s</div>";
public static final String HTML_PARAGRAPH_CONTENT = "<p>%s</p>";
public static final String HTML_DIV_PARAGRAPH_CONTENT = String.format(HTML_DIV_CONTENT, HTML_PARAGRAPH_CONTENT);
}

View File

@ -17,13 +17,24 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.common;
package ca.uhn.fhir.cr.constant;
public class SupplementalDataConstants {
import java.sql.Date;
import java.time.LocalDate;
private SupplementalDataConstants() {}
public class MeasureReportConstants {
private MeasureReportConstants() {}
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";
public static final String MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION = "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-supplementalData";
public static final String MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL = "http://hl7.org/fhir/us/davinci-deqm/SearchParameter/measurereport-supplemental-data";
public static final String MEASUREREPORT_PRODUCT_LINE_EXT_URL = "http://hl7.org/fhir/us/cqframework/cqfmeasures/StructureDefinition/cqfm-productLine";
public static final String MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION = "0.1.0";
public static final Date MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_DEFINITION_DATE = Date.valueOf(LocalDate.of(2022, 7, 20));
public static final String COUNTRY_CODING_SYSTEM_CODE = "urn:iso:std:iso:3166";
public static final String US_COUNTRY_CODE = "US";
public static final String US_COUNTRY_DISPLAY = "United States of America";
}

View File

@ -1,100 +0,0 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.dstu3;
import ca.uhn.fhir.cr.common.IDaoRegistryUser;
import ca.uhn.fhir.cr.common.Searches;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.ContactDetail;
import org.hl7.fhir.dstu3.model.ContactPoint;
import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus;
import org.hl7.fhir.dstu3.model.Enumerations.SearchParamType;
import org.hl7.fhir.dstu3.model.SearchParameter;
import org.hl7.fhir.dstu3.model.SearchParameter.XPathUsageType;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import static ca.uhn.fhir.cr.common.SupplementalDataConstants.MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION;
import static ca.uhn.fhir.cr.common.SupplementalDataConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL;
import static ca.uhn.fhir.cr.common.SupplementalDataConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION;
public interface ISupplementalDataSearchParamUser extends IDaoRegistryUser {
List<ContactDetail> CQI_CONTACT_DETAIL = Collections.singletonList(
new ContactDetail()
.addTelecom(
new ContactPoint()
.setSystem(ContactPoint.ContactPointSystem.URL)
.setValue("http://www.hl7.org/Special/committees/cqi/index.cfm")));
static String CODING_SYSTEM_CODE = "urn:iso:std:iso:3166";
static String CODING_COUNTRY_CODE = "US";
static String CODING_COUNTRY_DISPLAY = "United States of America";
List<CodeableConcept> US_JURISDICTION_CODING = Collections.singletonList(
new CodeableConcept()
.addCoding(
new Coding(CODING_SYSTEM_CODE, CODING_COUNTRY_CODE, CODING_COUNTRY_DISPLAY)));
default void ensureSupplementalDataElementSearchParameter(RequestDetails theRequestDetails) {
if (search(SearchParameter.class,
Searches.byUrlAndVersion(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL,
MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION),
theRequestDetails).iterator().hasNext()) {
return;
}
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(2022, 7, 20);
SearchParameter searchParameter = new SearchParameter()
.setUrl(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL)
.setVersion(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION)
.setName("DEQMMeasureReportSupplementalData")
.setStatus(PublicationStatus.ACTIVE)
.setDate(calendar.getTime())
.setPublisher("HL7 International - Clinical Quality Information Work Group")
.setContact(CQI_CONTACT_DETAIL)
.setDescription(
String.format(
"Returns resources (supplemental data) from references on extensions on the MeasureReport with urls matching %s.",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setJurisdiction(US_JURISDICTION_CODING)
.addBase("MeasureReport")
.setCode("supplemental-data")
.setType(SearchParamType.REFERENCE)
.setExpression(
String.format("MeasureReport.extension('%s').value",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpath(
String.format("f:MeasureReport/f:extension[@url='%s'].value",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpathUsage(XPathUsageType.NORMAL);
searchParameter.setId("deqm-measurereport-supplemental-data");
searchParameter.setTitle("Supplemental Data");
create(searchParameter, theRequestDetails);
}
}

View File

@ -19,21 +19,28 @@
*/
package ca.uhn.fhir.cr.dstu3.measure;
import ca.uhn.fhir.cr.common.IDaoRegistryUser;
import ca.uhn.fhir.cr.common.IDataProviderFactory;
import ca.uhn.fhir.cr.common.IFhirDalFactory;
import ca.uhn.fhir.cr.common.ILibrarySourceProviderFactory;
import ca.uhn.fhir.cr.common.ITerminologyProviderFactory;
import ca.uhn.fhir.cr.dstu3.ISupplementalDataSearchParamUser;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.util.BundleBuilder;
import org.cqframework.cql.cql2elm.LibrarySourceProvider;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.ContactDetail;
import org.hl7.fhir.dstu3.model.ContactPoint;
import org.hl7.fhir.dstu3.model.Endpoint;
import org.hl7.fhir.dstu3.model.Enumerations;
import org.hl7.fhir.dstu3.model.Extension;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Measure;
import org.hl7.fhir.dstu3.model.MeasureReport;
import org.hl7.fhir.dstu3.model.SearchParameter;
import org.hl7.fhir.dstu3.model.StringType;
import org.opencds.cqf.cql.engine.data.DataProvider;
import org.opencds.cqf.cql.engine.fhir.terminology.Dstu3FhirTerminologyProvider;
@ -44,9 +51,57 @@ import org.opencds.cqf.cql.evaluator.fhir.util.Clients;
import org.opencds.cqf.cql.evaluator.measure.MeasureEvaluationOptions;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class MeasureService implements ISupplementalDataSearchParamUser {
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.COUNTRY_CODING_SYSTEM_CODE;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_DEFINITION_DATE;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.US_COUNTRY_CODE;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.US_COUNTRY_DISPLAY;
public class MeasureService implements IDaoRegistryUser {
public static final List<ContactDetail> CQI_CONTACT_DETAIL = Collections.singletonList(
new ContactDetail()
.addTelecom(
new ContactPoint()
.setSystem(ContactPoint.ContactPointSystem.URL)
.setValue("http://www.hl7.org/Special/committees/cqi/index.cfm")));
public static final List<CodeableConcept> US_JURISDICTION_CODING = Collections.singletonList(
new CodeableConcept()
.addCoding(
new Coding(COUNTRY_CODING_SYSTEM_CODE, US_COUNTRY_CODE, US_COUNTRY_DISPLAY)));
public static final SearchParameter SUPPLEMENTAL_DATA_SEARCHPARAMETER = (SearchParameter) new SearchParameter()
.setUrl(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL)
.setVersion(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION)
.setName("DEQMMeasureReportSupplementalData")
.setStatus(Enumerations.PublicationStatus.ACTIVE)
.setDate(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_DEFINITION_DATE)
.setPublisher("HL7 International - Clinical Quality Information Work Group")
.setContact(CQI_CONTACT_DETAIL)
.setDescription(
String.format(
"Returns resources (supplemental data) from references on extensions on the MeasureReport with urls matching %s.",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setJurisdiction(US_JURISDICTION_CODING)
.addBase("MeasureReport")
.setCode("supplemental-data")
.setType(Enumerations.SearchParamType.REFERENCE)
.setExpression(
String.format("MeasureReport.extension('%s').value",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpath(
String.format("f:MeasureReport/f:extension[@url='%s'].value",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpathUsage(SearchParameter.XPathUsageType.NORMAL)
.setTitle("Supplemental Data")
.setId("deqm-measurereport-supplemental-data");
@Autowired
protected ITerminologyProviderFactory myTerminologyProviderFactory;
@ -121,7 +176,7 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
Bundle theAdditionalData,
Endpoint theTerminologyEndpoint) {
ensureSupplementalDataElementSearchParameter(myRequestDetails);
ensureSupplementalDataElementSearchParameter();
Measure measure = read(theId, myRequestDetails);
@ -161,4 +216,12 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
return this.myDaoRegistry;
}
protected void ensureSupplementalDataElementSearchParameter() {
//create a transaction bundle
BundleBuilder builder = new BundleBuilder(getFhirContext());
//set the request to be condition on code == supplemental data
builder.addTransactionCreateEntry(SUPPLEMENTAL_DATA_SEARCHPARAMETER).conditional("code=supplemental-data");
transaction(builder.getBundle(), this.myRequestDetails);
}
}

View File

@ -0,0 +1,34 @@
package ca.uhn.fhir.cr.enumeration;
import ca.uhn.fhir.i18n.Msg;
public enum CareGapsStatusCode {
OPEN_GAP("open-gap"), CLOSED_GAP("closed-gap"), NOT_APPLICABLE("not-applicable");
private final String myValue;
CareGapsStatusCode(final String theValue) {
myValue = theValue;
}
@Override
public String toString() {
return myValue;
}
public String toDisplayString() {
if (myValue.equals("open-gap")) {
return "Open Gap";
}
if (myValue.equals("closed-gap")) {
return "Closed Gap";
}
if (myValue.equals("not-applicable")) {
return "Not Applicable";
}
throw new RuntimeException(Msg.code(2301) + "Error getting display strings for care gaps status codes");
}
}

View File

@ -1,97 +0,0 @@
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.cr.r4;
import ca.uhn.fhir.cr.common.IDaoRegistryUser;
import ca.uhn.fhir.cr.common.Searches;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.ContactDetail;
import org.hl7.fhir.r4.model.ContactPoint;
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
import org.hl7.fhir.r4.model.Enumerations.SearchParamType;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.SearchParameter.XPathUsageType;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import static ca.uhn.fhir.cr.common.SupplementalDataConstants.MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION;
import static ca.uhn.fhir.cr.common.SupplementalDataConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL;
import static ca.uhn.fhir.cr.common.SupplementalDataConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION;
public interface ISupplementalDataSearchParamUser extends IDaoRegistryUser {
List<ContactDetail> CQI_CONTACTDETAIL = Collections.singletonList(
new ContactDetail()
.addTelecom(
new ContactPoint()
.setSystem(ContactPoint.ContactPointSystem.URL)
.setValue("http://www.hl7.org/Special/committees/cqi/index.cfm")));
List<CodeableConcept> US_JURISDICTION_CODING = Collections.singletonList(
new CodeableConcept()
.addCoding(
new Coding("urn:iso:std:iso:3166", "US", "United States of America")));
default void ensureSupplementalDataElementSearchParameter(RequestDetails theRequestDetails) {
if (search(SearchParameter.class,
Searches.byUrlAndVersion(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL,
MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION),
theRequestDetails).iterator().hasNext()) {
return;
}
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(2022, 7, 20);
SearchParameter searchParameter = new SearchParameter()
.setUrl(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL)
.setVersion(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION)
.setName("DEQMMeasureReportSupplementalData")
.setStatus(PublicationStatus.ACTIVE)
.setDate(calendar.getTime())
.setPublisher("HL7 International - Clinical Quality Information Work Group")
.setContact(CQI_CONTACTDETAIL)
.setDescription(
String.format(
"Returns resources (supplemental data) from references on extensions on the MeasureReport with urls matching %s.",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setJurisdiction(US_JURISDICTION_CODING)
.addBase("MeasureReport")
.setCode("supplemental-data")
.setType(SearchParamType.REFERENCE)
.setExpression(
String.format("MeasureReport.extension('%s').value",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpath(
String.format("f:MeasureReport/f:extension[@url='%s'].value",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpathUsage(XPathUsageType.NORMAL);
searchParameter.setId("deqm-measurereport-supplemental-data");
searchParameter.setTitle("Supplemental Data");
create(searchParameter, theRequestDetails);
}
}

View File

@ -0,0 +1,108 @@
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.Parameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
public class CareGapsOperationProvider {
private static final Logger ourLog = LoggerFactory.getLogger(CareGapsOperationProvider.class);
Function<RequestDetails, CareGapsService> myCareGapsServiceFunction;
public CareGapsOperationProvider(Function<RequestDetails, CareGapsService> theCareGapsServiceFunction) {
this.myCareGapsServiceFunction = theCareGapsServiceFunction;
}
/**
* Implements the <a href=
* "http://build.fhir.org/ig/HL7/davinci-deqm/OperationDefinition-care-gaps.html">$care-gaps</a>
* operation found in the
* <a href="http://build.fhir.org/ig/HL7/davinci-deqm/index.html">Da Vinci DEQM
* FHIR Implementation Guide</a> that overrides the <a href=
* "http://build.fhir.org/operation-measure-care-gaps.html">$care-gaps</a>
* operation found in the
* <a href="http://hl7.org/fhir/R4/clinicalreasoning-module.html">FHIR Clinical
* Reasoning Module</a>.
*
* The operation calculates measures describing gaps in care. For more details,
* reference the <a href=
* "http://build.fhir.org/ig/HL7/davinci-deqm/gaps-in-care-reporting.html">Gaps
* in Care Reporting</a> section of the
* <a href="http://build.fhir.org/ig/HL7/davinci-deqm/index.html">Da Vinci DEQM
* FHIR Implementation Guide</a>.
*
* A Parameters resource that includes zero to many document bundles that
* include Care Gap Measure Reports will be returned.
*
* Usage:
* URL: [base]/Measure/$care-gaps
*
* @param theRequestDetails generally auto-populated by the HAPI server
* framework.
* @param thePeriodStart the start of the gaps through period
* @param thePeriodEnd the end of the gaps through period
* @param theTopic the category of the measures that is of interest for
* the care gaps report
* @param theSubject a reference to either a Patient or Group for which
* the gaps in care report(s) will be generated
* @param thePractitioner a reference to a Practitioner for which the gaps in
* care report(s) will be generated
* @param theOrganization a reference to an Organization for which the gaps in
* care report(s) will be generated
* @param theStatus the status code of gaps in care reports that will be
* included in the result
* @param theMeasureId the id of Measure(s) for which the gaps in care
* report(s) will be calculated
* @param theMeasureIdentifier the identifier of Measure(s) for which the gaps in
* care report(s) will be calculated
* @param theMeasureUrl the canonical URL of Measure(s) for which the gaps
* in care report(s) will be calculated
* @param theProgram the program that a provider (either clinician or
* clinical organization) participates in
* @return Parameters of bundles of Care Gap Measure Reports
*/
@Description(shortDefinition = "$care-gaps operation", value = "Implements the <a href=\"http://build.fhir.org/ig/HL7/davinci-deqm/OperationDefinition-care-gaps.html\">$care-gaps</a> operation found in the <a href=\"http://build.fhir.org/ig/HL7/davinci-deqm/index.html\">Da Vinci DEQM FHIR Implementation Guide</a> which is an extension of the <a href=\"http://build.fhir.org/operation-measure-care-gaps.html\">$care-gaps</a> operation found in the <a href=\"http://hl7.org/fhir/R4/clinicalreasoning-module.html\">FHIR Clinical Reasoning Module</a>.")
@Operation(name = "$care-gaps", idempotent = false, type = Measure.class)
public Parameters careGapsReport(
RequestDetails theRequestDetails,
@OperationParam(name = "periodStart", typeName = "date") IPrimitiveType<Date> thePeriodStart,
@OperationParam(name = "periodEnd", typeName = "date") IPrimitiveType<Date> thePeriodEnd,
@OperationParam(name = "topic") List<String> theTopic,
@OperationParam(name = "subject") String theSubject,
@OperationParam(name = "practitioner") String thePractitioner,
@OperationParam(name = "organization") String theOrganization,
@OperationParam(name = "status") List<String> theStatus,
@OperationParam(name = "measureId") List<String> theMeasureId,
@OperationParam(name = "measureIdentifier") List<String> theMeasureIdentifier,
@OperationParam(name = "measureUrl") List<CanonicalType> theMeasureUrl,
@OperationParam(name = "program") List<String> theProgram) {
return myCareGapsServiceFunction
.apply(theRequestDetails)
.getCareGapsReport(
thePeriodStart,
thePeriodEnd,
theTopic,
theSubject,
thePractitioner,
theOrganization,
theStatus,
theMeasureId,
theMeasureIdentifier,
theMeasureUrl,
theProgram
);
}
}

View File

@ -0,0 +1,521 @@
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.cr.enumeration.CareGapsStatusCode;
import ca.uhn.fhir.cr.common.IDaoRegistryUser;
import ca.uhn.fhir.cr.common.Searches;
import ca.uhn.fhir.cr.config.CrProperties;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import com.google.common.base.Strings;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.DetectedIssue;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Group;
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.Organization;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.cql.evaluator.fhir.builder.BundleBuilder;
import org.opencds.cqf.cql.evaluator.fhir.builder.CodeableConceptSettings;
import org.opencds.cqf.cql.evaluator.fhir.builder.CompositionBuilder;
import org.opencds.cqf.cql.evaluator.fhir.builder.CompositionSectionComponentBuilder;
import org.opencds.cqf.cql.evaluator.fhir.builder.DetectedIssueBuilder;
import org.opencds.cqf.cql.evaluator.fhir.builder.NarrativeSettings;
import org.opencds.cqf.cql.evaluator.fhir.util.Ids;
import org.opencds.cqf.cql.evaluator.fhir.util.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import static ca.uhn.fhir.cr.constant.CareCapsConstants.CARE_GAPS_BUNDLE_PROFILE;
import static ca.uhn.fhir.cr.constant.CareCapsConstants.CARE_GAPS_COMPOSITION_PROFILE;
import static ca.uhn.fhir.cr.constant.CareCapsConstants.CARE_GAPS_DETECTED_ISSUE_PROFILE;
import static ca.uhn.fhir.cr.constant.CareCapsConstants.CARE_GAPS_GAP_STATUS_EXTENSION;
import static ca.uhn.fhir.cr.constant.CareCapsConstants.CARE_GAPS_GAP_STATUS_SYSTEM;
import static ca.uhn.fhir.cr.constant.CareCapsConstants.CARE_GAPS_REPORT_PROFILE;
import static ca.uhn.fhir.cr.constant.HtmlConstants.HTML_DIV_PARAGRAPH_CONTENT;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_MEASURE_POPULATION_SYSTEM;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Map.ofEntries;
import static org.hl7.fhir.r4.model.Factory.newId;
import static org.opencds.cqf.cql.evaluator.fhir.util.Resources.newResource;
public class CareGapsService implements IDaoRegistryUser {
private static final Logger ourLog = LoggerFactory.getLogger(CareGapsService.class);
public static final Map<String, CodeableConceptSettings> CARE_GAPS_CODES = ofEntries(
new AbstractMap.SimpleEntry<>("http://loinc.org/96315-7",
new CodeableConceptSettings().add(
"http://loinc.org", "96315-7", "Gaps in care report")),
new AbstractMap.SimpleEntry<>("http://terminology.hl7.org/CodeSystem/v3-ActCode/CAREGAP",
new CodeableConceptSettings().add(
"http://terminology.hl7.org/CodeSystem/v3-ActCode", "CAREGAP", "Care Gaps")));
private RequestDetails myRequestDetails;
private CrProperties myCrProperties;
private MeasureService myR4MeasureService;
private Executor myCqlExecutor;
private DaoRegistry myDaoRegistry;
private final Map<String, Resource> myConfiguredResources = new HashMap<>();
public CareGapsService(CrProperties theCrProperties,
MeasureService theMeasureService,
DaoRegistry theDaoRegistry,
Executor theExecutor,
RequestDetails theRequestDetails){
this.myDaoRegistry = theDaoRegistry;
this.myCrProperties = theCrProperties;
this.myR4MeasureService = theMeasureService;
this.myCqlExecutor = theExecutor;
this.myRequestDetails = theRequestDetails;
}
/**
* Calculate measures describing gaps in care
* @param thePeriodStart
* @param thePeriodEnd
* @param theTopic
* @param theSubject
* @param thePractitioner
* @param theOrganization
* @param theStatuses
* @param theMeasureIds
* @param theMeasureIdentifiers
* @param theMeasureUrls
* @param thePrograms
* @return Parameters that includes zero to many document bundles that
* include Care Gap Measure Reports will be returned.
*/
public Parameters getCareGapsReport(IPrimitiveType<Date> thePeriodStart,
IPrimitiveType<Date> thePeriodEnd,
List<String> theTopic,
String theSubject,
String thePractitioner,
String theOrganization,
List<String> theStatuses,
List<String> theMeasureIds,
List<String> theMeasureIdentifiers,
List<CanonicalType> theMeasureUrls,
List<String> thePrograms) {
validateConfiguration();
List<Measure> measures = ensureMeasures(getMeasures(theMeasureIds, theMeasureIdentifiers, theMeasureUrls, myRequestDetails));
List<Patient> patients;
if (!Strings.isNullOrEmpty(theSubject)) {
patients = getPatientListFromSubject(theSubject);
} else {
throw new NotImplementedOperationException(Msg.code(2275) + "Only the subject parameter has been implemented.");
}
List<CompletableFuture<Parameters.ParametersParameterComponent>> futures = new ArrayList<>();
Parameters result = initializeResult();
if (myCrProperties.getMeasureProperties().getThreadedCareGapsEnabled()) {
patients
.forEach(
patient -> {
Parameters.ParametersParameterComponent patientReports = patientReports(myRequestDetails,
thePeriodStart.getValueAsString(), thePeriodEnd.getValueAsString(), patient, theStatuses, measures,
theOrganization);
futures.add(CompletableFuture.supplyAsync(() -> patientReports, myCqlExecutor));
});
futures.forEach(x -> result.addParameter(x.join()));
} else {
patients.forEach(
patient -> {
Parameters.ParametersParameterComponent patientReports = patientReports(myRequestDetails,
thePeriodStart.getValueAsString(), thePeriodEnd.getValueAsString(), patient, theStatuses, measures,
theOrganization);
if (patientReports != null) {
result.addParameter(patientReports);
}
});
}
return result;
}
public void validateConfiguration() {
checkNotNull(myCrProperties.getMeasureProperties(),
"The measure_report setting properties are required for the $care-gaps operation.");
checkNotNull(myCrProperties.getMeasureProperties().getMeasureReportConfiguration(),
"The measure_report setting is required for the $care-gaps operation.");
checkArgument(!Strings.isNullOrEmpty(myCrProperties.getMeasureProperties().getMeasureReportConfiguration().getCareGapsReporter()),
"The measure_report.care_gaps_reporter setting is required for the $care-gaps operation.");
checkArgument(!Strings.isNullOrEmpty(myCrProperties.getMeasureProperties().getMeasureReportConfiguration().getCareGapsCompositionSectionAuthor()),
"The measure_report.care_gaps_composition_section_author setting is required for the $care-gaps operation.");
Resource configuredReporter = addConfiguredResource(Organization.class,
myCrProperties.getMeasureProperties().getMeasureReportConfiguration().getCareGapsReporter(), "care_gaps_reporter");
Resource configuredAuthor = addConfiguredResource(Organization.class,
myCrProperties.getMeasureProperties().getMeasureReportConfiguration().getCareGapsCompositionSectionAuthor(),
"care_gaps_composition_section_author");
checkNotNull(configuredReporter, String.format(
"The %s Resource is configured as the measure_report.care_gaps_reporter but the Resource could not be read.",
myCrProperties.getMeasureProperties().getMeasureReportConfiguration().getCareGapsReporter()));
checkNotNull(configuredAuthor, String.format(
"The %s Resource is configured as the measure_report.care_gaps_composition_section_author but the Resource could not be read.",
myCrProperties.getMeasureProperties().getMeasureReportConfiguration().getCareGapsCompositionSectionAuthor()));
}
List<Patient> getPatientListFromSubject(String theSubject) {
if (theSubject.startsWith("Patient/")) {
return Collections.singletonList(validatePatientExists(theSubject));
} else if (theSubject.startsWith("Group/")) {
return getPatientListFromGroup(theSubject);
}
ourLog.info("Subject member was not a Patient or a Group, so skipping. \n{}", theSubject);
return Collections.emptyList();
}
List<Patient> getPatientListFromGroup(String theSubjectGroupId) {
List<Patient> patientList = new ArrayList<>();
Group group = read(newId(theSubjectGroupId));
if (group == null) {
throw new IllegalArgumentException(Msg.code(2276) + "Could not find Group: " + theSubjectGroupId);
}
group.getMember().forEach(member -> {
Reference reference = member.getEntity();
if (reference.getReferenceElement().getResourceType().equals("Patient")) {
Patient patient = validatePatientExists(reference.getReference());
patientList.add(patient);
} else if (reference.getReferenceElement().getResourceType().equals("Group")) {
patientList.addAll(getPatientListFromGroup(reference.getReference()));
} else {
ourLog.info("Group member was not a Patient or a Group, so skipping. \n{}", reference.getReference());
}
});
return patientList;
}
Patient validatePatientExists(String thePatientRef) {
Patient patient = read(newId(thePatientRef));
if (patient == null) {
throw new IllegalArgumentException(Msg.code(2277) + "Could not find Patient: " + thePatientRef);
}
return patient;
}
List<Measure> getMeasures(List<String> theMeasureIds, List<String> theMeasureIdentifiers,
List<CanonicalType> theMeasureCanonicals, RequestDetails theRequestDetails) {
boolean hasMeasureIds = theMeasureIds != null && !theMeasureIds.isEmpty();
boolean hasMeasureIdentifiers = theMeasureIdentifiers != null && !theMeasureIdentifiers.isEmpty();
boolean hasMeasureUrls = theMeasureCanonicals != null && !theMeasureCanonicals.isEmpty();
if (!hasMeasureIds && !hasMeasureIdentifiers && !hasMeasureUrls) {
return Collections.emptyList();
}
List<Measure> measureList = new ArrayList<>();
Iterable<IBaseResource> measureSearchResults;
if (hasMeasureIds) {
measureSearchResults = search(Measure.class, Searches.byIds(theMeasureIds), theRequestDetails);
populateMeasures(measureList, measureSearchResults);
}
if(hasMeasureUrls) {
measureSearchResults = search(Measure.class, Searches.byCanonicals(theMeasureCanonicals), theRequestDetails);
populateMeasures(measureList, measureSearchResults);
}
// TODO: implement searching by measure identifiers
if (hasMeasureIdentifiers) {
throw new NotImplementedOperationException(Msg.code(2278) + "Measure identifiers have not yet been implemented.");
}
Map<String, Measure> result = new HashMap<>();
measureList.forEach(measure -> result.putIfAbsent(measure.getUrl(), measure));
return new ArrayList<>(result.values());
}
private void populateMeasures(List<Measure> measureList, Iterable<IBaseResource> measureSearchResults) {
if(measureSearchResults != null){
Iterator<IBaseResource> measures = measureSearchResults.iterator();
while(measures.hasNext()){
measureList.add((Measure)measures.next());
}
}
}
private <T extends Resource> T addConfiguredResource(Class<T> theResourceClass, String theId, String theKey) {
//T resource = repo.search(theResourceClass, Searches.byId(theId)).firstOrNull();
Iterable<IBaseResource> resourceResult = search(theResourceClass, Searches.byId(theId), myRequestDetails);
T resource = null;
if(resourceResult != null){
Iterator<IBaseResource> resources = resourceResult.iterator();
while(resources.hasNext()){
resource = (T) resources.next();
break;
}
if (resource != null) {
myConfiguredResources.put(theKey, resource);
}
}
return resource;
}
private List<Measure> ensureMeasures(List<Measure> theMeasures) {
theMeasures.forEach(measure -> {
if (!measure.hasScoring()) {
ourLog.info("Measure does not specify a scoring so skipping: {}.", measure.getId());
theMeasures.remove(measure);
}
if (!measure.hasImprovementNotation()) {
ourLog.info("Measure does not specify an improvement notation so skipping: {}.", measure.getId());
theMeasures.remove(measure);
}
});
return theMeasures;
}
private Parameters.ParametersParameterComponent patientReports(RequestDetails theRequestDetails, String thePeriodStart,
String thePeriodEnd, Patient thePatient, List<String> theStatuses, List<Measure> theMeasures, String theOrganization) {
// TODO: add organization to report, if it exists.
Composition composition = getComposition(thePatient);
List<DetectedIssue> detectedIssues = new ArrayList<>();
Map<String, Resource> evalPlusSDE = new HashMap<>();
List<MeasureReport> reports = getReports(theRequestDetails, thePeriodStart, thePeriodEnd, thePatient, theStatuses, theMeasures,
composition, detectedIssues, evalPlusSDE);
if (reports.isEmpty()) {
return null;
}
return initializePatientParameter(thePatient).setResource(
addBundleEntries(theRequestDetails.getFhirServerBase(), composition, detectedIssues, reports, evalPlusSDE));
}
private List<MeasureReport> getReports(RequestDetails theRequestDetails, String thePeriodStart, String thePeriodEnd,
Patient thePatient, List<String> theStatuses, List<Measure> theMeasures, Composition theComposition,
List<DetectedIssue> theDetectedIssues, Map<String, Resource> theEvalPlusSDEs) {
List<MeasureReport> reports = new ArrayList<>();
MeasureReport report;
for (Measure measure : theMeasures) {
report = myR4MeasureService.evaluateMeasure(measure.getIdElement(), thePeriodStart,
thePeriodEnd, "patient", Ids.simple(thePatient), null, null, null, null, null);
if (!report.hasGroup()) {
ourLog.info("Report does not include a group so skipping.\nSubject: {}\nMeasure: {}",
Ids.simple(thePatient),
Ids.simplePart(measure));
continue;
}
initializeReport(report);
CareGapsStatusCode gapStatus = getGapStatus(measure, report);
if (!theStatuses.contains(gapStatus.toString())) {
continue;
}
DetectedIssue detectedIssue = getDetectedIssue(thePatient, report, gapStatus);
theDetectedIssues.add(detectedIssue);
theComposition.addSection(getSection(measure, report, detectedIssue, gapStatus));
populateEvaluatedResources(report, theEvalPlusSDEs);
populateSDEResources(report, theEvalPlusSDEs);
reports.add(report);
}
return reports;
}
private void initializeReport(MeasureReport theMeasureReport) {
if (Strings.isNullOrEmpty(theMeasureReport.getId())) {
IIdType id = Ids.newId(MeasureReport.class, UUID.randomUUID().toString());
theMeasureReport.setId(id);
}
Reference reporter = new Reference().setReference(myCrProperties.getMeasureProperties().getMeasureReportConfiguration().getCareGapsReporter());
// TODO: figure out what this extension is for
// reporter.addExtension(new
// Extension().setUrl(CARE_GAPS_MEASUREREPORT_REPORTER_EXTENSION));
theMeasureReport.setReporter(reporter);
if (theMeasureReport.hasMeta()) {
theMeasureReport.getMeta().addProfile(CARE_GAPS_REPORT_PROFILE);
} else {
theMeasureReport.setMeta(new Meta().addProfile(CARE_GAPS_REPORT_PROFILE));
}
}
private Parameters.ParametersParameterComponent initializePatientParameter(Patient thePatient) {
Parameters.ParametersParameterComponent patientParameter = Resources
.newBackboneElement(Parameters.ParametersParameterComponent.class)
.setName("return");
patientParameter.setId("subject-" + Ids.simplePart(thePatient));
return patientParameter;
}
private Bundle addBundleEntries(String theServerBase, Composition theComposition, List<DetectedIssue> theDetectedIssues,
List<MeasureReport> theMeasureReports, Map<String, Resource> theEvalPlusSDEs) {
Bundle reportBundle = getBundle();
reportBundle.addEntry(getBundleEntry(theServerBase, theComposition));
theMeasureReports.forEach(report -> reportBundle.addEntry(getBundleEntry(theServerBase, report)));
theDetectedIssues.forEach(detectedIssue -> reportBundle.addEntry(getBundleEntry(theServerBase, detectedIssue)));
myConfiguredResources.values().forEach(resource -> reportBundle.addEntry(getBundleEntry(theServerBase, resource)));
theEvalPlusSDEs.values().forEach(resource -> reportBundle.addEntry(getBundleEntry(theServerBase, resource)));
return reportBundle;
}
private CareGapsStatusCode getGapStatus(Measure theMeasure, MeasureReport theMeasureReport) {
Pair<String, Boolean> inNumerator = new MutablePair<>("numerator", false);
theMeasureReport.getGroup().forEach(group -> group.getPopulation().forEach(population -> {
if (population.hasCode()
&& population.getCode().hasCoding(MEASUREREPORT_MEASURE_POPULATION_SYSTEM, inNumerator.getKey())
&& population.getCount() == 1) {
inNumerator.setValue(true);
}
}));
boolean isPositive = theMeasure.getImprovementNotation().hasCoding(MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM,
"increase");
if ((isPositive && !inNumerator.getValue()) || (!isPositive && inNumerator.getValue())) {
return CareGapsStatusCode.OPEN_GAP;
}
return CareGapsStatusCode.CLOSED_GAP;
}
private Bundle.BundleEntryComponent getBundleEntry(String theServerBase, Resource theResource) {
return new Bundle.BundleEntryComponent().setResource(theResource)
.setFullUrl(getFullUrl(theServerBase, theResource));
}
private Composition.SectionComponent getSection(Measure theMeasure, MeasureReport theMeasureReport, DetectedIssue theDetectedIssue,
CareGapsStatusCode theGapStatus) {
String narrative = String.format(HTML_DIV_PARAGRAPH_CONTENT,
theGapStatus == CareGapsStatusCode.CLOSED_GAP ? "No detected issues."
: String.format("Issues detected. See %s for details.", Ids.simple(theDetectedIssue)));
return new CompositionSectionComponentBuilder<>(Composition.SectionComponent.class)
.withTitle(theMeasure.hasTitle() ? theMeasure.getTitle() : theMeasure.getUrl())
.withFocus(Ids.simple(theMeasureReport))
.withText(new NarrativeSettings(narrative))
.withEntry(Ids.simple(theDetectedIssue))
.build();
}
private Bundle getBundle() {
return new BundleBuilder<>(Bundle.class)
.withProfile(CARE_GAPS_BUNDLE_PROFILE)
.withType(Bundle.BundleType.DOCUMENT.toString())
.build();
}
private Composition getComposition(Patient thePatient) {
return new CompositionBuilder<>(Composition.class)
.withProfile(CARE_GAPS_COMPOSITION_PROFILE)
.withType(CARE_GAPS_CODES.get("http://loinc.org/96315-7"))
.withStatus(Composition.CompositionStatus.FINAL.toString())
.withTitle("Care Gap Report for " + Ids.simplePart(thePatient))
.withSubject(Ids.simple(thePatient))
.withAuthor(Ids.simple(myConfiguredResources.get("care_gaps_composition_section_author")))
// .withCustodian(organization) // TODO: Optional: identifies the organization
// who is responsible for ongoing maintenance of and accessing to this gaps in
// care report. Add as a setting and optionally read if it's there.
.build();
}
private DetectedIssue getDetectedIssue(Patient thePatient, MeasureReport theMeasureReport, CareGapsStatusCode theCareGapStatusCode) {
return 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"))
.withPatient(Ids.simple(thePatient))
.withEvidenceDetail(Ids.simple(theMeasureReport))
.withModifierExtension(new ImmutablePair<>(
CARE_GAPS_GAP_STATUS_EXTENSION,
new CodeableConceptSettings().add(CARE_GAPS_GAP_STATUS_SYSTEM, theCareGapStatusCode.toString(),
theCareGapStatusCode.toDisplayString())))
.build();
}
protected void populateEvaluatedResources(MeasureReport theMeasureReport, Map<String, Resource> theResources) {
theMeasureReport.getEvaluatedResource().forEach(evaluatedResource -> {
IIdType resourceId = evaluatedResource.getReferenceElement();
if (resourceId.getResourceType() == null || theResources.containsKey(Ids.simple(resourceId))) {
return;
}
IBaseResource resourceBase = this.read(resourceId);
if (resourceBase instanceof Resource) {
Resource resource = (Resource) resourceBase;
theResources.put(Ids.simple(resourceId), resource);
}
});
}
protected void populateSDEResources(MeasureReport theMeasureReport, Map<String, Resource> theResources) {
if (theMeasureReport.hasExtension()) {
for (Extension extension : theMeasureReport.getExtension()) {
if (extension.hasUrl() && extension.getUrl().equals(MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION)) {
Reference sdeRef = extension.hasValue() && extension.getValue() instanceof Reference
? (Reference) extension.getValue()
: null;
if (sdeRef != null && sdeRef.hasReference() && !sdeRef.getReference().startsWith("#")) {
IdType sdeId = new IdType(sdeRef.getReference());
if (!theResources.containsKey(Ids.simple(sdeId))) {
theResources.put(Ids.simple(sdeId), read(sdeId));
}
}
}
}
}
}
private Parameters initializeResult() {
return newResource(Parameters.class, "care-gaps-report-" + UUID.randomUUID());
}
public static String getFullUrl(String theServerAddress, IBaseResource theResource) {
checkArgument(theResource.getIdElement().hasIdPart(),
"Cannot generate a fullUrl because the resource does not have an id.");
return getFullUrl(theServerAddress, theResource.fhirType(), Ids.simplePart(theResource));
}
public static String getFullUrl(String theServerAddress, String theFhirType, String theElementId) {
return String.format("%s%s/%s", theServerAddress + (theServerAddress.endsWith("/") ? "" : "/"), theFhirType,
theElementId);
}
@Override
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
}

View File

@ -0,0 +1,8 @@
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import java.util.function.Function;
public interface ISubmitDataService extends Function<RequestDetails, SubmitDataService> {
}

View File

@ -36,7 +36,6 @@ import org.springframework.stereotype.Component;
import java.util.function.Function;
@Component
public class MeasureOperationsProvider {
@Autowired
Function<RequestDetails, MeasureService> myR4MeasureServiceFactory;

View File

@ -19,27 +19,34 @@
*/
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.cr.common.IDaoRegistryUser;
import ca.uhn.fhir.cr.common.IDataProviderFactory;
import ca.uhn.fhir.cr.common.IFhirDalFactory;
import ca.uhn.fhir.cr.common.ILibrarySourceProviderFactory;
import ca.uhn.fhir.cr.common.ITerminologyProviderFactory;
import ca.uhn.fhir.cr.common.SupplementalDataConstants;
import ca.uhn.fhir.cr.r4.ISupplementalDataSearchParamUser;
import ca.uhn.fhir.cr.constant.MeasureReportConstants;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.util.BundleBuilder;
import org.apache.commons.lang3.StringUtils;
import org.cqframework.cql.cql2elm.LibrarySourceProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
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.ContactDetail;
import org.hl7.fhir.r4.model.ContactPoint;
import org.hl7.fhir.r4.model.Endpoint;
import org.hl7.fhir.r4.model.Enumerations;
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.SearchParameter;
import org.hl7.fhir.r4.model.StringType;
import org.opencds.cqf.cql.engine.data.DataProvider;
import org.opencds.cqf.cql.engine.fhir.terminology.R4FhirTerminologyProvider;
@ -48,13 +55,64 @@ import org.opencds.cqf.cql.evaluator.CqlOptions;
import org.opencds.cqf.cql.evaluator.fhir.dal.FhirDal;
import org.opencds.cqf.cql.evaluator.fhir.util.Clients;
import org.opencds.cqf.cql.evaluator.measure.MeasureEvaluationOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class MeasureService implements ISupplementalDataSearchParamUser {
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.COUNTRY_CODING_SYSTEM_CODE;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_DEFINITION_DATE;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.US_COUNTRY_CODE;
import static ca.uhn.fhir.cr.constant.MeasureReportConstants.US_COUNTRY_DISPLAY;
public class MeasureService implements IDaoRegistryUser {
private Logger ourLogger = LoggerFactory.getLogger(MeasureService.class);
public static final List<ContactDetail> CQI_CONTACTDETAIL = Collections.singletonList(
new ContactDetail()
.addTelecom(
new ContactPoint()
.setSystem(ContactPoint.ContactPointSystem.URL)
.setValue("http://www.hl7.org/Special/committees/cqi/index.cfm")));
public static final List<CodeableConcept> US_JURISDICTION_CODING = Collections.singletonList(
new CodeableConcept()
.addCoding(
new Coding(COUNTRY_CODING_SYSTEM_CODE, US_COUNTRY_CODE, US_COUNTRY_DISPLAY)));
public static final SearchParameter SUPPLEMENTAL_DATA_SEARCHPARAMETER = (SearchParameter) new SearchParameter()
.setUrl(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_URL)
.setVersion(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_VERSION)
.setName("DEQMMeasureReportSupplementalData")
.setStatus(Enumerations.PublicationStatus.ACTIVE)
.setDate(MEASUREREPORT_SUPPLEMENTALDATA_SEARCHPARAMETER_DEFINITION_DATE)
.setPublisher("HL7 International - Clinical Quality Information Work Group")
.setContact(CQI_CONTACTDETAIL)
.setDescription(
String.format(
"Returns resources (supplemental data) from references on extensions on the MeasureReport with urls matching %s.",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setJurisdiction(US_JURISDICTION_CODING)
.addBase("MeasureReport")
.setCode("supplemental-data")
.setType(Enumerations.SearchParamType.REFERENCE)
.setExpression(
String.format("MeasureReport.extension('%s').value",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpath(
String.format("f:MeasureReport/f:extension[@url='%s'].value",
MEASUREREPORT_MEASURE_SUPPLEMENTALDATA_EXTENSION))
.setXpathUsage(SearchParameter.XPathUsageType.NORMAL)
.setTitle("Supplemental Data")
.setId("deqm-measurereport-supplemental-data");
@Autowired
protected ITerminologyProviderFactory myTerminologyProviderFactory;
@ -130,7 +188,7 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
Bundle theAdditionalData,
Endpoint theTerminologyEndpoint) {
ensureSupplementalDataElementSearchParameter(myRequestDetails);
ensureSupplementalDataElementSearchParameter();
Measure measure = read(theId, myRequestDetails);
@ -171,10 +229,10 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
return measureReport;
}
private List<String> getPractitionerPatients(String practitioner, RequestDetails theRequestDetails) {
private List<String> getPractitionerPatients(String thePractitioner, RequestDetails theRequestDetails) {
SearchParameterMap map = SearchParameterMap.newSynchronous();
map.add("general-practitioner", new ReferenceParam(
practitioner.startsWith("Practitioner/") ? practitioner : "Practitioner/" + practitioner));
thePractitioner.startsWith("Practitioner/") ? thePractitioner : "Practitioner/" + thePractitioner));
List<String> patients = new ArrayList<>();
IBundleProvider patientProvider = myDaoRegistry.getResourceDao("Patient").search(map, theRequestDetails);
List<IBaseResource> patientList = patientProvider.getAllResources();
@ -182,12 +240,12 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
return patients;
}
private void addProductLineExtension(MeasureReport measureReport, String productLine) {
if (productLine != null) {
private void addProductLineExtension(MeasureReport theMeasureReport, String theProductLine) {
if (theProductLine != null) {
Extension ext = new Extension();
ext.setUrl(SupplementalDataConstants.MEASUREREPORT_PRODUCT_LINE_EXT_URL);
ext.setValue(new StringType(productLine));
measureReport.addExtension(ext);
ext.setUrl(MeasureReportConstants.MEASUREREPORT_PRODUCT_LINE_EXT_URL);
ext.setValue(new StringType(theProductLine));
theMeasureReport.addExtension(ext);
}
}
@ -196,4 +254,12 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
return this.myDaoRegistry;
}
protected void ensureSupplementalDataElementSearchParameter() {
//create a transaction bundle
BundleBuilder builder = new BundleBuilder(getFhirContext());
//set the request to be condition on code == supplemental data
builder.addTransactionCreateEntry(SUPPLEMENTAL_DATA_SEARCHPARAMETER).conditional("code=supplemental-data");
transaction(builder.getBundle(), this.myRequestDetails);
}
}

View File

@ -0,0 +1,61 @@
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.model.api.annotation.Description;
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.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.function.Function;
public class SubmitDataProvider {
private static final Logger ourLog = LoggerFactory.getLogger(SubmitDataProvider.class);
Function<RequestDetails, SubmitDataService> mySubmitDataServiceFunction;
public SubmitDataProvider(Function<RequestDetails, SubmitDataService> submitDataServiceFunction) {
this.mySubmitDataServiceFunction = submitDataServiceFunction;
}
/**
* Implements the <a href=
* "http://hl7.org/fhir/R4/measure-operation-submit-data.html">$submit-data</a>
* operation found in the
* <a href="http://hl7.org/fhir/R4/clinicalreasoning-module.html">FHIR Clinical
* Reasoning Module</a> per the
* <a href="http://build.fhir.org/ig/HL7/davinci-deqm/datax.html#submit-data">Da
* Vinci DEQM FHIR Implementation Guide</a>.
*
*
* The submitted MeasureReport and Resources will be saved to the local server.
* A Bundle reporting the result of the transaction will be returned.
*
* Usage:
* URL: [base]/Measure/$submit-data
* URL: [base]/Measure/[id]/$submit-data
*
* @param theRequestDetails generally auto-populated by the HAPI server
* framework.
* @param theId the Id of the Measure to submit data for
* @param theReport the MeasureReport to be submitted
* @param theResources the resources to be submitted
* @return Bundle the transaction result
*/
@Description(shortDefinition = "$submit-data", value = "Implements the <a href=\"http://hl7.org/fhir/R4/measure-operation-submit-data.html\">$submit-data</a> operation found in the <a href=\"http://hl7.org/fhir/R4/clinicalreasoning-module.html\">FHIR Clinical Reasoning Module</a> per the <a href=\"http://build.fhir.org/ig/HL7/davinci-deqm/datax.html#submit-data\">Da Vinci DEQM FHIR Implementation Guide</a>.")
@Operation(name = "$submit-data", type = Measure.class)
public Bundle submitData(RequestDetails theRequestDetails,
@IdParam IdType theId,
@OperationParam(name = "measureReport", min = 1, max = 1) MeasureReport theReport,
@OperationParam(name = "resource") List<IBaseResource> theResources) {
return mySubmitDataServiceFunction.apply(theRequestDetails)
.submitData(theId, theReport, theResources);
}
}

View File

@ -0,0 +1,83 @@
package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class SubmitDataService{
private static final Logger ourLogger = LoggerFactory.getLogger(SubmitDataService.class);
private final DaoRegistry myDaoRegistry;
private final RequestDetails myRequestDetails;
public SubmitDataService(DaoRegistry theDaoRegistry, RequestDetails theRequestDetails){
this.myDaoRegistry = theDaoRegistry;
this.myRequestDetails = theRequestDetails;
}
/**
* Save measure report and resources to the local repository
* @param theId
* @param theReport
* @param theResources
* @return Bundle transaction result
*/
public Bundle submitData(IdType theId, MeasureReport theReport, List<IBaseResource> theResources) {
/*
* TODO - resource validation using $data-requirements operation (params are the
* provided id and the measurement period from the MeasureReport)
*
* TODO - profile validation ... not sure how that would work ... (get
* StructureDefinition from URL or must it be stored in Ruler?)
*/
Bundle transactionBundle = new Bundle()
.setType(Bundle.BundleType.TRANSACTION)
.addEntry(createEntry(theReport));
if (theResources != null) {
for (IBaseResource res : theResources) {
// Unpack nested Bundles
if (res instanceof Bundle) {
Bundle nestedBundle = (Bundle) res;
for (Bundle.BundleEntryComponent entry : nestedBundle.getEntry()) {
transactionBundle.addEntry(createEntry(entry.getResource()));
}
} else {
transactionBundle.addEntry(createEntry(res));
}
}
}
return (Bundle) myDaoRegistry.getSystemDao().transaction(myRequestDetails, transactionBundle);
}
private Bundle.BundleEntryComponent createEntry(IBaseResource theResource) {
return new Bundle.BundleEntryComponent()
.setResource((Resource) theResource)
.setRequest(createRequest(theResource));
}
private Bundle.BundleEntryRequestComponent createRequest(IBaseResource theResource) {
Bundle.BundleEntryRequestComponent request = new Bundle.BundleEntryRequestComponent();
if (theResource.getIdElement().hasValue()) {
request
.setMethod(Bundle.HTTPVerb.PUT)
.setUrl(theResource.getIdElement().getValue());
} else {
request
.setMethod(Bundle.HTTPVerb.POST)
.setUrl(theResource.fhirType());
}
return request;
}
}

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.cr.config.CrDstu3Config;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.test.BaseJpaDstu3Test;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import io.specto.hoverfly.junit.dsl.HoverflyDsl;
import io.specto.hoverfly.junit.dsl.StubServiceBuilder;
import io.specto.hoverfly.junit.rule.HoverflyRule;
@ -14,6 +15,7 @@ import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.junit.ClassRule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
@ -55,14 +57,6 @@ public abstract class BaseCrDstu3Test extends BaseJpaDstu3Test implements IResou
return ourFhirContext;
}
public Bundle loadBundle(String theLocation) {
return loadBundle(Bundle.class, theLocation);
}
public IParser getFhirParser() {
return ourParser;
}
public StubServiceBuilder mockNotFound(String theResource) {
OperationOutcome outcome = new OperationOutcome();
outcome.getText().setStatusAsString("generated");
@ -106,10 +100,6 @@ public abstract class BaseCrDstu3Test extends BaseJpaDstu3Test implements IResou
.willReturn(success());
}
public Bundle makeBundle(List<? extends Resource> theResources) {
return makeBundle(theResources.toArray(new Resource[theResources.size()]));
}
public Bundle makeBundle(Resource... theResources) {
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.SEARCHSET);
@ -121,4 +111,8 @@ public abstract class BaseCrDstu3Test extends BaseJpaDstu3Test implements IResou
}
return bundle;
}
public Bundle loadBundle(String theLocation) {
return loadBundle(Bundle.class, theLocation);
}
}

View File

@ -32,11 +32,11 @@ import static io.specto.hoverfly.junit.dsl.ResponseCreators.success;
public abstract class BaseCrR4Test extends BaseResourceProviderR4Test implements IResourceLoader {
protected static final FhirContext ourFhirContext = FhirContext.forR4Cached();
private static final IParser ourParser = ourFhirContext.newJsonParser().setPrettyPrint(true);
private static final String TEST_ADDRESS = "test-address.com";
protected static final String TEST_ADDRESS = "http://test:9001/fhir";
@ClassRule
public static HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode(dsl(
service(TEST_ADDRESS)
.get("/fhir/metadata")
.get("/metadata")
.willReturn(success(getCapabilityStatement().toString(), "application/json"))
));

View File

@ -1,11 +1,26 @@
package ca.uhn.fhir.cr;
import ca.uhn.fhir.cr.common.IDaoRegistryUser;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.util.ClasspathUtil;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.cql.evaluator.fhir.util.Ids;
import org.springframework.core.io.DefaultResourceLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* This is a utility interface that allows a class that has a DaoRegistry to load Bundles and read Resources.
@ -51,4 +66,103 @@ public interface IResourceLoader extends IDaoRegistryUser {
return resource;
}
default public IBaseResource readResource(String theLocation) {
String resourceString = stringFromResource(theLocation);
return EncodingEnum.detectEncoding(resourceString).newParser(getFhirContext()).parseResource(resourceString);
}
default public IBaseResource readAndLoadResource(String theLocation) {
String resourceString = stringFromResource(theLocation);
if (theLocation.endsWith("json")) {
return loadResource(parseResource("json", resourceString));
} else {
return loadResource(parseResource("xml", resourceString));
}
}
default public IBaseResource loadResource(IBaseResource theResource) {
if (getDaoRegistry() == null) {
return theResource;
}
update(theResource);
return theResource;
}
default public IBaseResource parseResource(String theEncoding, String theResourceString) {
IParser parser;
switch (theEncoding.toLowerCase()) {
case "json":
parser = getFhirContext().newJsonParser();
break;
case "xml":
parser = getFhirContext().newXmlParser();
break;
default:
throw new IllegalArgumentException(
String.format("Expected encoding xml, or json. %s is not a valid encoding", theEncoding));
}
return parser.parseResource(theResourceString);
}
default public String stringFromResource(String theLocation) {
InputStream is = null;
try {
if (theLocation.startsWith(File.separator)) {
is = new FileInputStream(theLocation);
} else {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
org.springframework.core.io.Resource resource = resourceLoader.getResource(theLocation);
is = resource.getInputStream();
}
return IOUtils.toString(is, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException(String.format("Error loading resource from %s", theLocation), e);
}
}
default Object loadTransaction(String theLocation) {
IBaseBundle resource = (IBaseBundle) readResource(theLocation);
return transaction(resource, new SystemRequestDetails());
}
private Bundle.BundleEntryComponent createEntry(IBaseResource theResource) {
return new Bundle.BundleEntryComponent()
.setResource((Resource) theResource)
.setRequest(createRequest(theResource));
}
private Bundle.BundleEntryRequestComponent createRequest(IBaseResource theResource) {
Bundle.BundleEntryRequestComponent request = new Bundle.BundleEntryRequestComponent();
if (theResource.getIdElement().hasValue()) {
request
.setMethod(Bundle.HTTPVerb.PUT)
.setUrl(theResource.getIdElement().getValue());
} else {
request
.setMethod(Bundle.HTTPVerb.POST)
.setUrl(theResource.fhirType());
}
return request;
}
default <T extends IBaseResource> T newResource(Class<T> theResourceClass, String theIdPart) {
checkNotNull(theResourceClass);
checkNotNull(theIdPart);
T newResource = newResource(theResourceClass);
newResource.setId((IIdType) Ids.newId(getFhirContext(), newResource.fhirType(), theIdPart));
return newResource;
}
@SuppressWarnings("unchecked")
default <T extends IBaseResource> T newResource(Class<T> theResourceClass) {
checkNotNull(theResourceClass);
return (T) this.getFhirContext().getResourceDefinition(theResourceClass).newInstance();
}
}

View File

@ -21,12 +21,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class CrProviderDstu3Test extends BaseCrDstu3Test {
public class CrProviderDstu3Test extends BaseCrDstu3Test {
private static final Logger ourLog = LoggerFactory.getLogger(CrProviderDstu3Test.class);
protected final RequestDetails myRequestDetails = RequestDetailsHelper.newServletRequestDetails();
@ -77,12 +76,10 @@ public class CrProviderDstu3Test extends BaseCrDstu3Test {
loadResource(Library.class, "ca/uhn/fhir/cr/dstu3/hedis-ig/library/library-asf-logic.json", myRequestDetails);
// Load the measure for ASF: Unhealthy Alcohol Use Screening and Follow-up (ASF)
loadResource(Measure.class,"ca/uhn/fhir/cr/dstu3/hedis-ig/measure-asf.json", myRequestDetails);
var result = loadBundle("ca/uhn/fhir/cr/dstu3/hedis-ig/test-patient-6529-data.json");
Bundle result = loadBundle("ca/uhn/fhir/cr/dstu3/hedis-ig/test-patient-6529-data.json");
assertNotNull(result);
List<Bundle.BundleEntryComponent> entries = result.getEntry();
assertThat(entries, hasSize(22));
// assertEquals(entries.get(0).getResponse().getStatus(), "201 Created");
// assertEquals(entries.get(21).getResponse().getStatus(), "201 Created");
assertEquals(entries.size(), 22);
IdType measureId = new IdType("Measure", "measure-asf");
String patient = "Patient/Patient-6529";
@ -104,8 +101,8 @@ public class CrProviderDstu3Test extends BaseCrDstu3Test {
null,
myRequestDetails);
// Assert it worked
assertThat(report.getGroup(), hasSize(2));
assertThat(report.getGroup().get(0).getPopulation(), hasSize(3));
assertEquals(report.getGroup().size(), 2);
assertEquals(report.getGroup().get(0).getPopulation().size(), 3);
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
// Now timed runs
@ -147,8 +144,8 @@ public class CrProviderDstu3Test extends BaseCrDstu3Test {
MeasureReport report = myMeasureOperationsProvider.evaluateMeasure(measureId, periodStart, periodEnd, "population",
null, null, null, null, null, null, myRequestDetails);
// Assert it worked
assertThat(report.getGroup(), hasSize(2));
assertThat(report.getGroup().get(0).getPopulation(), hasSize(3));
assertEquals(report.getGroup().size(), 2);
assertEquals(report.getGroup().get(0).getPopulation().size(), 3);
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
// Now timed runs

View File

@ -70,17 +70,17 @@ class MeasureOperationsProviderTest extends BaseCrDstu3Test {
}
@Test
void testMeasureEvaluateWithTerminology(Hoverfly hoverfly) throws IOException {
void testMeasureEvaluateWithTerminology() throws IOException {
loadBundle("Exm105Fhir3Measure.json");
var returnMeasureReport = this.myMeasureOperationsProvider.evaluateMeasure(
new IdType("Measure", "measure-EXM105-FHIR3-8.0.000"),
"2019-01-01",
"2020-01-01",
"individual",
"patient",
"Patient/denom-EXM105-FHIR3",
null,
"2019-12-12",
null,
null,
null,
null,

View File

@ -0,0 +1,213 @@
package ca.uhn.fhir.cr.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cr.IResourceLoader;
import ca.uhn.fhir.cr.config.CrProperties;
import ca.uhn.fhir.cr.config.CrR4Config;
import ca.uhn.fhir.cr.r4.measure.CareGapsOperationProvider;
import ca.uhn.fhir.cr.r4.measure.SubmitDataProvider;
import ca.uhn.fhir.cr.r4.measure.SubmitDataService;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.test.utilities.JettyUtil;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
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.Measure;
import org.hl7.fhir.r4.model.Parameters;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* End to end test for care gaps functionality
* Scenario is that we have a Provider that is transmitting data to a Payer to validate that
* no gaps in care exist (a "gap in care" means that a Patient is not conformant with best practices for a given pathology).
* Specifically, for this test, we're checking to ensure that a Patient has had the appropriate colorectal cancer screenings.
*
* So, it's expected that the Payer already has the relevant quality measure content loaded. The first two steps here are initializing the Payer
* by loading Measure content, and by setting up a reporting Organization resource (IOW, the Payer's identify to associate with the care-gaps report).
*
* The next step is for the Provider to submit data to the Payer for review. That's the submit data operation.
*
* After that, the Provider can invoke $care-gaps to check for any issues, which are reported.
*
* The Provider can then resolve those issues, submit additional data, and then check to see if the gaps are closed.
*
* 1. Initialize Payer with Measure content
* 2. Initialize Payer with Organization info
* 3. Provider submits Patient data
* 4. Provider invokes care-gaps (and discovers issues)
* 5. (not included in test, since it's done out of bad) Provider closes gap (by having the Procedure done on the Patient).
* 6. Provider submits additional Patient data
* 7. Provider invokes care-gaps (and discovers issues are closed).
*/
@ContextConfiguration(classes = CrR4Config.class)
class CareGapsOperationProviderIT extends BaseJpaR4Test implements IResourceLoader {
private static RestfulServer ourRestServer;
private static IGenericClient ourClient;
private static FhirContext ourCtx;
private static CloseableHttpClient ourHttpClient;
private static Server ourServer;
private static String ourServerBase;
@Autowired
CareGapsOperationProvider myCareGapsOperationProvider;
@Autowired
CrProperties myCrProperties;
SubmitDataProvider mySubmitDataProvider;
private SimpleRequestHeaderInterceptor mySimpleHeaderInterceptor;
@SuppressWarnings("deprecation")
@AfterEach
public void after() {
ourClient.unregisterInterceptor(mySimpleHeaderInterceptor);
myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields());
}
@BeforeEach
public void beforeStartServer() throws Exception {
if (ourRestServer == null) {
RestfulServer restServer = new RestfulServer(ourCtx);
mySubmitDataProvider = new SubmitDataProvider(requestDetails -> {
return new SubmitDataService(getDaoRegistry(), requestDetails);
});
restServer.setPlainProviders(mySystemProvider, myCareGapsOperationProvider, mySubmitDataProvider);
ourServer = new Server(0);
ServletContextHandler proxyHandler = new ServletContextHandler();
proxyHandler.setContextPath("/");
ServletHolder servletHolder = new ServletHolder();
servletHolder.setServlet(restServer);
proxyHandler.addServlet(servletHolder, "/fhir/*");
ourCtx = FhirContext.forR4Cached();
restServer.setFhirContext(ourCtx);
ourServer.setHandler(proxyHandler);
JettyUtil.startServer(ourServer);
int myPort = JettyUtil.getPortForStartedServer(ourServer);
ourServerBase = "http://localhost:" + myPort + "/fhir";
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourHttpClient = builder.build();
ourCtx.getRestfulClientFactory().setSocketTimeout(600 * 1000);
ourClient = ourCtx.newRestfulGenericClient(ourServerBase);
ourClient.setLogRequestAndResponse(true);
ourRestServer = restServer;
}
ourRestServer.setDefaultResponseEncoding(EncodingEnum.XML);
ourRestServer.setPagingProvider(myPagingProvider);
mySimpleHeaderInterceptor = new SimpleRequestHeaderInterceptor();
ourClient.registerInterceptor(mySimpleHeaderInterceptor);
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
// Set properties
CrProperties.MeasureProperties measureProperties = new CrProperties.MeasureProperties();
CrProperties.MeasureProperties.MeasureReportConfiguration measureReportConfiguration = new CrProperties.MeasureProperties.MeasureReportConfiguration();
measureReportConfiguration.setCareGapsReporter("Organization/alphora");
measureReportConfiguration.setCareGapsCompositionSectionAuthor("Organization/alphora-author");
measureProperties.setMeasureReportConfiguration(measureReportConfiguration);
myCrProperties.setMeasureProperties(measureProperties);
}
@Test
public void careGapsEndToEnd(){
// 1. Initialize Payer content
var measureBundle = (Bundle) readResource("CaregapsColorectalCancerScreeningsFHIR-bundle.json");
ourClient.transaction().withBundle(measureBundle).execute();
// 2. Initialize Payer org data
var orgData = (Bundle) readResource("CaregapsAuthorAndReporter.json");
ourClient.transaction().withBundle(orgData).execute();
// 3. Provider submits Patient data
var patientData = (Parameters) readResource("CaregapsPatientData.json");
ourClient.operation().onInstance("Measure/ColorectalCancerScreeningsFHIR").named("submit-data")
.withParameters(patientData).execute();
// 4. Provider runs $care-gaps
var parameters = new Parameters();
parameters.addParameter("status", "open-gap");
parameters.addParameter("status", "closed-gap");
parameters.addParameter("periodStart", new DateType("2020-01-01"));
parameters.addParameter("periodEnd", new DateType("2020-12-31"));
parameters.addParameter("subject", "Patient/end-to-end-EXM130");
parameters.addParameter("measureId", "ColorectalCancerScreeningsFHIR");
var result = ourClient.operation().onType(Measure.class)
.named("$care-gaps")
.withParameters(parameters)
.returnResourceType(Parameters.class)
.execute();
// assert open-gap
assertForGaps(result);
// 5. (out of band) Provider fixes gaps
var newData = (Parameters) readResource("CaregapsSubmitDataCloseGap.json");
// 6. Provider submits additional Patient data showing that they did another procedure that was needed.
ourClient.operation().onInstance("Measure/ColorectalCancerScreeningsFHIR").named("submit-data").withParameters(newData).execute();
// 7. Provider runs care-gaps again
result = ourClient.operation().onType("Measure")
.named("care-gaps")
.withParameters(parameters)
.execute();
// assert closed-gap
assertForGaps(result);
}
private void assertForGaps(Parameters theResult) {
assertNotNull(theResult);
var dataBundle = (Bundle) theResult.getParameter().get(0).getResource();
var detectedIssue = dataBundle.getEntry()
.stream()
.filter(bundleEntryComponent -> "DetectedIssue".equalsIgnoreCase(bundleEntryComponent.getResource().getResourceType().name())).findFirst().get();
var extension = (Extension) detectedIssue.getResource().getChildByName("modifierExtension").getValues().get(0);
var codeableConcept = (CodeableConcept) extension.getValue();
Optional<Coding> coding = codeableConcept.getCoding()
.stream()
.filter(code -> "open-gap".equalsIgnoreCase(code.getCode()) || "closed-gap".equalsIgnoreCase(code.getCode())).findFirst();
assertTrue(!coding.isEmpty());
}
@Override
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
}

View File

@ -0,0 +1,502 @@
package ca.uhn.fhir.cr.r4;
import ca.uhn.fhir.cr.BaseCrR4Test;
import ca.uhn.fhir.cr.config.CrProperties;
import ca.uhn.fhir.cr.r4.measure.CareGapsService;
import ca.uhn.fhir.cr.r4.measure.MeasureService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Parameters;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Function;
import static javolution.testing.TestContext.assertEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(SpringExtension.class)
public class CareGapsServiceR4Test extends BaseCrR4Test {
private static final String ourPeriodStartValid = "2019-01-01";
private static IPrimitiveType<Date> ourPeriodStart = new DateDt("2019-01-01");
private static final String ourPeriodEndValid = "2019-12-31";
private static IPrimitiveType<Date> ourPeriodEnd = new DateDt("2019-12-31");
private static final String ourSubjectPatientValid = "Patient/numer-EXM125";
private static final String ourSubjectGroupValid = "Group/gic-gr-1";
private static final String ourSubjectGroupParallelValid = "Group/gic-gr-parallel";
private static final String ourStatusValid = "open-gap";
private List<String> myStatuses;
private List<CanonicalType> myMeasureUrls;
private static final String ourStatusValidSecond = "closed-gap";
private List<String> myMeasures;
private static final String ourMeasureIdValid = "BreastCancerScreeningFHIR";
private static final String ourMeasureUrlValid = "http://ecqi.healthit.gov/ecqms/Measure/BreastCancerScreeningFHIR";
private static final String ourPractitionerValid = "gic-pra-1";
private static final String ourOrganizationValid = "gic-org-1";
private static final String ourDateInvalid = "bad-date";
private static final String ourSubjectInvalid = "bad-subject";
private static final String ourStatusInvalid = "bad-status";
private static final String ourSubjectReferenceInvalid = "Measure/gic-sub-1";
Function<RequestDetails, CareGapsService> myCareGapsService;
CrProperties myCrProperties;
@Autowired
MeasureService myMeasureService;
Executor myExecutor;
@BeforeEach
public void beforeEach() {
loadBundle(Bundle.class, "CaregapsAuthorAndReporter.json");
readAndLoadResource("numer-EXM125-patient.json");
myStatuses = new ArrayList<>();
myMeasures = new ArrayList<>();
myMeasureUrls = new ArrayList<>();
myCrProperties = new CrProperties();
CrProperties.MeasureProperties measureProperties = new CrProperties.MeasureProperties();
CrProperties.MeasureProperties.MeasureReportConfiguration measureReportConfiguration = new CrProperties.MeasureProperties.MeasureReportConfiguration();
measureReportConfiguration.setCareGapsReporter("Organization/alphora");
measureReportConfiguration.setCareGapsCompositionSectionAuthor("Organization/alphora-author");
measureProperties.setMeasureReportConfiguration(measureReportConfiguration);
myCrProperties.setMeasureProperties(measureProperties);
myExecutor = Executors.newSingleThreadExecutor();
//measureService = new MeasureService();
myCareGapsService = requestDetails -> {
CareGapsService careGapsService = new CareGapsService(myCrProperties, myMeasureService, getDaoRegistry(), myExecutor, requestDetails);
return careGapsService;
};
}
private void beforeEachMeasure() {
loadBundle("BreastCancerScreeningFHIR-bundle.json");
}
private void beforeEachMultipleMeasures() {
loadBundle("BreastCancerScreeningFHIR-bundle.json");
loadBundle("ColorectalCancerScreeningsFHIR-bundle.json");
}
@Test
void testMinimalParametersValid() {
beforeEachMeasure();
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
Parameters result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectPatientValid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
);
assertNotNull(result);
}
@Test
void testPeriodStartNull() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
assertThrows(Exception.class, () -> myCareGapsService.apply(requestDetails).getCareGapsReport(null, ourPeriodEnd
, null
, ourSubjectPatientValid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
));
}
@Test
void testPeriodStartInvalid() {
beforeEachMeasure();
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
assertThrows(Exception.class, () -> myCareGapsService.apply(requestDetails).getCareGapsReport(new DateDt("12-21-2025"), ourPeriodEnd
, null
, ourSubjectPatientValid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
));
}
@Test
void testPeriodEndNull() {
beforeEachMeasure();
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
assertThrows(Exception.class, () -> myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, null
, null
, ourSubjectPatientValid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
));
}
@Test
void testPeriodEndInvalid() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
assertThrows(Exception.class, () -> myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, new DateDt("12-21-2025")
, null
, ourSubjectPatientValid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
));
}
@Test
void testSubjectGroupValid() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
readAndLoadResource("gic-gr-1.json");
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
assertDoesNotThrow(() -> {
myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectGroupValid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
);
});
}
@Test
void testSubjectInvalid() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
Parameters result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectInvalid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
);
assertTrue(result.getParameter().isEmpty());
}
@Test
void testSubjectReferenceInvalid() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
Parameters result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectReferenceInvalid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
);
assertTrue(result.getParameter().isEmpty());
}
@Test
void testSubjectAndPractitioner() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
Parameters result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectPatientValid
, ourPractitionerValid
, null
, myStatuses
, myMeasures
, null
, null
, null
);
assertTrue(result.getParameter().isEmpty());
}
@Test
void testSubjectAndOrganization() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
Parameters result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectPatientValid
, null
, ourOrganizationValid
, myStatuses
, myMeasures
, null
, null
, null
);
assertTrue(result.getParameter().isEmpty());
}
@Test
void testOrganizationOnly() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
assertThrows(NotImplementedOperationException.class, () -> myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, null
, null
, ourOrganizationValid
, myStatuses
, myMeasures
, null
, null
, null
));
}
@Test
void testPractitionerAndOrganization() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
assertThrows(NotImplementedOperationException.class, () -> myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, null
, ourPractitionerValid
, ourOrganizationValid
, myStatuses
, myMeasures
, null
, null
, null
));
}
@Test
void testPractitionerOnly() {
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
assertThrows(NotImplementedOperationException.class, () -> myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, null
, ourPractitionerValid
, null
, myStatuses
, myMeasures
, null
, null
, null
));
}
@Test
void testNoMeasure() {
myStatuses.add(ourStatusValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
var result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectPatientValid
, null
, null
, myStatuses
, null
, null
, null
, null
);
assertTrue(result.getParameter().isEmpty());
}
@Test
void testStatusInvalid() {
myStatuses.add(ourStatusInvalid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
Parameters result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectPatientValid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
);
assertTrue(result.getParameter().isEmpty());
}
@Test
void testStatusNull() {
myStatuses.add(ourStatusInvalid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
var result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectPatientValid
, null
, null
, null
, myMeasures
, null
, null
, null
);
assertTrue(result.getParameter().isEmpty());
}
@Test
public void testMeasures() {
beforeEachMultipleMeasures();
myStatuses.add(ourStatusValid);
ourPeriodStart = new DateDt("2019-01-01");
myMeasures.add("ColorectalCancerScreeningsFHIR");
myMeasureUrls.add(new CanonicalType(ourMeasureUrlValid));
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
Parameters result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectPatientValid
, null
, null
, myStatuses
, myMeasures
, null
, myMeasureUrls
, null
);
assertNotNull(result);
//Test to search for how many search parameters are created.
//only 1 should be created.
var searchParams = this.myDaoRegistry.getResourceDao("SearchParameter")
.search(new SearchParameterMap(), requestDetails);
assertNotNull(searchParams);
assertEquals(searchParams.getAllResources().size(), 1);
}
@Test
void testParallelMultiSubject() {
beforeEachParallelMeasure();
myStatuses.add(ourStatusValid);
myMeasures.add(ourMeasureIdValid);
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase("test.com");
Parameters result = myCareGapsService.apply(requestDetails).getCareGapsReport(ourPeriodStart, ourPeriodEnd
, null
, ourSubjectGroupParallelValid
, null
, null
, myStatuses
, myMeasures
, null
, null
, null
);
assertNotNull(result);
}
private void beforeEachParallelMeasure() {
readAndLoadResource("gic-gr-parallel.json");
loadBundle("BreastCancerScreeningFHIR-bundle.json");
}
}

View File

@ -0,0 +1,68 @@
package ca.uhn.fhir.cr.r4;
import ca.uhn.fhir.cr.BaseCrR4Test;
import ca.uhn.fhir.cr.common.Searches;
import ca.uhn.fhir.cr.r4.measure.SubmitDataService;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Iterator;
import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(SpringExtension.class)
public class SubmitDataServiceR4Test extends BaseCrR4Test {
Function<RequestDetails, SubmitDataService> mySubmitDataServiceFunction;
@BeforeEach
public void beforeEach() {
mySubmitDataServiceFunction = rs -> {
return new SubmitDataService(getDaoRegistry(), new SystemRequestDetails());
};
}
@Test
public void submitDataTest(){
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
MeasureReport mr = newResource(MeasureReport.class).setMeasure("Measure/A123");
Observation obs = newResource(Observation.class).setValue(new StringType("ABC"));
mySubmitDataServiceFunction.apply(requestDetails)
.submitData(new IdType("Measure", "A123"), mr,
Lists.newArrayList(obs));
Iterable<IBaseResource> resourcesResult = search(Observation.class, Searches.all());
Observation savedObs = null;
Iterator<IBaseResource> iterator = resourcesResult.iterator();
while(iterator.hasNext()){
savedObs = (Observation) iterator.next();
break;
}
assertNotNull(savedObs);
assertEquals("ABC", savedObs.getValue().primitiveValue());
resourcesResult = search(MeasureReport.class, Searches.all());
MeasureReport savedMr = null;
iterator = resourcesResult.iterator();
while(iterator.hasNext()){
savedMr = (MeasureReport) iterator.next();
break;
}
assertNotNull(savedMr);
assertEquals("Measure/A123", savedMr.getMeasure());
}
}

View File

@ -0,0 +1,58 @@
{
"resourceType": "Organization",
"id": "alphora",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/organization-deqm"
]
},
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "TAX",
"display": "Tax ID number"
}
]
},
"system": "urn:oid:2.16.840.1.113883.4.4",
"value": "123456789",
"assigner": {
"display": "www.irs.gov"
}
}
],
"active": true,
"type": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/organization-type",
"code": "prov",
"display": "Healthcare Provider"
}
]
}
],
"name": "alphora",
"telecom": [
{
"system": "phone",
"value": "(+1) 401-555-1212"
}
],
"address": [
{
"line": [
"73 Lakewood Street"
],
"city": "Warwick",
"state": "RI",
"postalCode": "02886",
"country": "USA"
}
]
}

View File

@ -0,0 +1,58 @@
{
"resourceType": "Organization",
"id": "alphora-author",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/organization-deqm"
]
},
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "TAX",
"display": "Tax ID number"
}
]
},
"system": "urn:oid:2.16.840.1.113883.4.4",
"value": "12345678910",
"assigner": {
"display": "www.irs.gov"
}
}
],
"active": true,
"type": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/organization-type",
"code": "prov",
"display": "Healthcare Provider"
}
]
}
],
"name": "alphora-author",
"telecom": [
{
"system": "phone",
"value": "(+1) 401-555-1313"
}
],
"address": [
{
"line": [
"737 Lakewood Street"
],
"city": "Warwick",
"state": "RI",
"postalCode": "02886",
"country": "USA"
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,135 @@
{
"resourceType": "Bundle",
"id": "gic-configuration",
"type": "transaction",
"entry": [
{
"resource": {
"resourceType": "Organization",
"id": "alphora",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/organization-deqm"
]
},
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "TAX",
"display": "Tax ID number"
}
]
},
"system": "urn:oid:2.16.840.1.113883.4.4",
"value": "123456789",
"assigner": {
"display": "www.irs.gov"
}
}
],
"active": true,
"type": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/organization-type",
"code": "prov",
"display": "Healthcare Provider"
}
]
}
],
"name": "alphora",
"telecom": [
{
"system": "phone",
"value": "(+1) 401-555-1212"
}
],
"address": [
{
"line": [
"73 Lakewood Street"
],
"city": "Warwick",
"state": "RI",
"postalCode": "02886",
"country": "USA"
}
]
},
"request": {
"method": "PUT",
"url": "Organization/alphora"
}
},
{
"resource": {
"resourceType": "Organization",
"id": "alphora-author",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/organization-deqm"
]
},
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "TAX",
"display": "Tax ID number"
}
]
},
"system": "urn:oid:2.16.840.1.113883.4.4",
"value": "12345678910",
"assigner": {
"display": "www.irs.gov"
}
}
],
"active": true,
"type": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/organization-type",
"code": "prov",
"display": "Healthcare Provider"
}
]
}
],
"name": "alphora-author",
"telecom": [
{
"system": "phone",
"value": "(+1) 401-555-1313"
}
],
"address": [
{
"line": [
"737 Lakewood Street"
],
"city": "Warwick",
"state": "RI",
"postalCode": "02886",
"country": "USA"
}
]
},
"request": {
"method": "PUT",
"url": "Organization/alphora-author"
}
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,277 @@
{
"resourceType": "Parameters",
"id": "EXM130-7.3.000-end-to-end-submit-data-bundle",
"parameter": [
{
"name": "measureReport",
"resource": {
"resourceType": "MeasureReport",
"id": "col-measurereport",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/datax-measurereport-deqm"
]
},
"extension": [
{
"url": "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-submitDataUpdateType",
"valueCode": "incremental"
}
],
"status": "complete",
"type": "data-collection",
"measure": "http://ecqi.healthit.gov/ecqms/Measure/ColorectalCancerScreeningsFHIR",
"subject": {
"reference": "Patient/numer-EXM130"
},
"date": "2021-01-01T16:59:52.404Z",
"reporter": {
"reference": "Organization/organization03"
},
"period": {
"start": "2019-01-01",
"end": "2019-12-31"
},
"evaluatedResource": [
{
"reference": "Procedure/numer-EXM130-2"
}
]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "numer-EXM130",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
]
},
"extension": [
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2028-9",
"display": "Asian"
}
}
]
},
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2135-2",
"display": "Hispanic or Latino"
}
}
]
}
],
"identifier": [
{
"use": "usual",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR",
"display": "Medical Record Number"
}
]
},
"system": "http://hospital.smarthealthit.org",
"value": "999999992"
}
],
"name": [
{
"family": "Blitz",
"given": [
"Don"
]
}
],
"gender": "male",
"birthDate": "1965-01-01"
}
},
{
"name": "resource",
"resource": {
"resourceType": "Encounter",
"id": "numer-EXM130-2",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter"
]
},
"status": "finished",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "AMB",
"display": "ambulatory"
},
"type": [
{
"coding": [
{
"system": "http://www.ama-assn.org/go/cpt",
"code": "99201",
"display": "Office or other outpatient visit for the evaluation and management of a new patient, which requires these 3 key components: A problem focused history; A problem focused examination; Straightforward medical decision making. Counseling and/or coordination of care with other physicians, other qualified health care professionals, or agencies are provided consistent with the nature of the problem(s) and the patient's and/or family's needs. Usually, the presenting problem(s) are self limited or minor. Typically, 10 minutes are spent face-to-face with the patient and/or family."
}
]
}
],
"subject": {
"reference": "Patient/numer-EXM130"
},
"period": {
"start": "2020-05-01T09:00:00-06:00",
"end": "2020-05-01T14:00:00-06:00"
}
}
},
{
"name": "resource",
"resource": {
"resourceType": "Procedure",
"id": "numer-EXM130-2",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-procedure"
]
},
"status": "completed",
"code": {
"coding": [
{
"system": "http://www.ama-assn.org/go/cpt",
"code": "44388",
"display": "Colonoscopy through stoma; with ablation of tumor(s), polyp(s), or other lesion(s) not amenable to removal by hot biopsy forceps, bipolar cautery or snare technique"
}
]
},
"subject": {
"reference": "Patient/numer-EXM130"
},
"performedPeriod": {
"start": "2020-05-01T10:00:00-06:00",
"end": "2020-05-01T12:00:00-06:00"
}
}
},
{
"name": "resource",
"resource": {
"resourceType": "Practitioner",
"id": "practitioner01",
"meta": {
"source": "http://example.org/fhir/server",
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/practitioner-deqm"
]
},
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "PRN",
"display": "Provider number"
}
]
},
"system": "http://hl7.org/fhir/sid/us-npi",
"value": "456789123"
}
],
"active": true,
"name": [
{
"family": "Hale",
"given": [
"Cody"
],
"suffix": [
"MD"
]
}
],
"gender": "male"
}
},
{
"name": "resource",
"resource": {
"resourceType": "Organization",
"id": "organization03",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/organization-deqm"
]
},
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "PRN",
"display": "Provider number"
}
]
},
"system": "http://hl7.org/fhir/sid/us-npi",
"value": "345678912",
"assigner": {
"display": "www.cms.gov"
}
}
],
"active": true,
"type": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/organization-type",
"code": "prov",
"display": "Healthcare Provider"
}
]
}
],
"name": "DaVinciHospital03",
"telecom": [
{
"system": "phone",
"value": "(+1) 201-555-1212"
}
],
"address": [
{
"line": [
"94 Olive Ave."
],
"city": "Union City",
"state": "NJ",
"postalCode": "07087",
"country": "USA"
}
]
}
}
]
}

View File

@ -0,0 +1,278 @@
{
"resourceType": "Parameters",
"id": "EXM130-7.3.000-end-to-end-submit-data-bundle",
"parameter": [
{
"name": "measureReport",
"resource": {
"resourceType": "MeasureReport",
"id": "col-measurereport",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/datax-measurereport-deqm"
]
},
"extension": [
{
"url": "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-submitDataUpdateType",
"valueCode": "incremental"
}
],
"status": "complete",
"type": "data-collection",
"measure": "http://ecqi.healthit.gov/ecqms/Measure/ColorectalCancerScreeningsFHIR",
"subject": {
"reference": "Patient/end-to-end-EXM130"
},
"date": "2021-01-01T16:59:52.404Z",
"reporter": {
"reference": "Organization/organization03"
},
"period": {
"start": "2019-01-01",
"end": "2019-12-31"
},
"evaluatedResource": [
{
"reference": "Encounter/end-to-end-EXM130-1",
"reference": "Procedure/end-to-end-EXM130-1"
}
]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Patient",
"id": "end-to-end-EXM130",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
]
},
"extension": [
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2028-9",
"display": "Asian"
}
}
]
},
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2135-2",
"display": "Hispanic or Latino"
}
}
]
}
],
"identifier": [
{
"use": "usual",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR",
"display": "Medical Record Number"
}
]
},
"system": "http://hospital.smarthealthit.org",
"value": "999999992"
}
],
"name": [
{
"family": "Blitz",
"given": [
"Don"
]
}
],
"gender": "male",
"birthDate": "1965-01-01"
}
},
{
"name": "resource",
"resource": {
"resourceType": "Practitioner",
"id": "practitioner01",
"meta": {
"source": "http://example.org/fhir/server",
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/practitioner-deqm"
]
},
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "PRN",
"display": "Provider number"
}
]
},
"system": "http://hl7.org/fhir/sid/us-npi",
"value": "456789123"
}
],
"active": true,
"name": [
{
"family": "Hale",
"given": [
"Cody"
],
"suffix": [
"MD"
]
}
],
"gender": "male"
}
},
{
"name": "resource",
"resource": {
"resourceType": "Organization",
"id": "organization03",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/organization-deqm"
]
},
"identifier": [
{
"use": "official",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "PRN",
"display": "Provider number"
}
]
},
"system": "http://hl7.org/fhir/sid/us-npi",
"value": "345678912",
"assigner": {
"display": "www.cms.gov"
}
}
],
"active": true,
"type": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/organization-type",
"code": "prov",
"display": "Healthcare Provider"
}
]
}
],
"name": "DaVinciHospital03",
"telecom": [
{
"system": "phone",
"value": "(+1) 201-555-1212"
}
],
"address": [
{
"line": [
"94 Olive Ave."
],
"city": "Union City",
"state": "NJ",
"postalCode": "07087",
"country": "USA"
}
]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Encounter",
"id": "end-to-end-EXM130-1",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter"
]
},
"status": "finished",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "AMB",
"display": "ambulatory"
},
"type": [
{
"coding": [
{
"system": "http://www.ama-assn.org/go/cpt",
"code": "99201",
"display": "Office or other outpatient visit for the evaluation and management of a new patient, which requires these 3 key components: A problem focused history; A problem focused examination; Straightforward medical decision making. Counseling and/or coordination of care with other physicians, other qualified health care professionals, or agencies are provided consistent with the nature of the problem(s) and the patient's and/or family's needs. Usually, the presenting problem(s) are self limited or minor. Typically, 10 minutes are spent face-to-face with the patient and/or family."
}
]
}
],
"subject": {
"reference": "Patient/end-to-end-EXM130"
},
"period": {
"start": "2020-01-01T09:00:00-06:00",
"end": "2020-01-01T14:00:00-06:00"
}
}
},
{
"name": "resource",
"resource": {
"resourceType": "Procedure",
"id": "end-to-end-EXM130-2",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-procedure"
]
},
"status": "completed",
"code": {
"coding": [
{
"system": "http://www.ama-assn.org/go/cpt",
"code": "33120",
"display": "Colonoscopy through stoma; with ablation of tumor(s), polyp(s), or other lesion(s) not amenable to removal by hot biopsy forceps, bipolar cautery or snare technique"
}
]
},
"subject": {
"reference": "Patient/end-to-end-EXM130"
},
"performedPeriod": {
"start": "2020-01-01T10:00:00-06:00",
"end": "2020-01-01T12:00:00-06:00"
}
}
}
]
}

View File

@ -0,0 +1,108 @@
{
"resourceType": "Parameters",
"id": "EXM130-7.3.000-end-to-end-submit-data-bundle",
"parameter": [
{
"name": "measureReport",
"resource": {
"resourceType": "MeasureReport",
"id": "col-measurereport-submit-data",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/datax-measurereport-deqm"
]
},
"extension": [
{
"url": "http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-submitDataUpdateType",
"valueCode": "incremental"
}
],
"status": "complete",
"type": "data-collection",
"measure": "http://ecqi.healthit.gov/ecqms/Measure/ColorectalCancerScreeningsFHIR",
"subject": {
"reference": "Patient/end-to-end-EXM130"
},
"date": "2021-01-01T16:59:52.404Z",
"reporter": {
"reference": "Organization/organization03"
},
"period": {
"start": "2019-01-01",
"end": "2019-12-31"
},
"evaluatedResource": [
{
"reference": "Procedure/end-to-end-EXM130-2"
}
]
}
},
{
"name": "resource",
"resource": {
"resourceType": "Encounter",
"id": "end-to-end-EXM130-2",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter"
]
},
"status": "finished",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "AMB",
"display": "ambulatory"
},
"type": [
{
"coding": [
{
"system": "http://www.ama-assn.org/go/cpt",
"code": "99201",
"display": "Office or other outpatient visit for the evaluation and management of a new patient, which requires these 3 key components: A problem focused history; A problem focused examination; Straightforward medical decision making. Counseling and/or coordination of care with other physicians, other qualified health care professionals, or agencies are provided consistent with the nature of the problem(s) and the patient's and/or family's needs. Usually, the presenting problem(s) are self limited or minor. Typically, 10 minutes are spent face-to-face with the patient and/or family."
}
]
}
],
"subject": {
"reference": "Patient/end-to-end-EXM130"
},
"period": {
"start": "2020-05-01T09:00:00-06:00",
"end": "2020-05-01T14:00:00-06:00"
}
}
},
{
"name": "resource",
"resource": {
"resourceType": "Procedure",
"id": "end-to-end-EXM130-2",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-procedure"
]
},
"status": "completed",
"code": {
"coding": [
{
"system": "http://www.ama-assn.org/go/cpt",
"code": "44388",
"display": "Colonoscopy through stoma; with ablation of tumor(s), polyp(s), or other lesion(s) not amenable to removal by hot biopsy forceps, bipolar cautery or snare technique"
}
]
},
"subject": {
"reference": "Patient/end-to-end-EXM130"
},
"performedPeriod": {
"start": "2020-05-01T10:00:00-06:00",
"end": "2020-05-01T12:00:00-06:00"
}
}
}
]
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,18 @@
{
"resourceType": "Group",
"id": "gic-gr-1",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/gaps-group-deqm"
]
},
"type": "person",
"actual": true,
"member": [
{
"entity": {
"reference": "Patient/numer-EXM125"
}
}
]
}

View File

@ -0,0 +1,23 @@
{
"resourceType": "Group",
"id": "gic-gr-parallel",
"meta": {
"profile": [
"http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/gaps-group-deqm"
]
},
"type": "person",
"actual": true,
"member": [
{
"entity": {
"reference": "Patient/numer-EXM125"
}
},
{
"entity": {
"reference": "Patient/denom-EXM125"
}
}
]
}

View File

@ -0,0 +1,63 @@
{
"resourceType": "Patient",
"id": "numer-EXM125",
"meta": {
"profile": [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
]
},
"extension": [
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2028-9",
"display": "Asian"
}
}
]
},
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2135-2",
"display": "Hispanic or Latino"
}
}
]
}
],
"identifier": [
{
"use": "usual",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR",
"display": "Medical Record Number"
}
]
},
"system": "http://hospital.smarthealthit.org",
"value": "999999995"
}
],
"name": [
{
"family": "McCarren",
"given": [
"Karen"
]
}
],
"gender": "female",
"birthDate": "1965-01-01"
}