diff --git a/dockerRunner.sh b/dockerRunner.sh deleted file mode 100755 index fc2adb2ac..000000000 --- a/dockerRunner.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# Handle closing application on signal interrupt (ctrl + c) -trap 'kill $CONTINUOUS_BUILD_PID $SERVER_PID; gradle --stop; exit' INT - -# Reset log file content for new application boot -echo "*** Logs for 'gradle installBootDist --continuous' ***" > builder.log -echo "*** Logs for 'gradle bootRun' ***" > runner.log - -# Print that the application is starting in watch mode -echo "starting application in watch mode..." - -# Start the continious build listener process -echo "starting continuous build listener..." -gradle build --continuous 2>&1 | tee builder.log & CONTINUOUS_BUILD_PID=$! - -# Start server process once initial build finishes -( while ! grep -m1 'BUILD SUCCESSFUL' < builder.log; do - sleep 15 -done -echo "starting crd server..." -gradle bootRun 2>&1 | tee runner.log ) & SERVER_PID=$! - -# Handle application background process exiting -wait $CONTINUOUS_BUILD_PID $SERVER_PID -EXIT_CODE=$? -echo "application exited with exit code $EXIT_CODE..." \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4b442974..ffed3a254 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/operations/build.gradle b/operations/build.gradle index 7f536fdf0..600a57e9e 100644 --- a/operations/build.gradle +++ b/operations/build.gradle @@ -1,7 +1,7 @@ -apply plugin: 'maven' +apply plugin: 'maven-publish' dependencies { - compile project(':resources') - compile 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' - compile 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' + implementation project(':resources') + implementation 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' } diff --git a/resources/build.gradle b/resources/build.gradle index c43254ba9..692387e0c 100644 --- a/resources/build.gradle +++ b/resources/build.gradle @@ -1,22 +1,22 @@ -apply plugin: 'maven' +apply plugin: 'maven-publish' dependencies { - compile group: 'com.helger', name: 'ph-schematron', version: '4.1.0' - compile 'com.googlecode.json-simple:json-simple:1.1.1' + implementation group: 'com.helger', name: 'ph-schematron', version: '4.1.0' + implementation 'com.googlecode.json-simple:json-simple:1.1.1' - compile 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' - compile 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' - compile group: 'ca.uhn.hapi.fhir', name: 'hapi-fhir-validation-resources-r4', version: '5.3.0' - compile 'ca.uhn.hapi.fhir:hapi-fhir-validation:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' + implementation group: 'ca.uhn.hapi.fhir', name: 'hapi-fhir-validation-resources-r4', version: '5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation:5.3.0' - compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.12.1' - compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.12.1' - compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.12.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.12.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.12.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.12.1' - compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final' - compile group: 'org.springframework', name: 'spring-context', version: '5.0.8.RELEASE' + implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final' + implementation group: 'org.springframework', name: 'spring-context', version: '5.0.8.RELEASE' - compile('ca.uhn.hapi.fhir:hapi-fhir-jpaserver-base:3.6.0'){ + implementation('ca.uhn.hapi.fhir:hapi-fhir-jpaserver-base:3.6.0'){ exclude group: 'org.thymeleaf' } } diff --git a/resources/src/main/java/org/cdshooks/Action.java b/resources/src/main/java/org/cdshooks/Action.java index 84cf4b44e..edac2a4ef 100755 --- a/resources/src/main/java/org/cdshooks/Action.java +++ b/resources/src/main/java/org/cdshooks/Action.java @@ -15,6 +15,7 @@ public Action(FhirComponentsT fhirComponents) { private TypeEnum type = null; private String description = null; private IBaseResource resource = null; + private String resourceId = null; private FhirComponentsT fhirComponents; @@ -26,9 +27,7 @@ public void setType(TypeEnum type) { this.type = type; } - public String getDescription() { - return description; - } + public String getDescription() { return description; } public void setDescription(String description) { this.description = description; @@ -36,7 +35,14 @@ public void setDescription(String description) { public IBaseResource getResource() { return resource; } - public void setResource(IBaseResource resource) { this.resource = resource; } + public void setResource(IBaseResource resource) { + this.resource = resource; + setResourceId(resource.fhirType() + "/" + resource.getIdElement().getIdPart()); + } + + public String getResourceId() { return resourceId; } + + public void setResourceId(String resourceId) { this.resourceId = resourceId; } public FhirComponentsT getFhirComponents() { return this.fhirComponents; diff --git a/resources/src/main/java/org/cdshooks/ActionSerializer.java b/resources/src/main/java/org/cdshooks/ActionSerializer.java index c22dc6b03..4b2a4448d 100644 --- a/resources/src/main/java/org/cdshooks/ActionSerializer.java +++ b/resources/src/main/java/org/cdshooks/ActionSerializer.java @@ -38,6 +38,8 @@ public void serialize( new ObjectMapper().readValue(jsonStrNew, HashMap.class); jgen.writeObjectField("resource", map); + jgen.writeStringField("resourceId", value.getResourceId()); + jgen.writeEndObject(); } } diff --git a/resources/src/main/java/org/cdshooks/Card.java b/resources/src/main/java/org/cdshooks/Card.java index 73d8d2358..a4b125703 100755 --- a/resources/src/main/java/org/cdshooks/Card.java +++ b/resources/src/main/java/org/cdshooks/Card.java @@ -5,16 +5,24 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class Card { + private String uuid = null; private String summary = null; - private String detail = null; private IndicatorEnum indicator = null; private Source source = null; private List suggestions = null; + private SelectionBehaviorEnum selectionBehavior = null; private List links = null; + public Card() { this.uuid = UUID.randomUUID().toString(); } + + public String getUuid() { return uuid; } + + private void setUuid(String uuid) { this.uuid = uuid; } + public String getSummary() { return summary; } @@ -68,6 +76,10 @@ public void setSuggestions(List suggestions) { this.suggestions = suggestions; } + public SelectionBehaviorEnum getSelectionBehavior() { return selectionBehavior; } + + public void setSelectionBehavior(SelectionBehaviorEnum selectionBehavior) { this.selectionBehavior = selectionBehavior; } + /** * Add a link. * @param linksItem The link. @@ -129,4 +141,38 @@ public String toString() { return String.valueOf(value); } } + + public enum SelectionBehaviorEnum { + ANY("any"), + AT_MOST_ONE("at-most-one"); + + private String value; + + SelectionBehaviorEnum(String value) { this.value = value; } + + /** + * Create the enum value from a string. Needed because the values have illegal java chars. + * @param value One of the enum values. + * @return selectionBehaviorEnum + */ + @JsonCreator + public static SelectionBehaviorEnum fromValue(String value) throws IOException { + for (SelectionBehaviorEnum selectionBehaviorEnum : SelectionBehaviorEnum.values()) { + if (selectionBehaviorEnum.toString().equals(value)) { + return selectionBehaviorEnum; + } + } + return null; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } } diff --git a/resources/src/main/java/org/cdshooks/CoverageRequirements.java b/resources/src/main/java/org/cdshooks/CoverageRequirements.java index 28be93d9b..cea3e3908 100644 --- a/resources/src/main/java/org/cdshooks/CoverageRequirements.java +++ b/resources/src/main/java/org/cdshooks/CoverageRequirements.java @@ -1,5 +1,7 @@ package org.cdshooks; +import java.util.UUID; + public class CoverageRequirements { private boolean applies; private String summary; @@ -12,10 +14,14 @@ public class CoverageRequirements { private String questionnairePARequestUri; private String questionnairePlanOfCareUri; private String questionnaireDispenseUri; + private String questionnaireAdditionalUri; private String requestId; private boolean priorAuthRequired; private boolean documentationRequired; + private boolean priorAuthApproved; + private String priorAuthId; + public boolean getApplies() { return applies; } public CoverageRequirements setApplies(boolean applies) { @@ -137,4 +143,32 @@ public CoverageRequirements setDocumentationRequired(boolean documentationRequir this.documentationRequired = documentationRequired; return this; } + + public boolean isPriorAuthApproved() { return priorAuthApproved; } + + public CoverageRequirements setPriorAuthApproved(boolean priorAuthApproved) { + this.priorAuthApproved = priorAuthApproved; + return this; + } + + public String getPriorAuthId() { return priorAuthId; } + + public CoverageRequirements setPriorAuthId(String priorAuthId) { + this.priorAuthId = priorAuthId; + return this; + } + + public CoverageRequirements generatePriorAuthId() { + // Generate a random UUID as the ID. This is the same method that Prior Auth (PAS) uses. + this.priorAuthId = UUID.randomUUID().toString(); + return this; + } + + public String getQuestionnaireAdditionalUri() { + return questionnaireAdditionalUri; + } + + public void setQuestionnaireAdditionalUri(String questionnaireAdditionalUri) { + this.questionnaireAdditionalUri = questionnaireAdditionalUri; + } } diff --git a/resources/src/main/java/org/cdshooks/Suggestion.java b/resources/src/main/java/org/cdshooks/Suggestion.java index e1c88aca1..0eb4e6d87 100755 --- a/resources/src/main/java/org/cdshooks/Suggestion.java +++ b/resources/src/main/java/org/cdshooks/Suggestion.java @@ -7,11 +7,13 @@ public class Suggestion { private String label = null; - private UUID uuid = null; + private String uuid = null; private List actions = null; - private boolean isRecommended = false; + private boolean isRecommended = true; + + public Suggestion() { this.uuid = UUID.randomUUID().toString(); } public String getLabel() { return label; @@ -21,11 +23,9 @@ public void setLabel(String label) { this.label = label; } - public UUID getUuid() { - return uuid; - } + public String getUuid() { return uuid; } - public void setUuid(UUID uuid) { + public void setUuid(String uuid) { this.uuid = uuid; } diff --git a/resources/src/main/java/org/hl7/davinci/r4/Utilities.java b/resources/src/main/java/org/hl7/davinci/r4/Utilities.java index 6cfd033b4..e1821bc71 100644 --- a/resources/src/main/java/org/hl7/davinci/r4/Utilities.java +++ b/resources/src/main/java/org/hl7/davinci/r4/Utilities.java @@ -1,35 +1,16 @@ package org.hl7.davinci.r4; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; -import org.hl7.davinci.FhirResourceInfo; -import org.hl7.davinci.PatientInfo; -import org.hl7.davinci.PractitionerRoleInfo; -import org.hl7.davinci.RequestIncompleteException; -import org.hl7.davinci.SharedUtilities; -import org.hl7.davinci.SuppressParserErrorHandler; +import org.hl7.davinci.*; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Address; +import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Address.AddressType; import org.hl7.fhir.r4.model.Address.AddressUse; -import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.Coverage; -import org.hl7.fhir.r4.model.DomainResource; -import org.hl7.fhir.r4.model.Library; -import org.hl7.fhir.r4.model.Location; -import org.hl7.fhir.r4.model.Organization; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.PractitionerRole; -import org.hl7.fhir.r4.model.Questionnaire; -import org.hl7.fhir.r4.model.QuestionnaireResponse; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.Resource; -import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.r4.model.ClaimResponse.AdjudicationComponent; public class Utilities { /** @@ -248,4 +229,99 @@ public static FhirResourceInfo getFhirResourceInfo(String resourceString) { return getFhirResourceInfo(parseFhirData(resourceString)); } + public static Reference convertIdToReference(String id, String fhirType) { + Reference reference = new Reference(); + if (id.toUpperCase().startsWith(fhirType.toUpperCase() + "/")) { + reference.setReference(id); + } else { + reference.setReference(fhirType + "/" + id); + } + return reference; + } + + public static ClaimResponse createClaimResponse(String priorAuthId, String patientId, String payerId, String providerId, String applicationFhirPath) { + Date now = new Date(); + + ClaimResponse claimResponse = new ClaimResponse(); + + claimResponse.setId(priorAuthId); + + Meta meta = new Meta(); + meta.addProfile("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-claimresponse"); + claimResponse.setMeta(meta); + + claimResponse.addIdentifier(new Identifier().setSystem(applicationFhirPath).setValue(claimResponse.getId())); + + claimResponse.setStatus(ClaimResponse.ClaimResponseStatus.ACTIVE); + + CodeableConcept codeableConcept = new CodeableConcept(); + Coding coding = new Coding(); + coding.setSystem("http://terminology.hl7.org/CodeSystem/claim-type"); + coding.setCode("professional"); + coding.setDisplay("Professional"); + codeableConcept.addCoding(coding); + claimResponse.setType(codeableConcept); + + claimResponse.setUse(ClaimResponse.Use.PREAUTHORIZATION); + + claimResponse.setPatient(convertIdToReference(patientId, "Patient")); + + claimResponse.setCreated(now); + + claimResponse.setInsurer(convertIdToReference(payerId, "Organization")); + + claimResponse.setOutcome(ClaimResponse.RemittanceOutcome.COMPLETE); + + claimResponse.setDisposition("Granted"); + + claimResponse.setPreAuthRef(claimResponse.getId()); + + ClaimResponse.ItemComponent itemComponent = new ClaimResponse.ItemComponent(); + + Extension reviewActionExtension = new Extension(); + reviewActionExtension.setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-reviewAction"); + reviewActionExtension.addExtension( + new Extension() + .setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-reviewActionCode") + .setValue(new CodeableConcept().addCoding( + new Coding() + .setSystem("https://valueset.x12.org/x217/005010/response/2000F/HCR/1/01/00/306") + .setCode("A1")))); + reviewActionExtension.addExtension( + new Extension() + .setUrl("number") + .setValue(new StringType(UUID.randomUUID().toString()))); + itemComponent.addExtension(reviewActionExtension); + + itemComponent.addExtension( + new Extension() + .setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-itemPreAuthIssueDate") + .setValue(new DateType().setValue(now))); + + itemComponent.addExtension( + new Extension() + .setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-authorizationNumber") + .setValue(new StringType(UUID.randomUUID().toString()))); + + Extension providerExtension = new Extension(); + providerExtension.setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-itemAuthorizedProvider"); + providerExtension.addExtension( + new Extension() + .setUrl("provider") + .setValue(new Reference().setReference(convertIdToReference(providerId, "Practitioner").getReference()))); + itemComponent.addExtension(providerExtension); + + itemComponent.setItemSequence(1); + + itemComponent.addAdjudication( + new AdjudicationComponent().setCategory( + new CodeableConcept().addCoding( + new Coding().setSystem("http://terminology.hl7.org/CodeSystem/adjudication") + .setCode("submitted")))); + + claimResponse.addItem(itemComponent); + + return claimResponse; + } + } diff --git a/server/build.gradle b/server/build.gradle index 33362bfad..cf9783546 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -5,7 +5,6 @@ buildscript { repositories { mavenCentral() maven { url 'https://repo.spring.io/snapshot' } - maven { url 'http://repo.jenkins-ci.org/releases/' } } dependencies { @@ -13,7 +12,7 @@ buildscript { } } -apply plugin: 'maven' +apply plugin: 'maven-publish' //apply plugin: 'war' apply plugin: 'java' apply plugin: 'org.springframework.boot' @@ -28,48 +27,50 @@ processResources { dependencies { developmentOnly("org.springframework.boot:spring-boot-devtools") - compile project(':resources') - compile project(':operations') + implementation project(':resources') + implementation project(':operations') + implementation 'com.googlecode.json-simple:json-simple:1.1.1' + implementation 'com.google.code.gson:gson:2.8.8' - compile('org.springframework.boot:spring-boot-starter-actuator') - compile('org.springframework.boot:spring-boot-starter-data-jpa') - compile('org.springframework.boot:spring-boot-starter-data-rest') - compile('org.springframework.boot:spring-boot-starter-thymeleaf') - compile('org.springframework.boot:spring-boot-starter-security') + implementation('org.springframework.boot:spring-boot-starter-actuator') + implementation('org.springframework.boot:spring-boot-starter-data-jpa') + implementation('org.springframework.boot:spring-boot-starter-data-rest') + implementation('org.springframework.boot:spring-boot-starter-thymeleaf') + implementation('org.springframework.boot:spring-boot-starter-security') + implementation('org.springframework.boot:spring-boot-starter-validation') - compile('com.h2database:h2') + implementation('com.h2database:h2') - compile("io.jsonwebtoken:jjwt:0.7.0") + implementation("io.jsonwebtoken:jjwt:0.7.0") -// compile 'javax.servlet:javax.servlet-api:3.1.0' - compile 'commons-beanutils:commons-beanutils:1.9.3' + implementation 'commons-beanutils:commons-beanutils:1.9.3' - testCompile('org.springframework.boot:spring-boot-starter-test') - testCompile "com.github.tomakehurst:wiremock-standalone:2.18.0" - testCompile('org.springframework.boot:spring-boot-starter-test') + testImplementation('org.springframework.boot:spring-boot-starter-test') + testImplementation "com.github.tomakehurst:wiremock-standalone:2.18.0" + testImplementation('org.springframework.boot:spring-boot-starter-test') - compile 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' - compile 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' - compile 'com.jayway.jsonpath:json-path:2.4.0' - compile 'joda-time:joda-time:2.10.5' + implementation 'com.jayway.jsonpath:json-path:2.4.0' + implementation 'joda-time:joda-time:2.10.5' //cql stuff - compile (group: 'org.opencds.cqf.cql', name: 'engine', version: '1.5.1') { + implementation (group: 'org.opencds.cqf.cql', name: 'engine', version: '1.5.1') { exclude group: 'org.slf4j', module: 'slf4j-log4j12' } - compile (group: 'org.opencds.cqf.cql', name: 'engine.fhir', version: '1.5.1') { + implementation (group: 'org.opencds.cqf.cql', name: 'engine.fhir', version: '1.5.1') { exclude group: 'org.slf4j', module: 'slf4j-log4j12' } //Use locally compiled cql libs (engine and fhir) - compile fileTree(dir: 'libs', include: ['*.jar']) - compile group: 'info.cqframework', name: 'cql-to-elm', version: '1.5.1' + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation group: 'info.cqframework', name: 'cql-to-elm', version: '1.5.1' - compile 'org.zeroturnaround:zt-zip:1.13' + implementation 'org.zeroturnaround:zt-zip:1.13' - compile group: 'org.kohsuke', name:'github-api', version:'1.77' + implementation group: 'org.kohsuke', name:'github-api', version:'1.77' } task buildReact(type:Exec) { @@ -142,7 +143,7 @@ void cloneCdsLibraryScript(branch) { println "GIT: clone CDS-Library branch " + branch exec { workingDir './' - commandLine 'git', 'clone', 'https://github.com/mcode/CDS-Library.git', 'CDS-Library' + commandLine 'git', 'clone', 'https://github.com/HL7-DaVinci/CDS-Library.git', 'CDS-Library' } checkoutCdsLibraryScript(branch) } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/Utils.java b/server/src/main/java/org/hl7/davinci/endpoint/Utils.java index c85e2f452..4d407e93c 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/Utils.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/Utils.java @@ -2,7 +2,6 @@ import java.net.MalformedURLException; import java.net.URL; -import java.util.List; import javax.servlet.http.HttpServletRequest; import com.google.common.net.HttpHeaders; @@ -39,27 +38,4 @@ public static URL getApplicationBaseUrl(HttpServletRequest request) { } } - public static String stripResourceType(String identifier) { - int indexOfDivider = identifier.indexOf('/'); - if (indexOfDivider+1 == identifier.length()) { - // remove the trailing '/' - return identifier.substring(0, indexOfDivider); - } else { - return identifier.substring(indexOfDivider+1); - } - } - - public static boolean idInSelectionsList(String identifier, List selections) { - if (selections.isEmpty()) { - return true; - } else { - for ( String selection : selections) { - if (identifier.contains(stripResourceType(selection))) { - return true; - } - } - return false; - } - } - } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsAbstract.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsAbstract.java deleted file mode 100644 index 750e37322..000000000 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsAbstract.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.hl7.davinci.endpoint.cdshooks.services.crd; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.cdshooks.*; -import org.hl7.davinci.FhirComponentsT; -import org.hl7.davinci.PrefetchTemplateElement; -import org.hl7.davinci.endpoint.config.YamlConfig; -import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleCriteria; -import org.hl7.davinci.r4.crdhook.DiscoveryExtension; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.List; - -@Component -public abstract class CdsAbstract>{ - static final Logger logger = LoggerFactory.getLogger(CdsAbstract.class); - - /** - * The {id} portion of the URL to this service which is available at - * {baseUrl}/cds-services/{id}. REQUIRED - */ - public String id; - - /** - * The hook this service should be invoked on. REQUIRED - */ - public Hook hook; - - /** - * The human-friendly name of this service. RECOMMENDED - */ - public String title; - - /** - * The description of this service. REQUIRED - */ - public String description; - - /** - * An object containing key/value pairs of FHIR queries that this service is - * requesting that the EHR prefetch and provide on each service call. The key is - * a string that describes the type of data being requested and the value is a - * string representing the FHIR query. OPTIONAL - */ - public Prefetch prefetch; - - private final List prefetchElements; - - protected FhirComponentsT fhirComponents; - - private final DiscoveryExtension extension; - - @Autowired - @JsonIgnore - public YamlConfig myConfig; - - public CdsAbstract(String id, Hook hook, String title, String description, - List prefetchElements, FhirComponentsT fhirComponents, - DiscoveryExtension extension) { - - if (id == null) { - throw new NullPointerException("CDSService id cannot be null"); - } - if (hook == null) { - throw new NullPointerException("CDSService hook cannot be null"); - } - if (description == null) { - throw new NullPointerException("CDSService description cannot be null"); - } - this.id = id; - this.hook = hook; - this.title = title; - this.description = description; - this.prefetchElements = prefetchElements; - prefetch = new Prefetch(); - for (PrefetchTemplateElement prefetchElement : prefetchElements) { - this.prefetch.put(prefetchElement.getKey(), prefetchElement.getQuery()); - } - this.fhirComponents = fhirComponents; - this.extension = extension; - } - - public DiscoveryExtension getExtension() { return extension; } - - public List getPrefetchElements() { - return prefetchElements; - } - - protected Link smartLinkBuilder(String patientId, String fhirBase, URL applicationBaseUrl, String questionnaireUri, - String reqResourceId, CoverageRequirementRuleCriteria criteria, boolean priorAuthRequired, String label) { - URI configLaunchUri = myConfig.getLaunchUrl(); - questionnaireUri = applicationBaseUrl + "/fhir/r4/" + questionnaireUri; - - String launchUrl; - if (myConfig.getLaunchUrl().isAbsolute()) { - launchUrl = myConfig.getLaunchUrl().toString(); - } else { - try { - launchUrl = new URL(applicationBaseUrl.getProtocol(), applicationBaseUrl.getHost(), - applicationBaseUrl.getPort(), applicationBaseUrl.getFile() + configLaunchUri.toString(), null).toString(); - } catch (MalformedURLException e) { - String msg = "Error creating smart launch URL"; - logger.error(msg); - throw new RuntimeException(msg); - } - } - - if (fhirBase != null && fhirBase.endsWith("/")) { - fhirBase = fhirBase.substring(0, fhirBase.length() - 1); - } - if (patientId != null && patientId.startsWith("Patient/")) { - patientId = patientId.substring(8); - } - - // PARAMS: - // template is the uri of the questionnaire - // request is the ID of the device request or medrec (not the full URI like the - // IG says, since it should be taken from fhirBase - - String filepath = "../../getfile/" + criteria.getQueryString(); - - String appContext = "template=" + questionnaireUri + "&request=" + reqResourceId; - appContext = appContext + "&fhirpath=" + applicationBaseUrl + "/fhir/"; - - appContext = appContext + "&priorauth=" + (priorAuthRequired ? "true" : "false"); - appContext = appContext + "&filepath=" + applicationBaseUrl + "/"; - if (myConfig.getUrlEncodeAppContext()) { - logger.info("CdsService::smartLinkBuilder: URL encoding appcontext"); - appContext = URLEncoder.encode(appContext, StandardCharsets.UTF_8).toString(); - } - - logger.info("smarLinkBuilder: appContext: " + appContext); - - if (myConfig.isAppendParamsToSmartLaunchUrl()) { - launchUrl = launchUrl + "?iss=" + fhirBase + "&patientId=" + patientId + "&template=" + questionnaireUri - + "&request=" + reqResourceId; - } else { - // TODO: The iss should be set by the EHR? - launchUrl = launchUrl; - } - - Link link = new Link(); - link.setType("smart"); - link.setLabel(label); - link.setUrl(launchUrl); - - link.setAppContext(appContext); - - return link; - } - - public abstract CdsResponse handleRequest(requestTypeT request, URL applicationBaseUrl); -} diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java index a2aec34ea..b5664cac2 100755 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java @@ -1,19 +1,33 @@ package org.hl7.davinci.endpoint.cdshooks.services.crd; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.ArrayList; +import java.util.Date; +import javax.validation.Valid; + import org.apache.commons.lang.StringUtils; import org.cdshooks.*; import org.hl7.davinci.FhirComponentsT; import org.hl7.davinci.PrefetchTemplateElement; import org.hl7.davinci.RequestIncompleteException; +import org.hl7.davinci.endpoint.config.YamlConfig; import org.hl7.davinci.endpoint.components.CardBuilder; import org.hl7.davinci.endpoint.components.CardBuilder.CqlResultsForCard; import org.hl7.davinci.endpoint.components.PrefetchHydrator; +import org.hl7.davinci.endpoint.database.FhirResourceRepository; import org.hl7.davinci.endpoint.database.RequestLog; import org.hl7.davinci.endpoint.database.RequestService; import org.hl7.davinci.endpoint.files.FileStore; +import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleCriteria; import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleResult; -import org.hl7.davinci.r4.crdhook.DiscoveryExtension; import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; +import org.hl7.davinci.r4.crdhook.DiscoveryExtension; import org.opencds.cqf.cql.engine.execution.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,26 +35,99 @@ import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestBody; -import javax.validation.Valid; -import java.net.URL; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - @Component -public abstract class CdsService> extends CdsAbstract { +public abstract class CdsService> { static final Logger logger = LoggerFactory.getLogger(CdsService.class); + /** + * The {id} portion of the URL to this service which is available at + * {baseUrl}/cds-services/{id}. REQUIRED + */ + public String id; + + /** + * The hook this service should be invoked on. REQUIRED + */ + public Hook hook; + + /** + * The human-friendly name of this service. RECOMMENDED + */ + public String title; + + /** + * The description of this service. REQUIRED + */ + public String description; + + /** + * An object containing key/value pairs of FHIR queries that this service is + * requesting that the EHR prefetch and provide on each service call. The key is + * a string that describes the type of data being requested and the value is a + * string representing the FHIR query. OPTIONAL + */ + public Prefetch prefetch; + + @Autowired + private YamlConfig myConfig; + @Autowired RequestService requestService; @Autowired FileStore fileStore; + @Autowired + private FhirResourceRepository fhirResourceRepository; + + private final List prefetchElements; + + protected FhirComponentsT fhirComponents; + + private final DiscoveryExtension extension; + + /** + * Create a new cdsservice. + * + * @param id Will be used in the url, should be unique. + * @param hook Which hook can call this. + * @param title Human title. + * @param description Human description. + * @param prefetchElements List of prefetch elements, will be in prefetch + * template. + * @param fhirComponents Fhir components to use + * @param extension Custom CDS Hooks extensions. + */ public CdsService(String id, Hook hook, String title, String description, List prefetchElements, FhirComponentsT fhirComponents, DiscoveryExtension extension) { - super(id, hook, title, description, prefetchElements, fhirComponents, extension); + + if (id == null) { + throw new NullPointerException("CDSService id cannot be null"); + } + if (hook == null) { + throw new NullPointerException("CDSService hook cannot be null"); + } + if (description == null) { + throw new NullPointerException("CDSService description cannot be null"); + } + this.id = id; + this.hook = hook; + this.title = title; + this.description = description; + this.prefetchElements = prefetchElements; + prefetch = new Prefetch(); + for (PrefetchTemplateElement prefetchElement : prefetchElements) { + this.prefetch.put(prefetchElement.getKey(), prefetchElement.getQuery()); + } + this.fhirComponents = fhirComponents; + this.extension = extension; + } + + public DiscoveryExtension getExtension() { return extension; } + + public List getPrefetchElements() { + return prefetchElements; } /** @@ -105,22 +192,30 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a if (results.getCoverageRequirements().getApplies()) { - if (coverageRequirements.isDocumentationRequired() || coverageRequirements.isPriorAuthRequired()) { + // if prior auth already approved + if (coverageRequirements.isPriorAuthApproved()) { + response.addCard(CardBuilder.priorAuthCard(results, results.getRequest(), fhirComponents, coverageRequirements.getPriorAuthId(), + request.getContext().getPatientId(), lookupResult.getCriteria().getPayorId(), request.getContext().getUserId(), + applicationBaseUrl.toString() + "/fhir/" + fhirComponents.getFhirVersion().toString(), + fhirResourceRepository)); + + } else if (coverageRequirements.isDocumentationRequired() || coverageRequirements.isPriorAuthRequired()) { if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireOrderUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireFaceToFaceUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireLabUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireProgressNoteUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePARequestUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePlanOfCareUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri())) { + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireAdditionalUri())) { List smartAppLinks = createQuestionnaireLinks(request, applicationBaseUrl, lookupResult, results); response.addCard(CardBuilder.transform(results, smartAppLinks)); // add a card for an alternative therapy if there is one if (results.getAlternativeTherapy().getApplies() && hookConfiguration.getAlternativeTherapy()) { try { - response.addCard(CardBuilder.alternativeTherapyCard(results.getAlternativeTherapy(), results.getRequest(), - fhirComponents)); + response.addCard(CardBuilder.alternativeTherapyCard(results.getAlternativeTherapy(), + results.getRequest(), fhirComponents)); } catch (RuntimeException e) { logger.warn("Failed to process alternative therapy: " + e.getMessage()); } @@ -133,7 +228,10 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a // no prior auth or documentation required logger.info("Add the no doc or prior auth required card"); Card card = CardBuilder.transform(results); - card = CardBuilder.createSuggestionsWithNote(card, results, fhirComponents); + card.addSuggestionsItem(CardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, + "Save Update To EHR", "Update original " + results.getRequest().fhirType() + " to add note", + true)); + card.setSelectionBehavior(Card.SelectionBehaviorEnum.ANY); response.addCard(card); } } @@ -157,7 +255,7 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a CardBuilder.errorCardIfNonePresent(response); } - // Adding card to requestLog + // Ading card to requestLog requestLog.setCardListFromCards(response.getCards()); requestService.edit(requestLog); @@ -170,41 +268,47 @@ private List createQuestionnaireLinks(requestTypeT request, URL applicatio CoverageRequirements coverageRequirements = results.getCoverageRequirements(); if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireOrderUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireOrderUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Order Form")); + coverageRequirements.getQuestionnaireOrderUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Order Form")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireFaceToFaceUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireFaceToFaceUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Face to Face Encounter Form")); + coverageRequirements.getQuestionnaireFaceToFaceUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Face to Face Encounter Form")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireLabUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireLabUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Lab Form")); + coverageRequirements.getQuestionnaireLabUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Lab Form")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireProgressNoteUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireProgressNoteUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Progress Note")); + coverageRequirements.getQuestionnaireProgressNoteUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Progress Note")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePARequestUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnairePARequestUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "PA Request")); + coverageRequirements.getQuestionnairePARequestUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "PA Request")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePlanOfCareUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnairePlanOfCareUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Plan of Care/Certification")); + coverageRequirements.getQuestionnairePlanOfCareUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Plan of Care/Certification")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireDispenseUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Dispense Form")); + coverageRequirements.getQuestionnaireDispenseUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Dispense Form")); + } + + if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireAdditionalUri())) { + listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, + coverageRequirements.getQuestionnaireAdditionalUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Additional Form")); } return listOfLinks; } @@ -222,7 +326,72 @@ protected Object evaluateStatement(String statement, Context context) { } } + private Link smartLinkBuilder(String patientId, String fhirBase, URL applicationBaseUrl, String questionnaireUri, + String reqResourceId, CoverageRequirementRuleCriteria criteria, boolean priorAuthRequired, String label) { + URI configLaunchUri = myConfig.getLaunchUrl(); + questionnaireUri = applicationBaseUrl + "/fhir/r4/" + questionnaireUri; + + String launchUrl; + if (myConfig.getLaunchUrl().isAbsolute()) { + launchUrl = myConfig.getLaunchUrl().toString(); + } else { + try { + launchUrl = new URL(applicationBaseUrl.getProtocol(), applicationBaseUrl.getHost(), + applicationBaseUrl.getPort(), applicationBaseUrl.getFile() + configLaunchUri.toString(), null).toString(); + } catch (MalformedURLException e) { + String msg = "Error creating smart launch URL"; + logger.error(msg); + throw new RuntimeException(msg); + } + } + + if (fhirBase != null && fhirBase.endsWith("/")) { + fhirBase = fhirBase.substring(0, fhirBase.length() - 1); + } + if (patientId != null && patientId.startsWith("Patient/")) { + patientId = patientId.substring(8); + } + // PARAMS: + // template is the uri of the questionnaire + // request is the ID of the device request or medrec (not the full URI like the + // IG says, since it should be taken from fhirBase + + String filepath = "../../getfile/" + criteria.getQueryString(); + + String appContext = "template=" + questionnaireUri + "&request=" + reqResourceId; + appContext = appContext + "&fhirpath=" + applicationBaseUrl + "/fhir/"; + + appContext = appContext + "&priorauth=" + (priorAuthRequired ? "true" : "false"); + appContext = appContext + "&filepath=" + applicationBaseUrl + "/"; + if (myConfig.getUrlEncodeAppContext()) { + logger.info("CdsService::smartLinkBuilder: URL encoding appcontext"); + try { + appContext = URLEncoder.encode(appContext, StandardCharsets.UTF_8.name()).toString(); + } catch (UnsupportedEncodingException e) { + logger.error("CdsService::smartLinkBuilder: failed to encode URL: " + e.getMessage()); + } + } + + logger.info("smarLinkBuilder: appContext: " + appContext); + + if (myConfig.isAppendParamsToSmartLaunchUrl()) { + launchUrl = launchUrl + "?iss=" + fhirBase + "&patientId=" + patientId + "&template=" + questionnaireUri + + "&request=" + reqResourceId; + } else { + // TODO: The iss should be set by the EHR? + launchUrl = launchUrl; + } + + Link link = new Link(); + link.setType("smart"); + link.setLabel(label); + link.setUrl(launchUrl); + + link.setAppContext(appContext); + + return link; + } // Implement these in child class public abstract List createCqlExecutionContexts(requestTypeT request, diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceInformation.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceInformation.java index ca7ea731c..2e08fbcbc 100755 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceInformation.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceInformation.java @@ -4,14 +4,14 @@ import java.util.List; public class CdsServiceInformation { - private List services = null; + private List services = null; /** * Add a service. * @param servicesItem The service. * @return */ - public CdsServiceInformation addServicesItem(CdsAbstract servicesItem) { + public CdsServiceInformation addServicesItem(CdsService servicesItem) { if (this.services == null) { this.services = new ArrayList<>(); } @@ -19,11 +19,11 @@ public CdsServiceInformation addServicesItem(CdsAbstract servicesItem) { return this; } - public List getServices() { + public List getServices() { return services; } - public void setServices(List services) { + public void setServices(List services) { this.services = services; } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java deleted file mode 100644 index f6e5dbe63..000000000 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.hl7.davinci.endpoint.cdshooks.services.crd; - -import org.cdshooks.Card; -import org.cdshooks.CdsRequest; -import org.cdshooks.CdsResponse; -import org.cdshooks.Hook; -import org.hl7.davinci.FhirComponentsT; -import org.hl7.davinci.PrefetchTemplateElement; -import org.hl7.davinci.endpoint.components.CardBuilder; -import org.hl7.davinci.r4.crdhook.DiscoveryExtension; -import org.hl7.fhir.r4.model.Coding; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.RequestBody; - -import javax.validation.Valid; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -@Component -public abstract class CdsServiceRems> extends CdsAbstract { - static final Logger logger = LoggerFactory.getLogger(CdsServiceRems.class); - - public List remsDrugs = new ArrayList<>(); - - public CdsServiceRems(String id, Hook hook, String title, String description, - List prefetchElements, FhirComponentsT fhirComponents, - DiscoveryExtension extension) { - super(id, hook, title, description, prefetchElements, fhirComponents, extension); - Coding turalio = new Coding() - .setCode("2183126") - .setSystem("http://www.nlm.nih.gov/research/umls/rxnorm") - .setDisplay("Turalio"); - Coding iPledge = new Coding() - .setCode("6064") - .setSystem("http://www.nlm.nih.gov/research/umls/rxnorm") - .setDisplay("Isotretinoin"); - Coding revlimid = new Coding() - .setCode("337535") - .setSystem("http://www.nlm.nih.gov/research/umls/rxnorm") - .setDisplay("Revlimid"); - Coding abstral = new Coding() - .setCode("1053648") - .setSystem("http://www.nlm.nih.gov/research/umls/rxnorm") - .setDisplay("Abstral"); - remsDrugs.add(turalio); - remsDrugs.add(iPledge); - remsDrugs.add(revlimid); - remsDrugs.add(abstral); - } - - /** - * Performs generic operations for incoming requests of any type. - * - * @param request the generically typed incoming request - * @return The response from the server - */ - public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL applicationBaseUrl) { - CdsResponse response = new CdsResponse(); - List medications = getMedications(request); - for (Coding medication : medications) { - Card card = CardBuilder.summaryCard(""); - if (isRemsDrug(medication)) { - card.setSummary(String.format("%s is a REMS drug", medication.getDisplay())); - } else { - card.setSummary(String.format("%s is not a REMS drug", medication.getDisplay())); - } - response.addCard(card); - } - return response; - } - - private boolean isRemsDrug(Coding medication) { - for( Coding remsDrug : remsDrugs){ - if (remsDrug.getCode().equals(medication.getCode())){ - return true; - } - } - return false; - } - - public abstract List getMedications(requestTypeT request); - -} \ No newline at end of file diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java index a5be23692..538169ef9 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java @@ -18,8 +18,6 @@ import java.util.List; import java.util.stream.Collectors; -import static org.hl7.davinci.endpoint.Utils.idInSelectionsList; - public class FhirBundleProcessor { static final Logger logger = LoggerFactory.getLogger(FhirBundleProcessor.class); @@ -50,7 +48,7 @@ public void processDeviceRequests() { logger.info("r4/FhirBundleProcessor::getAndProcessDeviceRequests: DeviceRequest(s) found"); for (DeviceRequest deviceRequest : deviceRequestList) { - if (idInSelectionsList(deviceRequest.getId(), selections)) { + if (idInSelectionsList(deviceRequest.getId())) { List criteriaList = createCriteriaList(deviceRequest.getCodeCodeableConcept(), deviceRequest.getInsurance(), null); buildExecutionContexts(criteriaList, (Patient) deviceRequest.getSubject().getResource(), "device_request", deviceRequest); } @@ -65,7 +63,7 @@ public void processMedicationRequests() { logger.info("r4/FhirBundleProcessor::getAndProcessMedicationRequests: MedicationRequest(s) found"); for (MedicationRequest medicationRequest : medicationRequestList) { - if (idInSelectionsList(medicationRequest.getId(), selections)) { + if (idInSelectionsList(medicationRequest.getId())) { List criteriaList = createCriteriaList(medicationRequest.getMedicationCodeableConcept(), medicationRequest.getInsurance(), null); buildExecutionContexts(criteriaList, (Patient) medicationRequest.getSubject().getResource(), "medication_request", medicationRequest); } @@ -83,7 +81,7 @@ public void processMedicationDispenses() { medicationDispenseBundle); for (MedicationDispense medicationDispense : medicationDispenseList) { - if (idInSelectionsList(medicationDispense.getId(), selections)) { + if (idInSelectionsList(medicationDispense.getId())) { List criteriaList = createCriteriaList(medicationDispense.getMedicationCodeableConcept(), null, payorList); buildExecutionContexts(criteriaList, (Patient) medicationDispense.getSubject().getResource(), "medication_dispense", medicationDispense); } @@ -98,7 +96,7 @@ public void processServiceRequests() { logger.info("r4/FhirBundleProcessor::getAndProcessServiceRequests: ServiceRequest(s) found"); for (ServiceRequest serviceRequest : serviceRequestList) { - if (idInSelectionsList(serviceRequest.getId(), selections)) { + if (idInSelectionsList(serviceRequest.getId())) { List criteriaList = createCriteriaList(serviceRequest.getCode(), serviceRequest.getInsurance(), null); buildExecutionContexts(criteriaList, (Patient) serviceRequest.getSubject().getResource(), "service_request", serviceRequest); } @@ -118,7 +116,7 @@ public void processOrderSelectMedicationStatements() { // process each of the MedicationRequests for (MedicationRequest medicationRequest : medicationRequestList) { - if (idInSelectionsList(medicationRequest.getId(), selections)) { + if (idInSelectionsList(medicationRequest.getId())) { // run on each of the MedicationStatements for (MedicationStatement medicationStatement : medicationStatementList) { @@ -212,4 +210,29 @@ private void buildExecutionContexts(List criter } } + private String stripResourceType(String identifier) { + int indexOfDivider = identifier.indexOf('/'); + if (indexOfDivider+1 == identifier.length()) { + // remove the trailing '/' + return identifier.substring(0, indexOfDivider); + } else { + return identifier.substring(indexOfDivider+1); + } + } + + private boolean idInSelectionsList(String identifier) { + if (this.selections.isEmpty()) { + // if selections list is empty, just assume we should process the request + return true; + } else { + for ( String selection : selections) { + if (identifier.contains(stripResourceType(selection))) { + logger.info("r4/FhirBundleProcessor::idInSelectionsList(" + identifier + "): identifier found in selections list"); + return true; + } + } + return false; + } + } + } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java index 3c92f68b3..912b97d8d 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java @@ -1,6 +1,7 @@ package org.hl7.davinci.endpoint.cdshooks.services.crd.r4; import org.cdshooks.AlternativeTherapy; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.*; import org.slf4j.Logger; @@ -123,4 +124,44 @@ public static IBaseResource addNoteToRequest(IBaseResource request, Annotation n return output; } + + public static IBaseResource addSupportingInfoToRequest(IBaseResource request, Reference reference) { + IBaseResource output = request; + + switch (request.fhirType()) { + case "DeviceRequest": + DeviceRequest deviceRequest = ((DeviceRequest) request).copy(); + deviceRequest.addSupportingInfo(reference); + output = deviceRequest; + break; + case "MedicationRequest": + MedicationRequest medicationRequest = ((MedicationRequest) request).copy(); + medicationRequest.addSupportingInformation(reference); + output = medicationRequest; + break; + case "MedicationDispense": + MedicationDispense medicationDispense = ((MedicationDispense) request).copy(); + medicationDispense.addSupportingInformation(reference); + output = medicationDispense; + break; + case "ServiceRequest": + ServiceRequest serviceRequest = ((ServiceRequest) request).copy(); + serviceRequest.addSupportingInfo(reference); + output = serviceRequest; + break; + case "Appointment": + Appointment appointment = ((Appointment) request).copy(); + appointment.addSupportingInformation(reference); + output = appointment; + break; + case "NutritionOrder": + case "SupplyRequest": + case "Encounter": + default: + logger.info("Unsupported fhir R4 resource type (" + request.fhirType() + ") when adding note"); + throw new RuntimeException("Unsupported fhir R4 resource type " + request.fhirType()); + } + + return output; + } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectServiceRems.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectServiceRems.java deleted file mode 100644 index a5cf2824b..000000000 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectServiceRems.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.hl7.davinci.endpoint.cdshooks.services.crd.r4; - -import org.cdshooks.Hook; -import org.hl7.davinci.PrefetchTemplateElement; -import org.hl7.davinci.endpoint.Utils; -import org.hl7.davinci.endpoint.cdshooks.services.crd.CdsServiceRems; -import org.hl7.davinci.r4.FhirComponents; -import org.hl7.davinci.r4.Utilities; -import org.hl7.davinci.r4.crdhook.CrdPrefetch; -import org.hl7.davinci.r4.crdhook.orderselect.CrdPrefetchTemplateElements; -import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.MedicationRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - - -@Component("r4_OrderSelectServiceRems") -public class OrderSelectServiceRems extends CdsServiceRems { - - public static final String ID = "order-select-rems"; - public static final String TITLE = "order-select-rems Coverage Requirements Discovery"; - public static final Hook HOOK = Hook.ORDER_SELECT; - public static final String DESCRIPTION = - "Get information regarding the coverage requirements for REMS drugs"; - public static final List PREFETCH_ELEMENTS = Arrays.asList( - CrdPrefetchTemplateElements.MEDICATION_STATEMENT_BUNDLE, - CrdPrefetchTemplateElements.MEDICATION_REQUEST_BUNDLE); - public static final FhirComponents FHIRCOMPONENTS = new FhirComponents(); - static final Logger logger = LoggerFactory.getLogger(OrderSelectServiceRems.class); - - public List remsDrugs = new ArrayList<>(); - - public OrderSelectServiceRems() { - super(ID, HOOK, TITLE, DESCRIPTION, PREFETCH_ELEMENTS, FHIRCOMPONENTS, null); - } - - - public List getMedications(OrderSelectRequest request) { - List selections = Arrays.asList(request.getContext().getSelections()); - CrdPrefetch prefetch = request.getPrefetch(); - List medications = getSelections(prefetch, selections); - return medications; - } - public List getSelections(CrdPrefetch prefetch, List selections) { - Bundle medicationRequestBundle = prefetch.getMedicationRequestBundle(); - List medicationRequestList = Utilities.getResourcesOfTypeFromBundle(MedicationRequest.class, medicationRequestBundle); - List codings = new ArrayList<>(); - - if (!medicationRequestList.isEmpty()) { - for (MedicationRequest medicationRequest : medicationRequestList) { - if (Utils.idInSelectionsList(medicationRequest.getId(), selections)) { - codings.add(medicationRequest.getMedicationCodeableConcept().getCodingFirstRep()); // assume there is only 1 coding per MR - } - } - } - - return codings; - } - -} diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java index 69ac808be..8b9c7d2ba 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java @@ -93,6 +93,18 @@ protected CqlResultsForCard executeCqlAndGetRelevantResults(Context context, Str logger.info("Prior Auth Required"); coverageRequirements.setSummary(humanReadableTopic + ": Prior Authorization required.") .setDetails("Prior Authorization required, follow the attached link for information."); + + // check if prior auth is automatically approved + if (evaluateStatement("APPROVE_PRIORAUTH", context) != null) { + coverageRequirements.setPriorAuthApproved((Boolean) evaluateStatement("APPROVE_PRIORAUTH", context)); + if (coverageRequirements.isPriorAuthApproved()) { + coverageRequirements.generatePriorAuthId(); + logger.info("Prior Auth Approved: " + coverageRequirements.getPriorAuthId()); + coverageRequirements.setSummary(humanReadableTopic + ": Prior Authorization approved.") + .setDetails("Prior Authorization approved, ID is " + coverageRequirements.getPriorAuthId()); + } + } + } else if (coverageRequirements.isDocumentationRequired()) { logger.info("Documentation Required"); coverageRequirements.setSummary(humanReadableTopic + ": Documentation Required.") @@ -173,6 +185,14 @@ protected CqlResultsForCard executeCqlAndGetRelevantResults(Context context, Str logger.info("-- No PA Request questionnaire defined"); } + try { + if (evaluateStatement("RESULT_QuestionnaireAdditionalUri", context) != null) { + coverageRequirements.setQuestionnaireAdditionalUri(evaluateStatement("RESULT_QuestionnaireAdditionalUri", context).toString()); + } + } catch (Exception e) { + logger.info("-- No additional questionnaire defined"); + } + // process the alternative therapies try { if (evaluateStatement("ALTERNATIVE_THERAPY", context) != null) { diff --git a/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java b/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java index 92a5633aa..e67ff6f42 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java @@ -1,18 +1,16 @@ package org.hl7.davinci.endpoint.components; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; +import java.util.*; import org.cdshooks.*; import org.hl7.davinci.FhirComponentsT; import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.FhirRequestProcessor; +import org.hl7.davinci.endpoint.database.FhirResource; +import org.hl7.davinci.endpoint.database.FhirResourceRepository; +import org.hl7.davinci.r4.Utilities; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Annotation; -import org.hl7.fhir.r4.model.DeviceRequest; -import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,21 +80,33 @@ public CqlResultsForCard setRequest(IBaseResource request) { * @param cqlResults * @return card with appropriate information */ - public static Card transform(CqlResultsForCard cqlResults) { + public static Card transform(CqlResultsForCard cqlResults, Boolean addLink) { Card card = baseCard(); - Link link = new Link(); - link.setUrl(cqlResults.getCoverageRequirements().getInfoLink()); - link.setType("absolute"); - link.setLabel("Documentation Requirements"); + if (addLink) { + Link link = new Link(); + link.setUrl(cqlResults.getCoverageRequirements().getInfoLink()); + link.setType("absolute"); + link.setLabel("Documentation Requirements"); + card.setLinks(Arrays.asList(link)); + } - card.setLinks(Arrays.asList(link)); card.setSummary(cqlResults.getCoverageRequirements().getSummary()); card.setDetail(cqlResults.getCoverageRequirements().getDetails()); return card; } + /** + * Transforms a result from the database into a card, defaults to adding the link. + * + * @param cqlResults + * @return card with appropriate information + */ + public static Card transform(CqlResultsForCard cqlResults) { + return transform(cqlResults, true); + } + /** * Transforms a result from the database into a card. * @@ -180,6 +190,8 @@ public static Card alternativeTherapyCard(AlternativeTherapy alternativeTherapy, suggestionList.add(alternativeTherapySuggestion); card.setSuggestions(suggestionList); + card.setSelectionBehavior(Card.SelectionBehaviorEnum.ANY); + return card; } @@ -192,14 +204,90 @@ public static Card drugInteractionCard(DrugInteraction drugInteraction) { return card; } - public static Card createSuggestionsWithNote(Card card, - CqlResultsForCard cqlResults, - FhirComponentsT fhirComponents) { - List suggestionList = new ArrayList<>(); + public static Card priorAuthCard(CqlResultsForCard cqlResults, + IBaseResource request, + FhirComponentsT fhirComponents, + String priorAuthId, + String patientId, + String payerId, + String providerId, + String applicationFhirPath, + FhirResourceRepository fhirResourceRepository) { + logger.info("Build Prior Auth Card"); + + Card card = transform(cqlResults, false); + + // create the ClaimResponse + ClaimResponse claimResponse = Utilities.createClaimResponse(priorAuthId, patientId, payerId, providerId, applicationFhirPath); + + // build the FhirResource and save to the database + FhirResource fhirResource = new FhirResource(); + fhirResource.setFhirVersion(fhirComponents.getFhirVersion().toString()) + .setResourceType(claimResponse.fhirType()) + .setData(fhirComponents.getJsonParser().encodeResourceToString(claimResponse)); + fhirResource.setId(claimResponse.getId()); + fhirResource.setName(claimResponse.getId()); + FhirResource newFhirResource = fhirResourceRepository.save(fhirResource); + logger.info("stored: " + newFhirResource.getFhirVersion() + "/" + newFhirResource.getResourceType() + "/" + newFhirResource.getId()); + + // create the reference to the ClaimResponse + Reference claimResponseReference = new Reference(); + claimResponseReference.setReference("ClaimResponse/" + claimResponse.getId()); + + // add SupportingInfo to the Request + IBaseResource outputRequest = FhirRequestProcessor.addSupportingInfoToRequest(request, claimResponseReference); + + // add suggestion with ClaimResponse + Suggestion suggestionWithClaimResponse = createSuggestionWithResource(outputRequest, claimResponse, fhirComponents, + "Store the prior authorization in the EHR"); + card.addSuggestionsItem(suggestionWithClaimResponse); + + // add suggestion with annotation + Suggestion suggestionWithAnnotation = createSuggestionWithNote(card, outputRequest, fhirComponents, + "Store prior authorization as an annotation to the order", "Add authorization to record", + false); + card.addSuggestionsItem(suggestionWithAnnotation); + + card.setSelectionBehavior(Card.SelectionBehaviorEnum.AT_MOST_ONE); + + return card; + } + + public static Suggestion createSuggestionWithResource(IBaseResource request, + IBaseResource resource, + FhirComponentsT fhirComponents, + String label) { + Suggestion suggestion = new Suggestion(); + + suggestion.setLabel(label); + suggestion.setIsRecommended(true); + + // build the create Action + Action createAction = new Action(fhirComponents); + createAction.setType(Action.TypeEnum.create); + createAction.setDescription("Store " + resource.fhirType()); + createAction.setResource(resource); + suggestion.addActionsItem(createAction); + + // build the update Action + Action updateAction = new Action(fhirComponents); + updateAction.setType(Action.TypeEnum.update); + updateAction.setDescription("Update to the resource " + request.fhirType()); + updateAction.setResource(request); + suggestion.addActionsItem(updateAction); + + return suggestion; + } + public static Suggestion createSuggestionWithNote(Card card, + IBaseResource request, + FhirComponentsT fhirComponents, + String label, + String description, + boolean isRecommended) { Suggestion requestWithNoteSuggestion = new Suggestion(); - requestWithNoteSuggestion.setLabel("Save Update To EHR"); - requestWithNoteSuggestion.setIsRecommended(true); + requestWithNoteSuggestion.setLabel(label); + requestWithNoteSuggestion.setIsRecommended(isRecommended); List actionList = new ArrayList<>(); // build the Annotation @@ -210,19 +298,17 @@ public static Card createSuggestionsWithNote(Card card, String text = card.getSummary() + ": " + card.getDetail(); annotation.setText(text); annotation.setTime(new Date()); // set the date and time to now - IBaseResource resource = FhirRequestProcessor.addNoteToRequest(cqlResults.getRequest(), annotation); + IBaseResource resource = FhirRequestProcessor.addNoteToRequest(request, annotation); Action updateAction = new Action(fhirComponents); updateAction.setType(Action.TypeEnum.update); - updateAction.setDescription("Update original " + cqlResults.getRequest().fhirType() + " to add note"); + updateAction.setDescription(description); updateAction.setResource(resource); actionList.add(updateAction); requestWithNoteSuggestion.setActions(actionList); - suggestionList.add(requestWithNoteSuggestion); - card.setSuggestions(suggestionList); - return card; + return requestWithNoteSuggestion; } /** diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java new file mode 100644 index 000000000..ea583a748 --- /dev/null +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -0,0 +1,409 @@ +package org.hl7.davinci.endpoint.controllers; + +import org.hl7.davinci.endpoint.Application; +import org.hl7.davinci.endpoint.files.FileResource; +import org.hl7.davinci.endpoint.files.FileStore; +import org.hl7.fhir.instance.model.api.IDomainResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.davinci.r4.FhirComponents; + +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; + +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemAnswerOptionComponent; +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; +import org.hl7.fhir.r4.model.Reference; + +// --- ORDER OF RESPONSE-REQUEST OPERATIONS +// (REQUEST) External user sends the initial QuestionnaireResponse JSON that contains which questionnaire it would like to trigger as n element the "contained" field. +// (RESPONSE) QuestionnaireController adds the first question with its answerResponse options (with its linkId and text) to the JSON in QuestionnaireResponse.contained.item[] and sends it back. +// (REQUEST) External user adds their answer to the question to the JSON in QuestionnaireResponse.item[] and sends it back. +// (RESPONSE) QuestionnaireController takes that response and adds the next indicated question to the JSON in QuestionnaireResponse.contained.item[] and sends it back. +// Repeat intil QuestionnaireController reaches a leaf-node, then it sets the status to "completed" from "in-progress" +// Ultimately, The QuestionnaireController responses add ONLY to the QuestionnaireResponse.contained.item[]. The external requester adds answers to QuestionnaireResponse.item[] and includes the associated linkid and text. + +@CrossOrigin +@RestController +@RequestMapping("/Questionnaire") +public class QuestionnaireController { + + @Autowired + private FileStore fileStore; + + /** + * An inner class that demos a tree to define next questions based on responses. + */ + private class AdaptiveQuestionnaireTree { + + // The initial question node of the tree. + private AdaptiveQuestionnaireNode root; + + /** + * Initial constructor that generates the beginning of the tree. + * @param inputQuestionnaire The input questionnaire from the CDS-Library. + */ + public AdaptiveQuestionnaireTree(Questionnaire inputQuestionnaire) { + // Top level parent question item; the first set of questions. + QuestionnaireItemComponent topLevelQuestion = inputQuestionnaire.getItemFirstRep(); + // Start the root building. + this.root = new AdaptiveQuestionnaireNode(topLevelQuestion); + } + + /** + * Returns the next question based on the response to the current question. Also sets the next question based on that response. + * @param inputQuestionnaireResponse + * @param allAnswerItems The set of answer items given to this tree. + * @return + */ + public List getNextQuestionsForAnswers(List allResponseItems, QuestionnaireResponse inputQuestionnaireResponse) { + if(allResponseItems == null) { + throw new NullPointerException("Input answer items is null."); + } else if ((new HashSet(allResponseItems.stream().map(item -> item.getLinkId()).collect(Collectors.toList()))).size() != allResponseItems.size()){ + throw new RuntimeException("Detected duplicate answers to the same question."); + } + return this.root.getNextQuestionForAnswers(allResponseItems, inputQuestionnaireResponse); + } + + /** + * Inner class that describes a node of the tree. + */ + private class AdaptiveQuestionnaireNode { + + // Contains the list of additional questions that should be displayed with this question. + private List supplementalQuestions; + // Contains the current question item that dictates the next question of the node. + private QuestionnaireItemComponent determinantQuestionItem; + // Map of (answerResponse->childQuestionItemNode) (The child could have answer options within it or be a leaf node. It does have a question item component though). + private Map children; + + /** + * Constructor + * @param determinantQuestionItem + */ + public AdaptiveQuestionnaireNode(QuestionnaireItemComponent determinantQuestion) { + + this.determinantQuestionItem = determinantQuestion; + // Get the child and supplemental question items of this question. + List subQuestions = determinantQuestion.getItem(); + // Extract the supplemental questions which do not have a child link-id branch from the determinant questions. + List nonSupplementLinkIds = determinantQuestionItem.getAnswerOption().stream().map(answerOption -> answerOption.getModifierExtensionFirstRep().getUrl()).collect(Collectors.toList()); + List childQuestions = this.extractChildQuestions(subQuestions, nonSupplementLinkIds); + // Extract the remaining questions as supplemental questions. + this.supplementalQuestions = this.extractSupplementalQuestions(subQuestions, nonSupplementLinkIds); + + // The number of answer options of the determinant question should always equal the number of child question items. + if((this.determinantQuestionItem.getAnswerOption().size() != childQuestions.size())){ + throw new RuntimeException("There should be the same number of answer options as sub-items. Answer options: " + this.determinantQuestionItem.getAnswerOption().size() + ", sub-items: " + childQuestions.size()); + } + + // If the determinant question item does not have any answer options, then this is a leaf node and should not generate any children. + if(determinantQuestionItem.hasAnswerOption()) { + Map childIdsToResponses = new HashMap(); + // This loop iterates over the possible answer options of this questionitem and links the linkId to its possible responses. + for(QuestionnaireItemAnswerOptionComponent answerOption : determinantQuestionItem.getAnswerOption()) { + // The Id of this answer response's next question. + String answerNextQuestionId = answerOption.getModifierExtensionFirstRep().getUrl(); + // The response that indicates this answer to the question. + String possibleAnswerResponse = answerOption.getValueCoding().getCode(); + // Check for issues. + if(answerNextQuestionId == null || possibleAnswerResponse == null){ + throw new RuntimeException("Malformed Adaptive Questionnaire. Missing a question ID or answer response."); + } + // Add the key-value pair of next question id to its assocated answer response. + childIdsToResponses.put(answerNextQuestionId, possibleAnswerResponse); + } + + // Create the map of answerResponses->subQuestionItems + this.children = new HashMap(); + List subQuestionItems = determinantQuestionItem.getItem(); + for(QuestionnaireItemComponent subQuestionItem : subQuestionItems){ + // SubQuestion linkId. + String subQuestionLinkId = subQuestionItem.getLinkId(); + // SubQuestion's associated response. + String subQuestionResponse = childIdsToResponses.get(subQuestionLinkId); + // Create a new node for this subQuestion. + AdaptiveQuestionnaireNode subQuestionNode = new AdaptiveQuestionnaireNode(subQuestionItem); + this.children.put(subQuestionResponse, subQuestionNode); + } + } + } + + /** + * Returns the next question based on the set of provided answers. + * @param allResponseItems + * @param inputQuestionnaireResponse + * @return + */ + public List getNextQuestionForAnswers(List allResponseItems, QuestionnaireResponse inputQuestionnaireResponse) { + + // Extract the current question being answered from the list if answer items. + String currentQuestionId = this.determinantQuestionItem.getLinkId(); + List currentQuestionResponses = allResponseItems.stream().filter(answerItem -> answerItem.getLinkId().equals(currentQuestionId)).collect(Collectors.toList()); + if(currentQuestionResponses.size() != 1) { + // If there are no more answer items to check, we've reached the end of the recursion. + // TODO - this could cause an unexpected end-of-questionnaire issue if incorrect responses are given. + return this.getQuestionSet(); + } + + QuestionnaireResponseItemComponent currentQuestionResponse = currentQuestionResponses.get(0); + QuestionnaireResponseItemAnswerComponent currentQuestionAnswer = currentQuestionResponse.getAnswerFirstRep(); + + // With the currrent question answer in hand, extract the next question. + String response; + if (currentQuestionAnswer.hasValueStringType()) { + response = currentQuestionAnswer.getValueStringType().asStringValue(); + } else if (currentQuestionAnswer.hasValueCoding()) { + response = currentQuestionAnswer.getValueCoding().getCode(); + } else { + throw new RuntimeException("Answer does not match one of the possible input types."); + } + if(!children.containsKey(response)){ + throw new NullPointerException("Response does not match with a possible next question."); + } + AdaptiveQuestionnaireNode nextNode = this.children.get(response); + + if(nextNode.isLeafNode()){ + // Since the next node is a leaf node, set the questionnaire response status to complete. + inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); + return nextNode.getQuestionSet(); + } + + // Has to be done this way without removing the previous answer response so that we don't alter the original list object. + List nextResponseItems = allResponseItems.stream().filter(responseItem -> !responseItem.equals(currentQuestionResponse)).collect(Collectors.toList()); + return nextNode.getNextQuestionForAnswers(nextResponseItems, inputQuestionnaireResponse); + } + + /** + * Returns the question items in the given list that do not have the linkids of the given list of strings. + * @param questionItems + * @param nonSupplementQuestions + * @return + */ + private List extractSupplementalQuestions( + List questionItems, List nonSupplementLinkIds) { + return questionItems.stream().filter(questionItem -> !nonSupplementLinkIds.contains(questionItem.getLinkId())).collect(Collectors.toList()); + } + + /** + * Returns the question items in the given list that do have the linkids of the given list of strings. + * @param questionItems + * @param nonSupplementQuestions + * @return + */ + private List extractChildQuestions( + List questionItems, List nonSupplementLinkIds) { + return questionItems.stream().filter(questionItem -> nonSupplementLinkIds.contains(questionItem.getLinkId())).collect(Collectors.toList()); + } + + /** + * Returns the set of questions associated with the node. Incldues all questions in the set, determinant and non-determinant. + * @return + */ + public List getQuestionSet() { + QuestionnaireItemComponent determinantQuestionNoChildren = this.removeChildrenFromQuestionItem(this.determinantQuestionItem); + List questionSet = new ArrayList(); + questionSet.add(determinantQuestionNoChildren); + questionSet.addAll(this.supplementalQuestions); + logger.info("--- Question Set: " + questionSet.stream().map(item -> item.getLinkId()).collect(Collectors.toList())); + return questionSet; + } + + /** + * Returns a new question item that is indentical to the input qusetion item except without the children. + * @param inputQuestionItem + * @return + */ + private QuestionnaireItemComponent removeChildrenFromQuestionItem(QuestionnaireItemComponent inputQuestionItem){ + QuestionnaireItemComponent questionItemNoChildren = new QuestionnaireItemComponent(); + questionItemNoChildren.setLinkId(inputQuestionItem.getLinkId()); + questionItemNoChildren.setText(inputQuestionItem.getText()); + questionItemNoChildren.setType(inputQuestionItem.getType()); + questionItemNoChildren.setRequired(inputQuestionItem.getRequired()); + questionItemNoChildren.setAnswerOption(inputQuestionItem.getAnswerOption()); + return questionItemNoChildren; + } + + /** + * Returns whether this questionniare is a leaf node. + * @return + */ + private boolean isLeafNode() { + return this.children == null || this.children.size() < 1; + } + } + } + + // Logger. + private static Logger logger = Logger.getLogger(Application.class.getName()); + // Trees that track the current and next questions. Is key-value mappng of: Map AdaptiveQuestionnaireTree> + private static final Map questionnaireTrees = new HashMap(); + + /** + * Retrieves the next question based on the request. + * @param request + * @param entity + * @return + */ + @PostMapping(value = "/$next-question", consumes = { MediaType.APPLICATION_JSON_VALUE, "application/fhir+json" }) + public ResponseEntity retrieveNextQuestion(HttpServletRequest request, HttpEntity entity) { + return getNextQuestionOperation(entity.getBody(), request); + } + + /** + * Returns the next question based on the request. + * @param body + * @param request + * @return + */ + private ResponseEntity getNextQuestionOperation(String body, HttpServletRequest request) { + logger.info("POST /Questionnaire/$next-question fhir+"); + + FhirContext ctx = new FhirComponents().getFhirContext(); + IParser parser = ctx.newJsonParser(); + + // Parses the body. + IDomainResource domainResource = (IDomainResource) parser.parseResource(QuestionnaireResponse.class, body); + if (!domainResource.fhirType().equalsIgnoreCase("QuestionnaireResponse")) { + logger.warning("unsupported resource type: "); + HttpStatus status = HttpStatus.BAD_REQUEST; + MediaType contentType = MediaType.TEXT_PLAIN; + return ResponseEntity.status(status).contentType(contentType).body("Bad Request"); + } else { + logger.info(" ---- Resource received " + domainResource.toString()); + QuestionnaireResponse inputQuestionnaireResponse = (QuestionnaireResponse) domainResource; + String fragmentId = inputQuestionnaireResponse.getQuestionnaire(); + List containedResource = inputQuestionnaireResponse.getContained(); + Questionnaire inputQuestionnaireFromRequest = null; + for (int i = 0; i < containedResource.size(); i++) { + Resource item = containedResource.get(i); + if (item.getResourceType().equals(ResourceType.Questionnaire)) { + Questionnaire checkInputQuestionnaire = (Questionnaire) item; + if (checkInputQuestionnaire.getId().equals(fragmentId)) { + inputQuestionnaireFromRequest = checkInputQuestionnaire; + break; + } + } + } + + logger.info("--- Received questionnaire response: " + ctx.newJsonParser().encodeResourceToString(inputQuestionnaireResponse)); + // Check that there are no duplicates in the recieved set of questions. + if ((new HashSet(((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().stream().map(item -> item.getLinkId()).collect(Collectors.toList()))).size() != ((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().size()){ + throw new RuntimeException("Received a set of questions with duplicates."); + } + + String questionnaireId = ((Reference) inputQuestionnaireResponse.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/contained-id").getValue()).getReference(); + System.out.println("Input Questionnaire: " + questionnaireId); + + if (inputQuestionnaireFromRequest != null) { + + if (!questionnaireTrees.containsKey(questionnaireId)) { + // If there is not already a tree that matches the requested questionnaire id, build it. + // Import the requested CDS-Library Questionnaire. + Questionnaire cdsQuestionnaire = QuestionnaireController.importCdsAdaptiveQuestionnaire(request, parser, fileStore, questionnaireId); + + // Build the tree. + AdaptiveQuestionnaireTree newTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); + questionnaireTrees.put(questionnaireId, newTree); + logger.info("--- Built Questionnaire Tree for " + questionnaireId); + } + + // Pull the tree for the requested questionnaire id. + AdaptiveQuestionnaireTree currentTree = questionnaireTrees.get(questionnaireId); + // Get the request's set of answer responses. + List allResponses = inputQuestionnaireResponse.getItem(); + // Pull the resulting next question that the recieved responses and answers point to from the tree without including its children. + List nextQuestionSetResults = currentTree.getNextQuestionsForAnswers(allResponses, inputQuestionnaireResponse); + // Add the next set of questions to the response. + QuestionnaireController.addQuestionSetToQuestionnaireResponse(inputQuestionnaireResponse, nextQuestionSetResults); + // Check that there no duplicates in the set of questions. + if ((new HashSet(((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().stream().map(item -> item.getLinkId()).collect(Collectors.toList()))).size() != ((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().size()){ + throw new RuntimeException("Attempted to send a set of questions with duplicates. Question IDs are: " + (((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().stream().map(item -> item.getLinkId()).collect(Collectors.toList()))); + } + + logger.info("--- Added next question set for questionnaire \'" + questionnaireId + "\' for responses \'" + allResponses + "\'."); + + // Build and send the response. + String formattedResourceString = ctx.newJsonParser().encodeResourceToString(inputQuestionnaireResponse); + logger.info("--- Sending questionnaire response: " + formattedResourceString); + return ResponseEntity.status(HttpStatus.ACCEPTED).contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") + .body(formattedResourceString); + } else { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") + .body("Input questionnaire from the request does not exist."); + } + } + } + + /** + * Imports the requested questionnaire from the CDS-Library. + * @param fileStore2 + * @param parser + * @param request + * @return + */ + private static Questionnaire importCdsAdaptiveQuestionnaire(HttpServletRequest request, IParser parser, FileStore fileStore, String questionnaireId) { + Questionnaire cdsQuestionnaire = null; + try { + String adaptiveQuestionniareFile = ("Questions-" + questionnaireId + "Adaptive.json").replace("#", ""); // The filename should be the questionnaire ID with these added values. + String topic = questionnaireId.replace("Additional", "").replace("#", ""); // The topic should be the questionnaire ID but without the 'Additional' tag. + // File is pulled from the file store as a file. + logger.info("--- Importing questionniare file: " + adaptiveQuestionniareFile + " from topic: " + topic); + FileResource fileResource = fileStore.getFile(topic, adaptiveQuestionniareFile, "R4", false); + if(fileResource == null) { + throw new RuntimeException("File resource pulled from the filestore is null."); + } + if(fileResource.getResource() == null) { + throw new RuntimeException("File resource pulled from the filestore has a null getResource()."); + } + cdsQuestionnaire = (Questionnaire) parser.parseResource(fileResource.getResource().getInputStream()); + logger.info("--- Imported Questionnaire " + cdsQuestionnaire.getId()); + } catch (DataFormatException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return cdsQuestionnaire; + } + + /** + * Adds the given set of questions to the contained questionniare in the questionnaire response. + * @param inputQuestionnaireResponse + * @param questionSet + */ + private static void addQuestionSetToQuestionnaireResponse(QuestionnaireResponse inputQuestionnaireResponse, List questionSet) { + // Add the next question set to the QuestionnaireResponse.contained[0].item[]. + Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); + questionSet.forEach(questionItem -> containedQuestionnaire.addItem(questionItem)); + } +} \ No newline at end of file diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/r4/CdsHooksController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/r4/CdsHooksController.java index b3afdfcc3..cb4ea5be6 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/r4/CdsHooksController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/r4/CdsHooksController.java @@ -1,10 +1,11 @@ package org.hl7.davinci.endpoint.controllers.r4; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; import org.cdshooks.CdsResponse; import org.hl7.davinci.endpoint.Utils; import org.hl7.davinci.endpoint.cdshooks.services.crd.CdsServiceInformation; import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.OrderSelectService; -import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.OrderSelectServiceRems; import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.OrderSignService; import org.hl7.davinci.r4.crdhook.CrdPrefetch; import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; @@ -12,10 +13,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.validation.Valid; +import java.io.IOException; +import java.util.stream.Collectors; @RestController("r4_CdsHooksController") public class CdsHooksController { @@ -28,7 +33,7 @@ public class CdsHooksController { @Autowired private OrderSelectService orderSelectService; @Autowired private OrderSignService orderSignService; - @Autowired private OrderSelectServiceRems orderSelectServiceRems; + /** * The FHIR r4 services discovery endpoint. * @return A services object containing an array of all services available on this server @@ -59,21 +64,6 @@ public CdsResponse handleOrderSelect(@Valid @RequestBody OrderSelectRequest requ return orderSelectService.handleRequest(request, Utils.getApplicationBaseUrl(httpServletRequest)); } - /** - * The coverage requirement discovery endpoint for the order select rems hook. - * @param request An order select triggered cds request - * @return The card response - */ - @CrossOrigin - @PostMapping(value = FHIR_RELEASE + URL_BASE + "/" + OrderSelectServiceRems.ID, - consumes = "application/json;charset=UTF-8") - public CdsResponse handleOrderSelectRems(@Valid @RequestBody OrderSelectRequest request, final HttpServletRequest httpServletRequest) { - logger.info("r4/handleOrderSelectRems"); - if (request.getPrefetch() == null) { - request.setPrefetch(new CrdPrefetch()); - } - return orderSelectServiceRems.handleRequest(request, Utils.getApplicationBaseUrl(httpServletRequest)); - } /** * The coverage requirement discovery endpoint for the order sign hook. * @param request An order sign triggered cds request diff --git a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java index ce1ccd6fa..08f8e50af 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java @@ -2,11 +2,11 @@ public class FhirResourceCriteria { - private String fhirVersion; - private String resourceType; - private String name; - private String id; - private String url; + private String fhirVersion = null; + private String resourceType = null; + private String name = null; + private String id = null; + private String url = null; public String getFhirVersion() { return fhirVersion; } @@ -44,8 +44,52 @@ public FhirResourceCriteria setUrl(String url) { } public String toString() { - return String.format( - "fhirVersion=%s, resourceType=%s, name=%s", fhirVersion, resourceType, name - ); + String string = new String(); + boolean first = true; + if (fhirVersion != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "fhirVersion=" + fhirVersion; + } + + if (resourceType != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "resourceType=" + resourceType; + } + + if (id != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "id=" + id; + } + + if (name != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "name=" + name; + } + + if (url != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "url=" + url; + } + return string; } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java index adacc77e9..5d01a8a0f 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java @@ -17,7 +17,7 @@ public interface FhirResourceRepository extends CrudRepository findById( @Param("criteria") FhirResourceCriteria criteria @@ -26,7 +26,7 @@ List findById( @Query( "SELECT r FROM FhirResource r WHERE " + "r.fhirVersion = :#{#criteria.fhirVersion} " - + "and r.resourceType = :#{#criteria.resourceType} " + + "and LOWER(r.resourceType) = :#{#criteria.resourceType} " + "and r.name = :#{#criteria.name}") List findByName( @Param("criteria") FhirResourceCriteria criteria @@ -35,7 +35,7 @@ List findByName( @Query( "SELECT r FROM FhirResource r WHERE " + "r.fhirVersion = :#{#criteria.fhirVersion} " - + "and r.resourceType = :#{#criteria.resourceType} " + + "and LOWER(r.resourceType) = :#{#criteria.resourceType} " + "and r.url = :#{#criteria.url}") List findByUrl( @Param("criteria") FhirResourceCriteria criteria diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java index a10381674..a0b52c5ed 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java @@ -21,6 +21,7 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.util.List; +import java.util.Locale; public abstract class CommonFileStore implements FileStore { @@ -101,10 +102,10 @@ protected FileResource readFhirResourceFromFiles(List fhirResource } public FileResource getFhirResourceByTopic(String fhirVersion, String resourceType, String name, String baseUrl) { - logger.info("CommonFileStore::getFhirResourceByTopic(): " + fhirVersion + "/" + resourceType + "/" + name); - FhirResourceCriteria criteria = new FhirResourceCriteria(); - criteria.setFhirVersion(fhirVersion).setResourceType(resourceType).setName(name); + criteria.setFhirVersion(fhirVersion).setResourceType(resourceType.toLowerCase()).setName(name); + logger.info("CommonFileStore::getFhirResourceByTopic(): " + criteria.toString()); + List fhirResourceList = fhirResources.findByName(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); @@ -125,12 +126,13 @@ public FileResource getFhirResourceById(String fhirVersion, String resourceType, public FileResource getFhirResourceById(String fhirVersion, String resourceType, String id, String baseUrl, boolean isRoot) { - logger.info("CommonFileStore::getFhirResourceById(): " + fhirVersion + "/" + resourceType + "/" + id); - FhirResourceCriteria criteria = new FhirResourceCriteria(); - criteria.setFhirVersion(fhirVersion).setResourceType(resourceType).setId(id); + criteria.setFhirVersion(fhirVersion).setResourceType(resourceType.toLowerCase()).setId(id); + logger.info("CommonFileStore::getFhirResourceById(): " + criteria.toString()); + List fhirResourceList = fhirResources.findById(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); + System.out.println("Resource Pulled: " + resource + "-" + resource.getFilename()); if ((resource != null) && fhirVersion.equalsIgnoreCase("r4")) { @@ -153,10 +155,10 @@ public FileResource getFhirResourceById(String fhirVersion, String resourceType, } public FileResource getFhirResourceByUrl(String fhirVersion, String resourceType, String url, String baseUrl) { - logger.info("CommonFileStore::getFhirResourceByUrl(): " + fhirVersion + "/" + resourceType + "/" + url); - FhirResourceCriteria criteria = new FhirResourceCriteria(); - criteria.setFhirVersion(fhirVersion).setResourceType(resourceType).setUrl(url); + criteria.setFhirVersion(fhirVersion).setResourceType(resourceType.toLowerCase()).setUrl(url); + logger.info("CommonFileStore::getFhirResourceByUrl(): " + criteria.toString()); + List fhirResourceList = fhirResources.findByUrl(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/SubQuestionnaireProcessor.java b/server/src/main/java/org/hl7/davinci/endpoint/files/SubQuestionnaireProcessor.java index f4fcf781b..a5b9adbb6 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/SubQuestionnaireProcessor.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/SubQuestionnaireProcessor.java @@ -8,12 +8,9 @@ import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Questionnaire; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.hl7.fhir.r4.model.Resource; /** * Processes FHIR R4 questionnaire to assemble sub-questionnaires into a complete questionnaire. diff --git a/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleCriteria.java b/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleCriteria.java index e630a7bad..b6728f847 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleCriteria.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleCriteria.java @@ -8,6 +8,7 @@ public class CoverageRequirementRuleCriteria { private String payor; + private String payorId; private String code; private String codeSystem; private String fhirVersion; @@ -45,6 +46,13 @@ public CoverageRequirementRuleCriteria setPayor(String payor) { return this; } + public String getPayorId() { return payorId; } + + public CoverageRequirementRuleCriteria setPayorId(String payorId) { + this.payorId = payorId; + return this; + } + public String getFhirVersion() { return fhirVersion; } public CoverageRequirementRuleCriteria setFhirVersion(String fhirVersion) { @@ -71,8 +79,9 @@ public static List createQueriesFromR4(List