$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>
|
<artifactId>hapi-fhir-storage</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||||
|
<artifactId>hapi-fhir-storage-cr</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</profile>
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
|
|
|
@ -71,6 +71,11 @@
|
||||||
<artifactId>hapi-fhir-structures-dstu3</artifactId>
|
<artifactId>hapi-fhir-structures-dstu3</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||||
|
<artifactId>hapi-fhir-base</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</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.engine.retrieve.BundleRetrieveProvider;
|
||||||
import org.opencds.cqf.cql.evaluator.fhir.Constants;
|
import org.opencds.cqf.cql.evaluator.fhir.Constants;
|
||||||
import org.opencds.cqf.cql.evaluator.fhir.adapter.AdapterFactory;
|
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.measure.MeasureEvaluationOptions;
|
||||||
import org.opencds.cqf.cql.evaluator.spring.fhir.adapter.AdapterConfiguration;
|
import org.opencds.cqf.cql.evaluator.spring.fhir.adapter.AdapterConfiguration;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -111,26 +110,22 @@ public abstract class BaseClinicalReasoningConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CrProperties.CqlProperties cqlProperties(CrProperties theCrProperties) {
|
public CrProperties.CqlProperties cqlProperties(CrProperties theCrProperties) {
|
||||||
return theCrProperties.getCql();
|
return theCrProperties.getCqlProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CrProperties.MeasureProperties measureProperties(CrProperties theCrProperties) {
|
public CrProperties.MeasureProperties measureProperties(CrProperties theCrProperties) {
|
||||||
return theCrProperties.getMeasure();
|
return theCrProperties.getMeasureProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public MeasureEvaluationOptions measureEvaluationOptions(CrProperties theCrProperties) {
|
public MeasureEvaluationOptions measureEvaluationOptions(CrProperties theCrProperties) {
|
||||||
theCrProperties.getMeasure();
|
return theCrProperties.getMeasureProperties().getMeasureEvaluationOptions();
|
||||||
MeasureEvaluationOptions measureEvaluation = theCrProperties.getMeasure().getMeasureEvaluation();
|
|
||||||
return measureEvaluation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CqlOptions cqlOptions(CrProperties theCrProperties) {
|
public CqlOptions cqlOptions(CrProperties theCrProperties) {
|
||||||
return theCrProperties.getCql().getOptions();
|
return theCrProperties.getCqlProperties().getCqlOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -140,7 +135,7 @@ public abstract class BaseClinicalReasoningConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CqlTranslatorOptions cqlTranslatorOptions(FhirContext theFhirContext, CrProperties.CqlProperties theCqlProperties) {
|
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)
|
if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.R4)
|
||||||
&& (options.getCompatibilityLevel().equals("1.5") || options.getCompatibilityLevel().equals("1.4"))) {
|
&& (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) {
|
ModelManager theModelManager, CqlTranslatorOptions theCqlTranslatorOptions, CrProperties.CqlProperties theCqlProperties) {
|
||||||
return lcp -> {
|
return lcp -> {
|
||||||
|
|
||||||
if (theCqlProperties.getOptions().useEmbeddedLibraries()) {
|
if (theCqlProperties.getCqlOptions().useEmbeddedLibraries()) {
|
||||||
lcp.add(new FhirLibrarySourceProvider());
|
lcp.add(new FhirLibrarySourceProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,80 +26,87 @@ import org.opencds.cqf.cql.evaluator.measure.MeasureEvaluationOptions;
|
||||||
|
|
||||||
public class CrProperties {
|
public class CrProperties {
|
||||||
|
|
||||||
private boolean enabled = true;
|
private boolean myCqlEnabled = true;
|
||||||
private MeasureProperties measureProperties;
|
private MeasureProperties myMeasureProperties;
|
||||||
private CqlProperties cqlProperties = new CqlProperties();
|
private CqlProperties myCqlProperties = new CqlProperties();
|
||||||
|
|
||||||
public CrProperties () {
|
public CrProperties () {
|
||||||
this.measureProperties = new MeasureProperties();
|
this.myMeasureProperties = new MeasureProperties();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isCqlEnabled() {
|
||||||
return enabled;
|
return myCqlEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEnabled(boolean enabled) {
|
public void setCqlEnabled(boolean theCqlEnabled) {
|
||||||
this.enabled = enabled;
|
this.myCqlEnabled = theCqlEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MeasureProperties getMeasure() {
|
public MeasureProperties getMeasureProperties() {
|
||||||
return measureProperties;
|
return myMeasureProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMeasure(MeasureProperties measureProperties) {
|
public void setMeasureProperties(MeasureProperties theMeasureProperties) {
|
||||||
this.measureProperties = measureProperties;
|
this.myMeasureProperties = theMeasureProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CqlProperties getCql() {
|
public CqlProperties getCqlProperties() {
|
||||||
return cqlProperties;
|
return myCqlProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCql(CqlProperties cqlProperties) {
|
public void setCqlProperties(CqlProperties theCqlProperties) {
|
||||||
this.cqlProperties = cqlProperties;
|
this.myCqlProperties = theCqlProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class MeasureProperties {
|
public static class MeasureProperties {
|
||||||
|
|
||||||
private boolean threadedCareGapsEnabled = true;
|
private boolean myThreadedCareGapsEnabled = true;
|
||||||
private MeasureReportConfiguration measureReportConfiguration;
|
private MeasureReportConfiguration myMeasureReportConfiguration;
|
||||||
private MeasureEvaluationOptions measureEvaluationOptions;
|
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() {
|
public MeasureProperties() {
|
||||||
measureEvaluationOptions = MeasureEvaluationOptions.defaultOptions();
|
myMeasureEvaluationOptions = MeasureEvaluationOptions.defaultOptions();
|
||||||
measureEvaluationOptions.setNumThreads(4);
|
myMeasureEvaluationOptions.setNumThreads(DEFAULT_THREADS_FOR_MEASURE_EVAL);
|
||||||
measureEvaluationOptions.setThreadedBatchSize(250);
|
myMeasureEvaluationOptions.setThreadedBatchSize(DEFAULT_THREADS_BATCH_SIZE);
|
||||||
measureEvaluationOptions.setThreadedEnabled(true);
|
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
|
//care gaps
|
||||||
public boolean getThreadedCareGapsEnabled() {
|
public boolean getThreadedCareGapsEnabled() {
|
||||||
return threadedCareGapsEnabled;
|
return myThreadedCareGapsEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setThreadedCareGapsEnabled(boolean enabled) {
|
public void setThreadedCareGapsEnabled(boolean theThreadedCareGapsEnabled) {
|
||||||
this.threadedCareGapsEnabled = enabled;
|
myThreadedCareGapsEnabled = theThreadedCareGapsEnabled;
|
||||||
|
}
|
||||||
|
public boolean isThreadedCareGapsEnabled() {
|
||||||
|
return myThreadedCareGapsEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
//report configuration
|
//report configuration
|
||||||
public MeasureReportConfiguration getMeasureReport() {
|
public MeasureReportConfiguration getMeasureReportConfiguration() {
|
||||||
return this.measureReportConfiguration;
|
return myMeasureReportConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMeasureReport(MeasureReportConfiguration measureReport) {
|
public void setMeasureReportConfiguration(MeasureReportConfiguration theMeasureReport) {
|
||||||
this.measureReportConfiguration = measureReport;
|
myMeasureReportConfiguration = theMeasureReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//measure evaluations
|
||||||
|
public void setMeasureEvaluationOptions(MeasureEvaluationOptions theMeasureEvaluation) {
|
||||||
|
myMeasureEvaluationOptions = theMeasureEvaluation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MeasureEvaluationOptions getMeasureEvaluationOptions() {
|
||||||
|
return myMeasureEvaluationOptions;
|
||||||
|
}
|
||||||
|
|
||||||
public static class MeasureReportConfiguration {
|
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
|
* <a href="http://build.fhir.org/ig/HL7/davinci-deqm/index.html">Da Vinci DEQM
|
||||||
* FHIR Implementation Guide</a>.
|
* FHIR Implementation Guide</a>.
|
||||||
**/
|
**/
|
||||||
private String careGapsReporter;
|
private String myCareGapsReporter;
|
||||||
/**
|
/**
|
||||||
* Implements the author element of the <a href=
|
* Implements the author element of the <a href=
|
||||||
* "http://www.hl7.org/fhir/composition.html">Composition</a> FHIR
|
* "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
|
* <a href="http://build.fhir.org/ig/HL7/davinci-deqm/index.html">Da Vinci DEQM
|
||||||
* FHIR Implementation Guide</a>.
|
* FHIR Implementation Guide</a>.
|
||||||
**/
|
**/
|
||||||
private String careGapsCompositionSectionAuthor;
|
private String myCareGapsCompositionSectionAuthor;
|
||||||
|
|
||||||
public String getReporter() {
|
public String getCareGapsReporter() {
|
||||||
return careGapsReporter;
|
return myCareGapsReporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCareGapsReporter(String careGapsReporter) {
|
public void setCareGapsReporter(String theCareGapsReporter) {
|
||||||
this.careGapsReporter = null;// ResourceBuilder.ensureOrganizationReference(careGapsReporter);
|
myCareGapsReporter = theCareGapsReporter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCompositionAuthor() {
|
public String getCareGapsCompositionSectionAuthor() {
|
||||||
return careGapsCompositionSectionAuthor;
|
return myCareGapsCompositionSectionAuthor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCareGapsCompositionSectionAuthor(String careGapsCompositionSectionAuthor) {
|
public void setCareGapsCompositionSectionAuthor(String theCareGapsCompositionSectionAuthor) {
|
||||||
this.careGapsCompositionSectionAuthor = careGapsCompositionSectionAuthor;
|
myCareGapsCompositionSectionAuthor = theCareGapsCompositionSectionAuthor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,41 +154,41 @@ public class CrProperties {
|
||||||
|
|
||||||
public static class CqlProperties {
|
public static class CqlProperties {
|
||||||
|
|
||||||
private boolean useEmbeddedLibraries = true;
|
private boolean myCqlUseOfEmbeddedLibraries = true;
|
||||||
|
|
||||||
private CqlEngineOptions runtimeOptions = CqlEngineOptions.defaultOptions();
|
private CqlEngineOptions myCqlRuntimeOptions = CqlEngineOptions.defaultOptions();
|
||||||
private CqlTranslatorOptions compilerOptions = CqlTranslatorOptions.defaultOptions();
|
private CqlTranslatorOptions myCqlTranslatorOptions = CqlTranslatorOptions.defaultOptions();
|
||||||
|
|
||||||
|
|
||||||
public boolean useEmbeddedLibraries() {
|
public boolean isCqlUseOfEmbeddedLibraries() {
|
||||||
return this.useEmbeddedLibraries;
|
return myCqlUseOfEmbeddedLibraries;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUseEmbeddedLibraries(boolean useEmbeddedLibraries) {
|
public void setCqlUseOfEmbeddedLibraries(boolean theCqlUseOfEmbeddedLibraries) {
|
||||||
this.useEmbeddedLibraries = useEmbeddedLibraries;
|
myCqlUseOfEmbeddedLibraries = theCqlUseOfEmbeddedLibraries;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CqlEngineOptions getRuntime() {
|
public CqlEngineOptions getCqlRuntimeOptions() {
|
||||||
return this.runtimeOptions;
|
return myCqlRuntimeOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRuntime(CqlEngineOptions runtime) {
|
public void setCqlRuntimeOptions(CqlEngineOptions theRuntime) {
|
||||||
this.runtimeOptions = runtime;
|
myCqlRuntimeOptions = theRuntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CqlTranslatorOptions getCompiler() {
|
public CqlTranslatorOptions getCqlTranslatorOptions() {
|
||||||
return this.compilerOptions;
|
return myCqlTranslatorOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCompiler(CqlTranslatorOptions compiler) {
|
public void setCqlTranslatorOptions(CqlTranslatorOptions theCqlTranslatorOptions) {
|
||||||
this.compilerOptions = compiler;
|
myCqlTranslatorOptions = theCqlTranslatorOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CqlOptions getOptions() {
|
public CqlOptions getCqlOptions() {
|
||||||
CqlOptions cqlOptions = new CqlOptions();
|
CqlOptions cqlOptions = new CqlOptions();
|
||||||
cqlOptions.setUseEmbeddedLibraries(this.useEmbeddedLibraries());
|
cqlOptions.setUseEmbeddedLibraries(isCqlUseOfEmbeddedLibraries());
|
||||||
cqlOptions.setCqlEngineOptions(this.getRuntime());
|
cqlOptions.setCqlEngineOptions(getCqlRuntimeOptions());
|
||||||
cqlOptions.setCqlTranslatorOptions(this.getCompiler());
|
cqlOptions.setCqlTranslatorOptions(getCqlTranslatorOptions());
|
||||||
return cqlOptions;
|
return cqlOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,14 @@
|
||||||
*/
|
*/
|
||||||
package ca.uhn.fhir.cr.config;
|
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.MeasureOperationsProvider;
|
||||||
import ca.uhn.fhir.cr.r4.measure.MeasureService;
|
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 ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
@ -51,4 +57,31 @@ public class CrR4Config extends BaseClinicalReasoningConfig {
|
||||||
public MeasureOperationsProvider r4measureOperationsProvider() {
|
public MeasureOperationsProvider r4measureOperationsProvider() {
|
||||||
return new MeasureOperationsProvider();
|
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.
|
* limitations under the License.
|
||||||
* #L%
|
* #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_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_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_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 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;
|
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.IDataProviderFactory;
|
||||||
import ca.uhn.fhir.cr.common.IFhirDalFactory;
|
import ca.uhn.fhir.cr.common.IFhirDalFactory;
|
||||||
import ca.uhn.fhir.cr.common.ILibrarySourceProviderFactory;
|
import ca.uhn.fhir.cr.common.ILibrarySourceProviderFactory;
|
||||||
import ca.uhn.fhir.cr.common.ITerminologyProviderFactory;
|
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.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||||
|
import ca.uhn.fhir.util.BundleBuilder;
|
||||||
import org.cqframework.cql.cql2elm.LibrarySourceProvider;
|
import org.cqframework.cql.cql2elm.LibrarySourceProvider;
|
||||||
import org.hl7.fhir.dstu3.model.Bundle;
|
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.Endpoint;
|
||||||
|
import org.hl7.fhir.dstu3.model.Enumerations;
|
||||||
import org.hl7.fhir.dstu3.model.Extension;
|
import org.hl7.fhir.dstu3.model.Extension;
|
||||||
import org.hl7.fhir.dstu3.model.IdType;
|
import org.hl7.fhir.dstu3.model.IdType;
|
||||||
import org.hl7.fhir.dstu3.model.Measure;
|
import org.hl7.fhir.dstu3.model.Measure;
|
||||||
import org.hl7.fhir.dstu3.model.MeasureReport;
|
import org.hl7.fhir.dstu3.model.MeasureReport;
|
||||||
|
import org.hl7.fhir.dstu3.model.SearchParameter;
|
||||||
import org.hl7.fhir.dstu3.model.StringType;
|
import org.hl7.fhir.dstu3.model.StringType;
|
||||||
import org.opencds.cqf.cql.engine.data.DataProvider;
|
import org.opencds.cqf.cql.engine.data.DataProvider;
|
||||||
import org.opencds.cqf.cql.engine.fhir.terminology.Dstu3FhirTerminologyProvider;
|
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.opencds.cqf.cql.evaluator.measure.MeasureEvaluationOptions;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
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
|
@Autowired
|
||||||
protected ITerminologyProviderFactory myTerminologyProviderFactory;
|
protected ITerminologyProviderFactory myTerminologyProviderFactory;
|
||||||
|
@ -121,7 +176,7 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
|
||||||
Bundle theAdditionalData,
|
Bundle theAdditionalData,
|
||||||
Endpoint theTerminologyEndpoint) {
|
Endpoint theTerminologyEndpoint) {
|
||||||
|
|
||||||
ensureSupplementalDataElementSearchParameter(myRequestDetails);
|
ensureSupplementalDataElementSearchParameter();
|
||||||
|
|
||||||
Measure measure = read(theId, myRequestDetails);
|
Measure measure = read(theId, myRequestDetails);
|
||||||
|
|
||||||
|
@ -161,4 +216,12 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
|
||||||
return this.myDaoRegistry;
|
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;
|
import java.util.function.Function;
|
||||||
|
|
||||||
@Component
|
|
||||||
public class MeasureOperationsProvider {
|
public class MeasureOperationsProvider {
|
||||||
@Autowired
|
@Autowired
|
||||||
Function<RequestDetails, MeasureService> myR4MeasureServiceFactory;
|
Function<RequestDetails, MeasureService> myR4MeasureServiceFactory;
|
||||||
|
|
|
@ -19,27 +19,34 @@
|
||||||
*/
|
*/
|
||||||
package ca.uhn.fhir.cr.r4.measure;
|
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.IDataProviderFactory;
|
||||||
import ca.uhn.fhir.cr.common.IFhirDalFactory;
|
import ca.uhn.fhir.cr.common.IFhirDalFactory;
|
||||||
import ca.uhn.fhir.cr.common.ILibrarySourceProviderFactory;
|
import ca.uhn.fhir.cr.common.ILibrarySourceProviderFactory;
|
||||||
import ca.uhn.fhir.cr.common.ITerminologyProviderFactory;
|
import ca.uhn.fhir.cr.common.ITerminologyProviderFactory;
|
||||||
import ca.uhn.fhir.cr.common.SupplementalDataConstants;
|
import ca.uhn.fhir.cr.constant.MeasureReportConstants;
|
||||||
import ca.uhn.fhir.cr.r4.ISupplementalDataSearchParamUser;
|
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||||
|
import ca.uhn.fhir.util.BundleBuilder;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.cqframework.cql.cql2elm.LibrarySourceProvider;
|
import org.cqframework.cql.cql2elm.LibrarySourceProvider;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
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.Endpoint;
|
||||||
|
import org.hl7.fhir.r4.model.Enumerations;
|
||||||
import org.hl7.fhir.r4.model.Extension;
|
import org.hl7.fhir.r4.model.Extension;
|
||||||
import org.hl7.fhir.r4.model.IdType;
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
import org.hl7.fhir.r4.model.Measure;
|
import org.hl7.fhir.r4.model.Measure;
|
||||||
import org.hl7.fhir.r4.model.MeasureReport;
|
import org.hl7.fhir.r4.model.MeasureReport;
|
||||||
|
import org.hl7.fhir.r4.model.SearchParameter;
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
import org.opencds.cqf.cql.engine.data.DataProvider;
|
import org.opencds.cqf.cql.engine.data.DataProvider;
|
||||||
import org.opencds.cqf.cql.engine.fhir.terminology.R4FhirTerminologyProvider;
|
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.dal.FhirDal;
|
||||||
import org.opencds.cqf.cql.evaluator.fhir.util.Clients;
|
import org.opencds.cqf.cql.evaluator.fhir.util.Clients;
|
||||||
import org.opencds.cqf.cql.evaluator.measure.MeasureEvaluationOptions;
|
import org.opencds.cqf.cql.evaluator.measure.MeasureEvaluationOptions;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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
|
@Autowired
|
||||||
protected ITerminologyProviderFactory myTerminologyProviderFactory;
|
protected ITerminologyProviderFactory myTerminologyProviderFactory;
|
||||||
|
@ -130,7 +188,7 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
|
||||||
Bundle theAdditionalData,
|
Bundle theAdditionalData,
|
||||||
Endpoint theTerminologyEndpoint) {
|
Endpoint theTerminologyEndpoint) {
|
||||||
|
|
||||||
ensureSupplementalDataElementSearchParameter(myRequestDetails);
|
ensureSupplementalDataElementSearchParameter();
|
||||||
|
|
||||||
Measure measure = read(theId, myRequestDetails);
|
Measure measure = read(theId, myRequestDetails);
|
||||||
|
|
||||||
|
@ -171,10 +229,10 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
|
||||||
return measureReport;
|
return measureReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getPractitionerPatients(String practitioner, RequestDetails theRequestDetails) {
|
private List<String> getPractitionerPatients(String thePractitioner, RequestDetails theRequestDetails) {
|
||||||
SearchParameterMap map = SearchParameterMap.newSynchronous();
|
SearchParameterMap map = SearchParameterMap.newSynchronous();
|
||||||
map.add("general-practitioner", new ReferenceParam(
|
map.add("general-practitioner", new ReferenceParam(
|
||||||
practitioner.startsWith("Practitioner/") ? practitioner : "Practitioner/" + practitioner));
|
thePractitioner.startsWith("Practitioner/") ? thePractitioner : "Practitioner/" + thePractitioner));
|
||||||
List<String> patients = new ArrayList<>();
|
List<String> patients = new ArrayList<>();
|
||||||
IBundleProvider patientProvider = myDaoRegistry.getResourceDao("Patient").search(map, theRequestDetails);
|
IBundleProvider patientProvider = myDaoRegistry.getResourceDao("Patient").search(map, theRequestDetails);
|
||||||
List<IBaseResource> patientList = patientProvider.getAllResources();
|
List<IBaseResource> patientList = patientProvider.getAllResources();
|
||||||
|
@ -182,12 +240,12 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
|
||||||
return patients;
|
return patients;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addProductLineExtension(MeasureReport measureReport, String productLine) {
|
private void addProductLineExtension(MeasureReport theMeasureReport, String theProductLine) {
|
||||||
if (productLine != null) {
|
if (theProductLine != null) {
|
||||||
Extension ext = new Extension();
|
Extension ext = new Extension();
|
||||||
ext.setUrl(SupplementalDataConstants.MEASUREREPORT_PRODUCT_LINE_EXT_URL);
|
ext.setUrl(MeasureReportConstants.MEASUREREPORT_PRODUCT_LINE_EXT_URL);
|
||||||
ext.setValue(new StringType(productLine));
|
ext.setValue(new StringType(theProductLine));
|
||||||
measureReport.addExtension(ext);
|
theMeasureReport.addExtension(ext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,4 +254,12 @@ public class MeasureService implements ISupplementalDataSearchParamUser {
|
||||||
return this.myDaoRegistry;
|
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.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.test.BaseJpaDstu3Test;
|
import ca.uhn.fhir.jpa.test.BaseJpaDstu3Test;
|
||||||
import ca.uhn.fhir.parser.IParser;
|
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.HoverflyDsl;
|
||||||
import io.specto.hoverfly.junit.dsl.StubServiceBuilder;
|
import io.specto.hoverfly.junit.dsl.StubServiceBuilder;
|
||||||
import io.specto.hoverfly.junit.rule.HoverflyRule;
|
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.OperationOutcome;
|
||||||
import org.hl7.fhir.dstu3.model.Resource;
|
import org.hl7.fhir.dstu3.model.Resource;
|
||||||
import org.hl7.fhir.dstu3.model.ValueSet;
|
import org.hl7.fhir.dstu3.model.ValueSet;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.test.context.ContextConfiguration;
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
@ -55,14 +57,6 @@ public abstract class BaseCrDstu3Test extends BaseJpaDstu3Test implements IResou
|
||||||
return ourFhirContext;
|
return ourFhirContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bundle loadBundle(String theLocation) {
|
|
||||||
return loadBundle(Bundle.class, theLocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IParser getFhirParser() {
|
|
||||||
return ourParser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StubServiceBuilder mockNotFound(String theResource) {
|
public StubServiceBuilder mockNotFound(String theResource) {
|
||||||
OperationOutcome outcome = new OperationOutcome();
|
OperationOutcome outcome = new OperationOutcome();
|
||||||
outcome.getText().setStatusAsString("generated");
|
outcome.getText().setStatusAsString("generated");
|
||||||
|
@ -106,10 +100,6 @@ public abstract class BaseCrDstu3Test extends BaseJpaDstu3Test implements IResou
|
||||||
.willReturn(success());
|
.willReturn(success());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bundle makeBundle(List<? extends Resource> theResources) {
|
|
||||||
return makeBundle(theResources.toArray(new Resource[theResources.size()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bundle makeBundle(Resource... theResources) {
|
public Bundle makeBundle(Resource... theResources) {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.setType(Bundle.BundleType.SEARCHSET);
|
bundle.setType(Bundle.BundleType.SEARCHSET);
|
||||||
|
@ -121,4 +111,8 @@ public abstract class BaseCrDstu3Test extends BaseJpaDstu3Test implements IResou
|
||||||
}
|
}
|
||||||
return bundle;
|
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 {
|
public abstract class BaseCrR4Test extends BaseResourceProviderR4Test implements IResourceLoader {
|
||||||
protected static final FhirContext ourFhirContext = FhirContext.forR4Cached();
|
protected static final FhirContext ourFhirContext = FhirContext.forR4Cached();
|
||||||
private static final IParser ourParser = ourFhirContext.newJsonParser().setPrettyPrint(true);
|
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
|
@ClassRule
|
||||||
public static HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode(dsl(
|
public static HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode(dsl(
|
||||||
service(TEST_ADDRESS)
|
service(TEST_ADDRESS)
|
||||||
.get("/fhir/metadata")
|
.get("/metadata")
|
||||||
.willReturn(success(getCapabilityStatement().toString(), "application/json"))
|
.willReturn(success(getCapabilityStatement().toString(), "application/json"))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
package ca.uhn.fhir.cr;
|
package ca.uhn.fhir.cr;
|
||||||
|
|
||||||
import ca.uhn.fhir.cr.common.IDaoRegistryUser;
|
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.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||||
import ca.uhn.fhir.util.ClasspathUtil;
|
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.IBaseBundle;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
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.
|
* 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;
|
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.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
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);
|
private static final Logger ourLog = LoggerFactory.getLogger(CrProviderDstu3Test.class);
|
||||||
protected final RequestDetails myRequestDetails = RequestDetailsHelper.newServletRequestDetails();
|
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);
|
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)
|
// 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);
|
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);
|
assertNotNull(result);
|
||||||
List<Bundle.BundleEntryComponent> entries = result.getEntry();
|
List<Bundle.BundleEntryComponent> entries = result.getEntry();
|
||||||
assertThat(entries, hasSize(22));
|
assertEquals(entries.size(), 22);
|
||||||
// assertEquals(entries.get(0).getResponse().getStatus(), "201 Created");
|
|
||||||
// assertEquals(entries.get(21).getResponse().getStatus(), "201 Created");
|
|
||||||
|
|
||||||
IdType measureId = new IdType("Measure", "measure-asf");
|
IdType measureId = new IdType("Measure", "measure-asf");
|
||||||
String patient = "Patient/Patient-6529";
|
String patient = "Patient/Patient-6529";
|
||||||
|
@ -104,8 +101,8 @@ public class CrProviderDstu3Test extends BaseCrDstu3Test {
|
||||||
null,
|
null,
|
||||||
myRequestDetails);
|
myRequestDetails);
|
||||||
// Assert it worked
|
// Assert it worked
|
||||||
assertThat(report.getGroup(), hasSize(2));
|
assertEquals(report.getGroup().size(), 2);
|
||||||
assertThat(report.getGroup().get(0).getPopulation(), hasSize(3));
|
assertEquals(report.getGroup().get(0).getPopulation().size(), 3);
|
||||||
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
|
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
|
||||||
|
|
||||||
// Now timed runs
|
// Now timed runs
|
||||||
|
@ -147,8 +144,8 @@ public class CrProviderDstu3Test extends BaseCrDstu3Test {
|
||||||
MeasureReport report = myMeasureOperationsProvider.evaluateMeasure(measureId, periodStart, periodEnd, "population",
|
MeasureReport report = myMeasureOperationsProvider.evaluateMeasure(measureId, periodStart, periodEnd, "population",
|
||||||
null, null, null, null, null, null, myRequestDetails);
|
null, null, null, null, null, null, myRequestDetails);
|
||||||
// Assert it worked
|
// Assert it worked
|
||||||
assertThat(report.getGroup(), hasSize(2));
|
assertEquals(report.getGroup().size(), 2);
|
||||||
assertThat(report.getGroup().get(0).getPopulation(), hasSize(3));
|
assertEquals(report.getGroup().get(0).getPopulation().size(), 3);
|
||||||
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
|
ourLog.debug(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
|
||||||
|
|
||||||
// Now timed runs
|
// Now timed runs
|
||||||
|
|
|
@ -70,17 +70,17 @@ class MeasureOperationsProviderTest extends BaseCrDstu3Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMeasureEvaluateWithTerminology(Hoverfly hoverfly) throws IOException {
|
void testMeasureEvaluateWithTerminology() throws IOException {
|
||||||
loadBundle("Exm105Fhir3Measure.json");
|
loadBundle("Exm105Fhir3Measure.json");
|
||||||
|
|
||||||
var returnMeasureReport = this.myMeasureOperationsProvider.evaluateMeasure(
|
var returnMeasureReport = this.myMeasureOperationsProvider.evaluateMeasure(
|
||||||
new IdType("Measure", "measure-EXM105-FHIR3-8.0.000"),
|
new IdType("Measure", "measure-EXM105-FHIR3-8.0.000"),
|
||||||
"2019-01-01",
|
"2019-01-01",
|
||||||
"2020-01-01",
|
"2020-01-01",
|
||||||
"individual",
|
"patient",
|
||||||
"Patient/denom-EXM105-FHIR3",
|
"Patient/denom-EXM105-FHIR3",
|
||||||
null,
|
null,
|
||||||
"2019-12-12",
|
null,
|
||||||
null,
|
null,
|
||||||
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