$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:
parent
3a2b722093
commit
450ccb5599
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
type: add
|
||||
issue: 4562
|
||||
title: "Added support for the $care-gaps operation defined by the DaVinci DEQM IG"
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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";
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -36,7 +36,6 @@ import org.springframework.stereotype.Component;
|
|||
|
||||
import java.util.function.Function;
|
||||
|
||||
@Component
|
||||
public class MeasureOperationsProvider {
|
||||
@Autowired
|
||||
Function<RequestDetails, MeasureService> myR4MeasureServiceFactory;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
));
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
@ -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
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue