diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/RiskAdjustmentOperationConfig.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/RiskAdjustmentOperationConfig.java
new file mode 100644
index 000000000..998c943f4
--- /dev/null
+++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/RiskAdjustmentOperationConfig.java
@@ -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);
+ }
+}
diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/ra/SubmitDataProvider.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/ra/SubmitDataProvider.java
new file mode 100644
index 000000000..651b38a79
--- /dev/null
+++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/ra/SubmitDataProvider.java
@@ -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
+ * RA Data Exchange MeasureReport profile
+ *
+ * @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 $ra-submit-data 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 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 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);
+ }
+}
diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/ra/SubmitRemarkDataProvider.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/ra/SubmitRemarkDataProvider.java
new file mode 100644
index 000000000..67269a485
--- /dev/null
+++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/ra/SubmitRemarkDataProvider.java
@@ -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 $submit-remark-data 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));
+ }
+ }
+}
diff --git a/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/r4/BaseCrR4TestServer.java b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/r4/BaseCrR4TestServer.java
index a19422955..440e00c26 100644
--- a/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/r4/BaseCrR4TestServer.java
+++ b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/r4/BaseCrR4TestServer.java
@@ -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;
@@ -50,7 +51,8 @@
EvaluateOperationConfig.class,
ExtractOperationConfig.class,
PackageOperationConfig.class,
- PopulateOperationConfig.class
+ PopulateOperationConfig.class,
+ RiskAdjustmentOperationConfig.class
})
public abstract class BaseCrR4TestServer extends BaseJpaR4Test implements IResourceLoader {
diff --git a/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/r4/RiskAdjustmentOperationProviderIT.java b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/r4/RiskAdjustmentOperationProviderIT.java
new file mode 100644
index 000000000..562a7bfe2
--- /dev/null
+++ b/cqf-fhir-cr-hapi/src/test/java/org/opencds/cqf/fhir/cr/hapi/r4/RiskAdjustmentOperationProviderIT.java
@@ -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);
+ }
+}
diff --git a/cqf-fhir-cr-hapi/src/test/resources/ra-datax-measurereport01.json b/cqf-fhir-cr-hapi/src/test/resources/ra-datax-measurereport01.json
new file mode 100644
index 000000000..afa165638
--- /dev/null
+++ b/cqf-fhir-cr-hapi/src/test/resources/ra-datax-measurereport01.json
@@ -0,0 +1,61 @@
+{
+ "resourceType" : "MeasureReport",
+ "id" : "ra-datax-measurereport01",
+ "meta" : {
+ "extension" : [
+ {
+ "url" : "http://hl7.org/fhir/StructureDefinition/instance-name",
+ "valueString" : "Data Exchange MeasureReport Example01"
+ },
+ {
+ "url" : "http://hl7.org/fhir/StructureDefinition/instance-description",
+ "valueMarkdown" : "This is an example for the Risk Adjustment Data Exchange MeasureReport profile. It evaluatedResource references an example C-CDA document that is being submitted."
+ }
+ ],
+ "profile" : [
+ "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-datax-measurereport"
+ ]
+ },
+ "extension" : [
+ {
+ "url" : "http://hl7.org/fhir/StructureDefinition/measurereport-category",
+ "valueCodeableConcept" : {
+ "coding" : [
+ {
+ "system" : "http://hl7.org/fhir/CodeSystem/measurereport-category",
+ "code" : "ra"
+ }
+ ]
+ }
+ },
+ {
+ "url" : "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-payerCodingGapReportId",
+ "valueId" : "ra-measurereport01"
+ },
+ {
+ "url" : "http://hl7.org/fhir/us/davinci-ra/StructureDefinition/ra-reportingVendor",
+ "valueReference" : {
+ "reference" : "Organization/ra-vendor01"
+ }
+ }
+ ],
+ "status" : "complete",
+ "type" : "data-collection",
+ "measure" : "http://hl7.org/fhir/us/davinci-ra/Measure/RAModelExample01",
+ "subject" : {
+ "reference" : "Patient/ra-patient01"
+ },
+ "date" : "2021-11-10",
+ "reporter" : {
+ "reference" : "Organization/ra-org02pat02"
+ },
+ "period" : {
+ "start" : "2021-01-01",
+ "end" : "2021-09-30"
+ },
+ "evaluatedResource" : [
+ {
+ "reference" : "DocumentReference/ra-documentreference01pat01"
+ }
+ ]
+}
\ No newline at end of file