Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.opencds.cqf.fhir.cr.hapi.config.r4;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.api.server.IRepositoryFactory;
import ca.uhn.fhir.rest.server.RestfulServer;
import java.util.List;
import java.util.Map;
import org.opencds.cqf.fhir.cr.hapi.config.ProviderLoader;
import org.opencds.cqf.fhir.cr.hapi.config.ProviderSelector;
import org.opencds.cqf.fhir.cr.hapi.config.RepositoryConfig;
import org.opencds.cqf.fhir.cr.hapi.r4.ISubmitDataProcessorFactory;
import org.opencds.cqf.fhir.cr.hapi.r4.ra.SubmitDataProvider;
import org.opencds.cqf.fhir.cr.hapi.r4.ra.SubmitRemarkDataProvider;
import org.opencds.cqf.fhir.cr.measure.r4.R4SubmitDataService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({RepositoryConfig.class})
public class RiskAdjustmentOperationConfig {

@Bean
ISubmitDataProcessorFactory r4SubmitDataProcessorFactory(IRepositoryFactory repositoryFactory) {
return rd -> new R4SubmitDataService(repositoryFactory.create(rd));
}

@Bean(name = "riskAdjustmentSubmitDataProvider")
SubmitDataProvider r4RaSubmitDataProvider(ISubmitDataProcessorFactory r4SubmitDataProcessorFactory) {
return new SubmitDataProvider(r4SubmitDataProcessorFactory);
}

@Bean(name = "riskAdjustmentSubmitRemarkDataProvider")
SubmitRemarkDataProvider r4RaSubmitDataRemarkProvider(ISubmitDataProcessorFactory r4SubmitDataProcessorFactory) {
return new SubmitRemarkDataProvider(r4SubmitDataProcessorFactory);
}

@Bean(name = "riskAdjustmentOperationLoader")
public ProviderLoader riskAdjustmentOperationLoader(
ApplicationContext applicationContext, FhirContext fhirContext, RestfulServer restfulServer) {

var selector = new ProviderSelector(
fhirContext,
Map.of(FhirVersionEnum.R4, List.of(SubmitDataProvider.class, SubmitRemarkDataProvider.class)));

return new ProviderLoader(restfulServer, applicationContext, selector);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.opencds.cqf.fhir.cr.hapi.r4.ra;

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 ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.opencds.cqf.fhir.cr.hapi.r4.ISubmitDataProcessorFactory;

public class SubmitDataProvider {

// Operation canonical name and RA MeasureReport profile canonical
private static final String OPERATION_NAME = "$ra-submit-data";
private static final String RA_DATAEX_MR_PROFILE =
"http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-datax-measurereport";

private final ISubmitDataProcessorFactory r4SubmitDataProcessorFactory;

public SubmitDataProvider(ISubmitDataProcessorFactory r4SubmitDataProcessorFactory) {
this.r4SubmitDataProcessorFactory =
Objects.requireNonNull(r4SubmitDataProcessorFactory, "r4SubmitDataProcessorFactory is required");
}

/**
* The $ra-submit-data operation is used to submit a Risk Adjustment Data Exchange
* MeasureReport and related data-of-interest resources.
* Endpoint: [base]/Measure/$ra-submit-data
* Deviation from base $submit-data:
* - The {@code measureReport} input MUST conform to the
* <a href="http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-datax-measurereport">RA Data Exchange MeasureReport profile</a>
*
* @param requestDetails Autopopulated by HAPI.
* @param report The RA Data Exchange MeasureReport (1..1).
* @param resources Related data-of-interest resources (0..*).
* @return A transaction response {@link Bundle}.
*/
@Description(
shortDefinition = "$ra-submit-data",
value =
"Implements the <a href=\"http://hl7.org/fhir/us/davinci-ra/OperationDefinition/submit-data\">$ra-submit-data</a> operation.")
@Operation(name = OPERATION_NAME, type = Measure.class)
public Bundle submitData(
RequestDetails requestDetails,
@OperationParam(name = "measureReport", min = 1, max = 1) MeasureReport report,
@OperationParam(name = "resource") List<IBaseResource> resources) {

// Validate required input
if (report == null) {
throw new InvalidRequestException("Parameter 'measureReport' must not be null.");
}
if (!hasProfile(report)) {
String profiles = report.hasMeta() && report.getMeta().hasProfile()
? report.getMeta().getProfile().stream()
.map(CanonicalType::asStringValue)
.collect(Collectors.joining(", "))
: "(none)";
throw new InvalidRequestException("The 'measureReport' MUST conform to profile: " + RA_DATAEX_MR_PROFILE
+ ". Provided profiles: " + profiles + ".");
}

// Normalize optional inputs
List<IBaseResource> safeResources = resources == null
? Collections.emptyList()
: resources.stream().filter(Objects::nonNull).collect(Collectors.toList());

// Delegate to implementation
return r4SubmitDataProcessorFactory.create(requestDetails).submitData(report, safeResources);
}

private static boolean hasProfile(MeasureReport report) {
return report.hasMeta()
&& report.getMeta().hasProfile()
&& report.getMeta().getProfile().stream()
.map(CanonicalType::asStringValue)
.anyMatch(SubmitDataProvider.RA_DATAEX_MR_PROFILE::equals);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package org.opencds.cqf.fhir.cr.hapi.r4.ra;

import ca.uhn.fhir.context.FhirContext;
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 ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.BundleUtil;
import java.util.Objects;
import java.util.stream.Collectors;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CanonicalType;
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.Resource;
import org.opencds.cqf.fhir.cr.hapi.r4.ISubmitDataProcessorFactory;

public class SubmitRemarkDataProvider {

// Operation canonical name and RA MeasureReport profile canonical
private static final String OPERATION_NAME = "$submit-remark-data";
private static final String RA_MR_REMARK_BUNDLE_PROFILE =
"http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-measurereport-remark-bundle";
private static final String RA_MR_REMARK_PROFILE =
"http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-measurereport-with-remark";

private final ISubmitDataProcessorFactory r4SubmitDataProcessorFactory;

public SubmitRemarkDataProvider(ISubmitDataProcessorFactory r4SubmitDataProcessorFactory) {
this.r4SubmitDataProcessorFactory =
Objects.requireNonNull(r4SubmitDataProcessorFactory, "r4SubmitDataProcessorFactory is required");
}

/**
* A Condition Category Remark may reference resources such as Practitioner and Condition, using
* Patch to submit the remark may not be feasible. This operation is used to submit a Risk
* Adjustment Coding Gap Report with one or more ccRemarks on at least one of the Condition
* Categories, along with the relevant resources referenced by the ccRemark(s).
* URL: [base]/Measure/$submit-remark-data
* URL: [base]/Measure/[id]/$submit-remark-data
*
* @param requestDetails Autopopulated by HAPI.
* @param id The ID of the Measure to submit remark data
* @param bundle A Bundle that contains a Risk Adjustment Coding Gap Report with
* ccRemark(s) on at least one of the Condition Categories and the
* ccRemark referenced resources
* @return A transaction response {@link Bundle}.
*/
@Description(
shortDefinition = "$submit-remark-data",
value =
"Implements the <a href=\"https://build.fhir.org/ig/HL7/davinci-ra/OperationDefinition-submit-remark-data.html\">$submit-remark-data</a> operation.")
@Operation(name = OPERATION_NAME, type = Measure.class)
public Bundle submitRemarkData(
RequestDetails requestDetails,
@IdParam IdType id,
@OperationParam(name = "bundle", min = 1) Bundle bundle) {

// Validate required input
validateResource(bundle, "bundle", "Bundle", RA_MR_REMARK_BUNDLE_PROFILE);

// Extract MR from Bundle (should be the first entry in the Bundle) and validate
var firstEntry = bundle.getEntryFirstRep().getResource();
validateResource(firstEntry, null, "MeasureReport", RA_MR_REMARK_PROFILE);

// Extract MeasureReport from Bundle and remark resources
var report = (MeasureReport) firstEntry;
var remarkResources = BundleUtil.toListOfResources(FhirContext.forR4Cached(), bundle).stream()
.filter(resource -> !(resource instanceof MeasureReport))
.toList();

// TODO: Deduplicate remarkResources?

// Delegate to implementation
return r4SubmitDataProcessorFactory.create(requestDetails).submitData(report, remarkResources);
}

private static boolean hasProfile(Resource resource, String profile) {
return resource.hasMeta()
&& resource.getMeta().hasProfile()
&& resource.getMeta().getProfile().stream()
.map(CanonicalType::asStringValue)
.anyMatch(profile::equals);
}

private static void validateResource(Resource resource, String parameterName, String resourceName, String profile) {
if (resource == null) {
throw new InvalidRequestException(
parameterName != null
? String.format("Parameter '%s' must not be null.", parameterName)
: String.format("Resource '%s' must not be null.", resourceName));
}
validateProfile(resource, profile);
}

private static void validateProfile(Resource resource, String profile) {
if (!hasProfile(resource, profile)) {
String profiles = resource.hasMeta() && resource.getMeta().hasProfile()
? resource.getMeta().getProfile().stream()
.map(CanonicalType::asStringValue)
.collect(Collectors.joining(", "))
: "(none)";
throw new InvalidRequestException(String.format(
"The '%s' MUST conform to profile: %s. Provided profiles: %s.",
resource.fhirType(), profile, profiles));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.opencds.cqf.fhir.cr.hapi.config.r4.ExtractOperationConfig;
import org.opencds.cqf.fhir.cr.hapi.config.r4.PackageOperationConfig;
import org.opencds.cqf.fhir.cr.hapi.config.r4.PopulateOperationConfig;
import org.opencds.cqf.fhir.cr.hapi.config.r4.RiskAdjustmentOperationConfig;
import org.opencds.cqf.fhir.cr.hapi.config.test.TestCrStorageSettingsConfigurer;
import org.opencds.cqf.fhir.cr.hapi.config.test.r4.TestCrR4Config;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -50,7 +51,8 @@
EvaluateOperationConfig.class,
ExtractOperationConfig.class,
PackageOperationConfig.class,
PopulateOperationConfig.class
PopulateOperationConfig.class,
RiskAdjustmentOperationConfig.class
})
public abstract class BaseCrR4TestServer extends BaseJpaR4Test implements IResourceLoader {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.opencds.cqf.fhir.cr.hapi.r4;

import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Resource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class RiskAdjustmentOperationProviderIT extends BaseCrR4TestServer {

@Test
void testRiskAdjustmentSubmitDataOperationInvalidRequest() {
try {
ourClient
.operation()
.onType("Measure")
.named("ra-submit-data")
.withNoParameters(Parameters.class)
.execute();
Assertions.fail();
} catch (InvalidRequestException ire) {
// Passes
}
}

@Test
void testRiskAdjustmentSubmitRemarkDataOperationInvalidRequest() {
try {
ourClient
.operation()
.onType("Measure")
.named("submit-remark-data")
.withNoParameters(Parameters.class)
.execute();
Assertions.fail();
} catch (InvalidRequestException ire) {
// Passes
}
}

@Test
void testRiskAdjustmentSubmitDataOperationInvalidProfile() {
var mr = (MeasureReport) readResource("ra-datax-measurereport01.json");
mr.getMeta().setProfile(null);
var params = org.opencds.cqf.fhir.utility.r4.Parameters.parameters(
org.opencds.cqf.fhir.utility.r4.Parameters.part("measureReport", mr));
try {
ourClient
.operation()
.onType("Measure")
.named("ra-submit-data")
.withParameters(params)
.execute();
Assertions.fail();
} catch (InvalidRequestException ire) {
// Passes
}
}

@Test
void testRiskAdjustmentSubmitRemarkDataOperationInvalidProfile() {
var mr = (MeasureReport) readResource("ra-datax-measurereport01.json");
var params = org.opencds.cqf.fhir.utility.r4.Parameters.parameters(
org.opencds.cqf.fhir.utility.r4.Parameters.part("measureReport", mr));
try {
ourClient
.operation()
.onType("Measure")
.named("submit-remark-data")
.withParameters(params)
.execute();
Assertions.fail();
} catch (InvalidRequestException ire) {
// Passes
}
}

@Test
void testRiskAdjustmentSubmitDataOperationValidRequest() {
var mr = readResource("ra-datax-measurereport01.json");
var params = org.opencds.cqf.fhir.utility.r4.Parameters.parameters(
org.opencds.cqf.fhir.utility.r4.Parameters.part("measureReport", (Resource) mr));
var result = ourClient
.operation()
.onType("Measure")
.named("ra-submit-data")
.withParameters(params)
.execute();
Assertions.assertNotNull(result);
}
}
Loading