diff --git a/resources/src/main/java/org/cdshooks/Card.java b/resources/src/main/java/org/cdshooks/Card.java index 2801dbf66..cf455490a 100755 --- a/resources/src/main/java/org/cdshooks/Card.java +++ b/resources/src/main/java/org/cdshooks/Card.java @@ -39,6 +39,15 @@ public String getDetail() { public void setDetail(String detail) { this.detail = detail; } + + // Add details to what already exists + public void addDetail(String detail) { + if (this.detail == null) { + setDetail(detail); + } else { + this.detail = detail + "\n" + this.detail; + } + } public IndicatorEnum getIndicator() { return indicator; 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 105d888f7..195116b10 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,45 +1,46 @@ 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 com.google.gson.Gson; - 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.cdshooks.services.crd.r4.FhirRequestProcessor; import org.hl7.davinci.endpoint.components.CardBuilder; -import org.hl7.davinci.endpoint.components.PrefetchHydrator; import org.hl7.davinci.endpoint.components.CardBuilder.CqlResultsForCard; +import org.hl7.davinci.endpoint.components.PrefetchHydrator; import org.hl7.davinci.endpoint.components.QueryBatchRequest; +import org.hl7.davinci.endpoint.config.YamlConfig; 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.CardTypes; import org.hl7.davinci.r4.CoverageGuidance; -import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; import org.hl7.davinci.r4.crdhook.DiscoveryExtension; +import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.opencds.cqf.cql.engine.execution.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestBody; +import javax.validation.Valid; +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.ArrayList; +import java.util.Date; +import java.util.List; + @Component public abstract class CdsService> { static final Logger logger = LoggerFactory.getLogger(CdsService.class); @@ -154,7 +155,6 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a // hydrated requestLog.advanceTimeline(requestService); - // Attempt a Query Batch Request to backfill missing attributes. if (myConfig.isQueryBatchRequest()) { QueryBatchRequest qbr = new QueryBatchRequest(this.fhirComponents); @@ -164,6 +164,7 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a logger.info("***** ***** request from requestLog: " + requestLog.toString() ); CdsResponse response = new CdsResponse(); + CardBuilder cardBuilder = new CardBuilder(); // CQL Fetched List lookupResults; @@ -173,7 +174,7 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a } catch (RequestIncompleteException e) { logger.warn("RequestIncompleteException " + request); logger.warn(e.getMessage() + "; summary card sent to client"); - response.addCard(CardBuilder.summaryCard(CardTypes.COVERAGE, e.getMessage())); + response.addCard(cardBuilder.summaryCard(CardTypes.COVERAGE, e.getMessage())); requestLog.setCardListFromCards(response.getCards()); requestLog.setResults(e.getMessage()); requestService.edit(requestLog); @@ -198,6 +199,7 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a requestLog.addTopic(requestService, lookupResult.getTopic()); CqlResultsForCard results = executeCqlAndGetRelevantResults(lookupResult.getContext(), lookupResult.getTopic()); CoverageRequirements coverageRequirements = results.getCoverageRequirements(); + cardBuilder.setDeidentifiedResourcesContainsPhi(lookupResult.getDeidentifiedResourceContainsPhi()); if (results.ruleApplies()) { foundApplicableRule = true; @@ -206,7 +208,7 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a // if prior auth already approved if (coverageRequirements.isPriorAuthApproved()) { - response.addCard(CardBuilder.priorAuthCard(results, results.getRequest(), fhirComponents, coverageRequirements.getPriorAuthId(), + 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)); @@ -223,14 +225,14 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a List smartAppLinks = createQuestionnaireLinks(request, applicationBaseUrl, lookupResult, results); if (coverageRequirements.isPriorAuthRequired()) { - Card card = CardBuilder.transform(CardTypes.PRIOR_AUTH, results, smartAppLinks); - card.addSuggestionsItem(CardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, + Card card = cardBuilder.transform(CardTypes.PRIOR_AUTH, results, smartAppLinks); + card.addSuggestionsItem(cardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, "Save Update To EHR", "Update original " + results.getRequest().fhirType() + " to add note", true, CoverageGuidance.ADMIN)); response.addCard(card); } else if (coverageRequirements.isDocumentationRequired()) { - Card card = CardBuilder.transform(CardTypes.DTR_CLIN, results, smartAppLinks); - card.addSuggestionsItem(CardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, + Card card = cardBuilder.transform(CardTypes.DTR_CLIN, results, smartAppLinks); + card.addSuggestionsItem(cardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, "Save Update To EHR", "Update original " + results.getRequest().fhirType() + " to add note", true, CoverageGuidance.CLINICAL)); response.addCard(card); @@ -239,7 +241,7 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a // add a card for an alternative therapy if there is one if (results.getAlternativeTherapy().getApplies() && hookConfiguration.getAlternativeTherapy()) { try { - response.addCard(CardBuilder.alternativeTherapyCard(results.getAlternativeTherapy(), + response.addCard(cardBuilder.alternativeTherapyCard(results.getAlternativeTherapy(), results.getRequest(), fhirComponents)); } catch (RuntimeException e) { logger.warn("Failed to process alternative therapy: " + e.getMessage()); @@ -247,13 +249,13 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a } } else { logger.warn("Unspecified Questionnaire URI; summary card sent to client"); - response.addCard(CardBuilder.transform(CardTypes.COVERAGE, results)); + response.addCard(cardBuilder.transform(CardTypes.COVERAGE, results)); } } else { // no prior auth or documentation required logger.info("Add the no doc or prior auth required card"); - Card card = CardBuilder.transform(CardTypes.COVERAGE, results); - card.addSuggestionsItem(CardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, + Card card = cardBuilder.transform(CardTypes.COVERAGE, results); + card.addSuggestionsItem(cardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, "Save Update To EHR", "Update original " + results.getRequest().fhirType() + " to add note", true, CoverageGuidance.COVERED)); card.setSelectionBehavior(Card.SelectionBehaviorEnum.ANY); @@ -263,7 +265,7 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a // apply the DrugInteractions if (results.getDrugInteraction().getApplies()) { - response.addCard(CardBuilder.drugInteractionCard(results.getDrugInteraction(), results.getRequest())); + response.addCard(cardBuilder.drugInteractionCard(results.getDrugInteraction(), results.getRequest())); } } } @@ -275,9 +277,9 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a if (!foundApplicableRule) { String msg = "No documentation rules found"; logger.warn(msg + "; summary card sent to client"); - response.addCard(CardBuilder.summaryCard(CardTypes.COVERAGE, msg)); + response.addCard(cardBuilder.summaryCard(CardTypes.COVERAGE, msg)); } - CardBuilder.errorCardIfNonePresent(CardTypes.COVERAGE, response); + cardBuilder.errorCardIfNonePresent(CardTypes.COVERAGE, response); } // Ading card to requestLog @@ -290,51 +292,36 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a private List createQuestionnaireLinks(requestTypeT request, URL applicationBaseUrl, CoverageRequirementRuleResult lookupResult, CqlResultsForCard results) { List listOfLinks = new ArrayList<>(); + List> linksToAdd = new ArrayList<>(); 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")); + linksToAdd.add(Pair.of(coverageRequirements.getQuestionnaireOrderUri(), "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")); + linksToAdd.add(Pair.of(coverageRequirements.getQuestionnaireFaceToFaceUri(), "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")); + linksToAdd.add(Pair.of(coverageRequirements.getQuestionnaireLabUri(),"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")); + linksToAdd.add(Pair.of(coverageRequirements.getQuestionnaireProgressNoteUri(),"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")); + linksToAdd.add(Pair.of(coverageRequirements.getQuestionnairePARequestUri(),"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")); + linksToAdd.add(Pair.of(coverageRequirements.getQuestionnairePlanOfCareUri(),"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")); + linksToAdd.add(Pair.of(coverageRequirements.getQuestionnaireDispenseUri(),"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")); + linksToAdd.add(Pair.of(coverageRequirements.getQuestionnaireAdditionalUri(),"Additional Form")); } + linksToAdd.forEach((e) -> { + listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, + e.getFirst(), coverageRequirements.getRequestId(), results.getRequest(), e.getSecond())); + }); return listOfLinks; } @@ -352,7 +339,7 @@ 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) { + String reqResourceId, IBaseResource request, String label) { URI configLaunchUri = myConfig.getLaunchUrl(); questionnaireUri = applicationBaseUrl + "/fhir/r4/" + questionnaireUri; @@ -383,11 +370,8 @@ private Link smartLinkBuilder(String patientId, String fhirBase, URL application // 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 appContext = "template=" + questionnaireUri + "&request=" + reqResourceId; - appContext = appContext + "&fhirpath=" + applicationBaseUrl + "/fhir/"; + String appContext = "questionnaire=" + questionnaireUri + "&order=" + reqResourceId + "&coverage=" + FhirRequestProcessor.getCoverageFromRequest(request).getReference(); - appContext = appContext + "&priorauth=" + (priorAuthRequired ? "true" : "false"); - appContext = appContext + "&filepath=" + applicationBaseUrl + "/"; if (myConfig.getUrlEncodeAppContext()) { logger.info("CdsService::smartLinkBuilder: URL encoding appcontext"); try { 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 1c53356ca..d298048c6 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 @@ -15,8 +15,11 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.Date; + public class FhirBundleProcessor { static final Logger logger = LoggerFactory.getLogger(FhirBundleProcessor.class); @@ -26,6 +29,8 @@ public class FhirBundleProcessor { private List selections; private List results = new ArrayList<>(); + private boolean deidentifiedResourcesContainPhi = false; + public FhirBundleProcessor(FileStore fileStore, String baseUrl, List selections) { this.fileStore = fileStore; @@ -39,14 +44,101 @@ public FhirBundleProcessor(FileStore fileStore, String baseUrl) { public List getResults() { return results; } + public boolean getDeidentifiedResourceContainsPhi() { return deidentifiedResourcesContainPhi; } + + private boolean validateField(boolean empty, String field) { + if (!empty) { + logger.warn("Instance is claiming to be deidentified but found information in the " + field + " field."); + } + return !empty; + } + + public boolean verifyDeidentifiedPatient(Bundle bundle) { + boolean invalid = false; + List patientList = Utilities.getResourcesOfTypeFromBundle(Patient.class, bundle); + for (Patient patient: patientList) { + Meta meta = patient.getMeta(); + for (CanonicalType profile : meta.getProfile()) { + if (profile.equals("http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-patient-deident")) { + invalid |= validateField(patient.getText().isEmpty(), "patient.text"); + invalid |= validateField(patient.getIdentifier().isEmpty(), "patient.identifier"); + invalid |= validateField(patient.getName().isEmpty(), "patient.name"); + invalid |= validateField(patient.getTelecom().isEmpty(), "patient.telecom"); + invalid |= validateField(patient.getDeceased() == null, "patient.deceased"); + invalid |= validateField(patient.getMultipleBirth() == null, "patient.multipleBirth"); + invalid |= validateField(patient.getPhoto().isEmpty(), "patient.photo"); + invalid |= validateField(patient.getContact().isEmpty(), "patient.contact"); + invalid |= validateField(patient.getLink().isEmpty(), "patient.link"); + + // check the address + for (Address address : patient.getAddress()) { + invalid |= validateField(address.getText() == null, "patient.address[].text"); + invalid |= validateField(address.getLine().isEmpty(), "patient.address[].line"); + invalid |= validateField(address.getCity() == null, "patient.address[].city"); + invalid |= validateField(address.getDistrict() == null, "patient.address[].district"); + invalid |= validateField(address.getPostalCode() == null, "patient.address[].postalCode"); + invalid |= validateField(address.getPeriod().isEmpty(), "patient.address[].period"); + } + + // check the birthdate + Date now = new Date(); + long diffInMs = Math.abs(now.getTime() - patient.getBirthDate().getTime()); + long diffInDays = TimeUnit.DAYS.convert(diffInMs, TimeUnit.MILLISECONDS); + String birthDateStr = patient.getBirthDateElement().asStringValue(); + + // if age is less than 2 years then there should be a year and month + if (diffInDays < (365 * 2)) { + invalid |= validateField(birthDateStr.length() <= 7, "patient.birthDate day (" + birthDateStr + ")"); + } else { + // otherwise there should only be a year + invalid |= validateField(birthDateStr.length() <= 4, "patient.birthDate month (" + birthDateStr + ")"); + } + } + } + } + return invalid; + } + public boolean verifyDeidentifiedCoverage(Bundle bundle) { + boolean invalid = false; + List coverageList = Utilities.getResourcesOfTypeFromBundle(Coverage.class, bundle); + for (Coverage coverage: coverageList) { + Meta meta = coverage.getMeta(); + for (CanonicalType profile : meta.getProfile()) { + if (profile.equals("http://hl7.org/fhir/us/davinci-crd/StructureDefinition/profile-coverage-deident")) { + invalid |= validateField(coverage.getText().isEmpty(), "coverage.text"); + invalid |= validateField(coverage.getIdentifier().isEmpty(), "coverage.identifier"); + invalid |= validateField(coverage.getPolicyHolder().isEmpty(), "coverage.policyHolder"); + invalid |= validateField(coverage.getSubscriber().isEmpty(), "coverage.subscriber"); + invalid |= validateField(coverage.getSubscriberId() == null, "coverage.subscriberId"); + invalid |= validateField(coverage.getDependent() == null, "coverage.dependent"); + invalid |= validateField(coverage.getRelationship().isEmpty(), "coverage.relationship"); + invalid |= validateField(coverage.getOrder() <= 0, "coverage.order"); + invalid |= validateField(coverage.getNetwork() == null, "coverage.network"); + invalid |= validateField(coverage.getCostToBeneficiary().isEmpty(), "coverage.costToBeneficiary"); + invalid |= validateField(coverage.getContract().isEmpty(), "coverage.contract"); + } + } + } + return invalid; + } + + public boolean verifyDeidentifiedResources(Bundle bundle) { + boolean invalid = verifyDeidentifiedPatient(bundle); + invalid |= verifyDeidentifiedCoverage(bundle); + deidentifiedResourcesContainPhi |= invalid; + return invalid; + } + public void processDeviceRequests(Bundle deviceRequestBundle, Bundle coverageBundle) { List deviceRequestList = Utilities.getResourcesOfTypeFromBundle(DeviceRequest.class, deviceRequestBundle); List patients = Utilities.getResourcesOfTypeFromBundle(Patient.class, deviceRequestBundle); logger.info("r4/FhirBundleProcessor::processDeviceRequests: Found " + patients.size() + " patients."); List payorList = Utilities.getResourcesOfTypeFromBundle(Organization.class, coverageBundle); // TODO - do something with the coverage. if (deviceRequestList.isEmpty()) return; - + logger.info("r4/FhirBundleProcessor::processDeviceRequests: " + deviceRequestList.size() + " DeviceRequest(s) found"); + verifyDeidentifiedResources(deviceRequestBundle); + verifyDeidentifiedResources(coverageBundle); for (DeviceRequest deviceRequest : deviceRequestList) { if (idInSelectionsList(deviceRequest.getId())) { @@ -74,6 +166,8 @@ public void processMedicationRequests(Bundle medicationRequestBundle, Bundle cov if (medicationRequestList.isEmpty()) return; logger.info("r4/FhirBundleProcessor::processMedicationRequests: MedicationRequest(s) found"); + verifyDeidentifiedResources(medicationRequestBundle); + verifyDeidentifiedResources(coverageBundle); for (MedicationRequest medicationRequest : medicationRequestList) { if (idInSelectionsList(medicationRequest.getId())) { @@ -103,6 +197,8 @@ public void processMedicationDispenses(Bundle medicationDispenseBundle, Bundle c if (medicationDispenseList.isEmpty()) return; logger.info("r4/FhirBundleProcessor::processMedicationDispenses: MedicationDispense(s) found"); + verifyDeidentifiedResources(medicationDispenseBundle); + verifyDeidentifiedResources(coverageBundle); for (MedicationDispense medicationDispense : medicationDispenseList) { if (idInSelectionsList(medicationDispense.getId())) { @@ -128,6 +224,8 @@ public void processServiceRequests(Bundle serviceRequestBundle, Bundle coverageB if (serviceRequestList.isEmpty()) return; logger.info("r4/FhirBundleProcessor::getAndProcessServiceRequests: ServiceRequest(s) found"); + verifyDeidentifiedResources(serviceRequestBundle); + verifyDeidentifiedResources(coverageBundle); for (ServiceRequest serviceRequest : serviceRequestList) { if (idInSelectionsList(serviceRequest.getId())) { @@ -157,6 +255,9 @@ public void processOrderSelectMedicationStatements(Bundle medicationRequestBundl if (medicationRequestList.isEmpty()) return; logger.info("r4/FhirBundleProcessor::processOrderSelectMedicationStatements: MedicationRequests(s) found"); + verifyDeidentifiedResources(medicationRequestBundle); + verifyDeidentifiedResources(medicationStatementBundle); + verifyDeidentifiedResources(coverageBundle); // process each of the MedicationRequests for (MedicationRequest medicationRequest : medicationRequestList) { @@ -239,6 +340,7 @@ private void buildExecutionContexts(List criter HashMap cqlParams = new HashMap<>(); cqlParams.put("Patient", patient); cqlParams.put(requestType, request); + buildExecutionContexts(criteriaList, cqlParams); } @@ -257,6 +359,7 @@ private void buildExecutionContexts(List criter //get the CqlRule CqlRule cqlRule = fileStore.getCqlRule(rule.getTopic(), rule.getFhirVersion()); result.setContext(CqlExecutionContextBuilder.getExecutionContext(cqlRule, cqlParams, baseUrl)); + result.setDeidentifiedResourceContainsPhi(deidentifiedResourcesContainPhi); results.add(result); } catch (Exception e) { logger.info("r4/FhirBundleProcessor::buildExecutionContexts: failed processing cql bundle: " + e.getMessage()); 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 561966a5b..daad32741 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 @@ -23,6 +23,8 @@ public class CardBuilder { static final Logger logger = LoggerFactory.getLogger(CardBuilder.class); + public boolean deidentifiedResourcesContainPhi = false; + public static class CqlResultsForCard { private Boolean ruleApplies; @@ -77,6 +79,10 @@ public CqlResultsForCard setRequest(IBaseResource request) { } } + public void setDeidentifiedResourcesContainsPhi(boolean deidentifiedResourcesContainPhi) { + this.deidentifiedResourcesContainPhi = deidentifiedResourcesContainPhi; + } + /** * Transforms a result from the database into a card. * @@ -84,7 +90,7 @@ public CqlResultsForCard setRequest(IBaseResource request) { * @param cqlResults * @return card with appropriate information */ - public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, Boolean addLink) { + public Card transform(CardTypes cardType, CqlResultsForCard cqlResults, Boolean addLink) { String requestId = Utilities.getIdFromIBaseResource(cqlResults.getRequest()); Card card = baseCard(cardType, requestId); @@ -97,7 +103,7 @@ public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, B } card.setSummary(cqlResults.getCoverageRequirements().getSummary()); - card.setDetail(cqlResults.getCoverageRequirements().getDetails()); + card.addDetail(cqlResults.getCoverageRequirements().getDetails()); return card; } @@ -109,7 +115,7 @@ public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, B * @param cqlResults * @return card with appropriate information */ - public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults) { + public Card transform(CardTypes cardType, CqlResultsForCard cqlResults) { return transform(cardType, cqlResults, true); } @@ -121,7 +127,7 @@ public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults) { * @param smartAppLaunchLink smart app launch Link * @return card with appropriate information */ - public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, Link smartAppLaunchLink) { + public Card transform(CardTypes cardType, CqlResultsForCard cqlResults, Link smartAppLaunchLink) { Card card = transform(cardType, cqlResults); List links = new ArrayList(card.getLinks()); links.add(smartAppLaunchLink); @@ -137,7 +143,7 @@ public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, L * @param smartAppLaunchLinks a list of links * @return card to be returned */ - public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, List smartAppLaunchLinks) { + public Card transform(CardTypes cardType, CqlResultsForCard cqlResults, List smartAppLaunchLinks) { Card card = transform(cardType, cqlResults); List links = new ArrayList(card.getLinks()); links.addAll(smartAppLaunchLinks); @@ -152,20 +158,20 @@ public static Card transform(CardTypes cardType, CqlResultsForCard cqlResults, L * @param summary The desired summary for the card * @return valid card */ - public static Card summaryCard(CardTypes cardType, String summary) { + public Card summaryCard(CardTypes cardType, String summary) { Card card = baseCard(cardType, ""); card.setSummary(summary); return card; } - public static Card alternativeTherapyCard(AlternativeTherapy alternativeTherapy, IBaseResource resource, + public Card alternativeTherapyCard(AlternativeTherapy alternativeTherapy, IBaseResource resource, FhirComponentsT fhirComponents) { logger.info("Build Alternative Therapy Card: " + alternativeTherapy.toString()); String requestId = Utilities.getIdFromIBaseResource(resource); Card card = baseCard(CardTypes.THERAPY_ALTERNATIVES_OPT, requestId); card.setSummary("Alternative Therapy Suggested"); - card.setDetail(alternativeTherapy.getDisplay() + " (" + alternativeTherapy.getCode() + ") should be used instead."); + card.addDetail(alternativeTherapy.getDisplay() + " (" + alternativeTherapy.getCode() + ") should be used instead."); List suggestionList = new ArrayList<>(); Suggestion alternativeTherapySuggestion = new Suggestion(); @@ -205,17 +211,17 @@ public static Card alternativeTherapyCard(AlternativeTherapy alternativeTherapy, return card; } - public static Card drugInteractionCard(DrugInteraction drugInteraction, IBaseResource resource) { + public Card drugInteractionCard(DrugInteraction drugInteraction, IBaseResource resource) { logger.info("Build Drug Interaction Card: " + drugInteraction.getSummary()); String requestId = Utilities.getIdFromIBaseResource(resource); Card card = baseCard(CardTypes.CONTRAINDICATION, requestId); card.setSummary(drugInteraction.getSummary()); - card.setDetail(drugInteraction.getDetail()); + card.addDetail(drugInteraction.getDetail()); card.setIndicator(Card.IndicatorEnum.WARNING); return card; } - public static Card priorAuthCard(CqlResultsForCard cqlResults, + public Card priorAuthCard(CqlResultsForCard cqlResults, IBaseResource request, FhirComponentsT fhirComponents, String priorAuthId, @@ -264,7 +270,7 @@ public static Card priorAuthCard(CqlResultsForCard cqlResults, return card; } - public static Suggestion createSuggestionWithResource(IBaseResource request, + public Suggestion createSuggestionWithResource(IBaseResource request, IBaseResource resource, FhirComponentsT fhirComponents, String label, @@ -291,7 +297,7 @@ public static Suggestion createSuggestionWithResource(IBaseResource request, return suggestion; } - public static Suggestion createSuggestionWithNote(Card card, + public Suggestion createSuggestionWithNote(Card card, IBaseResource request, FhirComponentsT fhirComponents, String label, @@ -360,7 +366,7 @@ public static Suggestion createSuggestionWithNote(Card card, return requestWithNoteSuggestion; } - private static Source createSource(CardTypes cardType) { + private Source createSource(CardTypes cardType) { Source source = new Source(); source.setLabel("Da Vinci CRD Reference Implementation"); source.setTopic(cardType.getCoding()); @@ -374,11 +380,10 @@ private static Source createSource(CardTypes cardType) { * @param cardType * @param response The response to check and add cards to */ - public static void errorCardIfNonePresent(CardTypes cardType, CdsResponse response) { + public void errorCardIfNonePresent(CardTypes cardType, CdsResponse response) { if (response.getCards() == null || response.getCards().size() == 0) { - Card card = new Card(); + Card card = baseCard(cardType, ""); card.setIndicator(Card.IndicatorEnum.WARNING); - card.setSource(createSource(cardType)); String msg = "Unable to process hook request from provided information."; card.setSummary(msg); response.addCard(card); @@ -386,7 +391,7 @@ public static void errorCardIfNonePresent(CardTypes cardType, CdsResponse respon } } - private static Card baseCard(CardTypes cardType, String requestId) { + private Card baseCard(CardTypes cardType, String requestId) { Card card = new Card(); card.setIndicator(Card.IndicatorEnum.INFO); card.setSource(createSource(cardType)); @@ -396,6 +401,10 @@ private static Card baseCard(CardTypes cardType, String requestId) { cardExtension.addAssociatedResource(requestId); card.setExtension(cardExtension); } + + if (deidentifiedResourcesContainPhi) { + card.setDetail("Note: de-identified resources provided in request contain Protected Health Information (PHI). Please notify administrator."); + } return card; } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/FhirController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/FhirController.java index 90fe75460..19209be96 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/FhirController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/FhirController.java @@ -1,5 +1,6 @@ package org.hl7.davinci.endpoint.controllers; +import org.aspectj.weaver.patterns.TypePatternQuestions; import org.hl7.davinci.FhirResourceInfo; import org.hl7.davinci.endpoint.Application; import org.hl7.davinci.endpoint.Utils; @@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.*; import java.io.IOException; +import java.util.Optional; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; @@ -28,7 +30,7 @@ */ @RestController public class FhirController { - private static Logger logger = Logger.getLogger(Application.class.getName()); + private static Logger logger = Logger.getLogger(FhirController.class.getName()); @Autowired private FileStore fileStore; @@ -229,7 +231,23 @@ public ResponseEntity submitFhirResource(HttpServletRequest request, Htt @PostMapping(path = "/fhir/{fhirVersion}/Questionnaire/$questionnaire-package", consumes = { MediaType.APPLICATION_JSON_VALUE, "application/fhir+json" }) public ResponseEntity questionnaireForOrderOperation(HttpServletRequest request, HttpEntity entity, @PathVariable String fhirVersion) { + return handleQuestionnaireOperation(request, entity, fhirVersion, null); + } + + /** + * FHIR Operation to retrieve a Questionnaire and CQL files associated with a given request. + * @param fhirVersion (converted to uppercase) + * @return FHIR Bundle + * @throws IOException + */ + @PostMapping(path = "/fhir/{fhirVersion}/Questionnaire/{questionnaireId}/$questionnaire-package", consumes = { MediaType.APPLICATION_JSON_VALUE, "application/fhir+json" }) + public ResponseEntity questionnaireForOrderOperationScoped(HttpServletRequest request, HttpEntity entity, + @PathVariable String fhirVersion, @PathVariable String questionnaireId) { + return handleQuestionnaireOperation(request, entity, fhirVersion, questionnaireId); + } + private ResponseEntity handleQuestionnaireOperation(HttpServletRequest request, HttpEntity entity, + String fhirVersion, String questionnaireId) { fhirVersion = fhirVersion.toUpperCase(); String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; @@ -238,15 +256,15 @@ public ResponseEntity questionnaireForOrderOperation(HttpServletRequest String resource = null; if (fhirVersion.equalsIgnoreCase("R4")) { QuestionnairePackageOperation operation = new QuestionnairePackageOperation(fileStore, baseUrl); - resource = operation.execute(entity.getBody()); + resource = operation.execute(entity.getBody(), questionnaireId); if (resource == null) { logger.warning("bad parameters"); HttpStatus status = HttpStatus.BAD_REQUEST; MediaType contentType = MediaType.TEXT_PLAIN; - + return ResponseEntity.status(status).contentType(contentType) - .body("Bad Parameters"); + .body("Bad Parameters"); } } else { @@ -255,11 +273,11 @@ public ResponseEntity questionnaireForOrderOperation(HttpServletRequest MediaType contentType = MediaType.TEXT_PLAIN; return ResponseEntity.status(status).contentType(contentType) - .body("Bad Request"); + .body("Bad Request"); } return ResponseEntity.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON) - .body(resource); + .body(resource); } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/QuestionnairePackageOperation.java b/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/QuestionnairePackageOperation.java index 0f91dc43a..fd73d4f8d 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/QuestionnairePackageOperation.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/fhir/r4/QuestionnairePackageOperation.java @@ -49,7 +49,7 @@ public QuestionnairePackageOperation(FileStore fileStore, String baseUrl) { /* * Do the work retrieving all of the Questionnaire, Library and Valueset Resources. */ - public String execute(String resourceString) { + public String execute(String resourceString, String questionnaireId) { Parameters outputParameters = new Parameters(); IBaseResource resource = null; @@ -68,7 +68,6 @@ public String execute(String resourceString) { // list of all of the orders Bundle orders = getAllResources(parameters, "order"); - if (coverage == null || orders.isEmpty()) { logger.error("Failed to find order or coverage within parameters"); return null; @@ -88,43 +87,22 @@ public String execute(String resourceString) { fhirBundleProcessor.processServiceRequests(orders, coverageBundle); fhirBundleProcessor.processMedicationDispenses(orders, coverageBundle); List topics = createTopicList(fhirBundleProcessor); - for (String topic : topics) { logger.info("--> process topic: " + topic); - - // get all of the Quesionnaires for the topic - Bundle bundle = fileStore.getFhirResourcesByTopicAsFhirBundle("R4", "Questionnaire", topic.toLowerCase(), baseUrl); - List bundleEntries = bundle.getEntry(); - for (BundleEntryComponent entry : bundleEntries) { - - addResourceToBundle(entry.getResource(), bundleContents, completeBundle); - - if (entry.getResource().fhirType().equalsIgnoreCase("Questionnaire")) { - Questionnaire questionnaire = (Questionnaire)entry.getResource(); - - List extensions = questionnaire.getExtension(); - for (Extension extension : extensions) { - if (extension.getUrl().endsWith("cqf-library")) { - CanonicalType data = (CanonicalType)extension.getValue(); - String url = data.asStringValue(); - Resource libraryResource = null; - - // look in the map and retrieve it instead of looking it up on disk if found - if (resources.containsKey(url)) { - libraryResource = resources.get(url); - } else { - libraryResource = fileStore.getFhirResourceByUrlAsFhirResource("R4", "Library", url, baseUrl); - resources.put(url, libraryResource); - } - - if (addResourceToBundle(libraryResource, bundleContents, completeBundle)) { - // recursively add the depends-on libraries if added to bundle - addLibraryDependencies((Library)libraryResource, bundleContents, completeBundle); - } - } - } + if (questionnaireId == null) { + // get all of the Quesionnaires for the topic + Bundle bundle = fileStore.getFhirResourcesByTopicAsFhirBundle("R4", "Questionnaire", topic.toLowerCase(), baseUrl); + List bundleEntries = bundle.getEntry(); + for (BundleEntryComponent entry : bundleEntries) { + processResource(entry.getResource(), bundleContents, completeBundle); + } // Questionnaires + } else { + // get only the specified Questionnaire + Resource questionnaireResource = fileStore.getFhirResourceByIdAsFhirResource("R4", "Questionnaire", questionnaireId, baseUrl); + if (questionnaireResource != null) { + processResource(questionnaireResource, bundleContents, completeBundle); } - } // Questionnaires + } } // topics // add the bundle to the output parameters if it contains any resources @@ -194,6 +172,36 @@ private List createTopicList(FhirBundleProcessor fhirBundleProcessor) { return topics; } + private void processResource(Resource resource, List bundleContents, Bundle completeBundle) { + addResourceToBundle(resource, bundleContents, completeBundle); + + if (resource.fhirType().equalsIgnoreCase("Questionnaire")) { + Questionnaire questionnaire = (Questionnaire)resource; + + List extensions = questionnaire.getExtension(); + for (Extension extension : extensions) { + if (extension.getUrl().endsWith("cqf-library")) { + CanonicalType data = (CanonicalType)extension.getValue(); + String url = data.asStringValue(); + Resource libraryResource = null; + + // look in the map and retrieve it instead of looking it up on disk if found + if (resources.containsKey(url)) { + libraryResource = resources.get(url); + } else { + libraryResource = fileStore.getFhirResourceByUrlAsFhirResource("R4", "Library", url, baseUrl); + resources.put(url, libraryResource); + } + + if (addResourceToBundle(libraryResource, bundleContents, completeBundle)) { + // recursively add the depends-on libraries if added to bundle + addLibraryDependencies((Library)libraryResource, bundleContents, completeBundle); + } + } + } + } + } + /* * Recursively add all of the libraries dependencies related by the "depends-on" type. */ 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 83aca77f9..325ec3838 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 @@ -162,9 +162,9 @@ public FileResource getFhirResourceById(String fhirVersion, String resourceType, 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")) { + System.out.println("Resource Pulled: " + resource + "-" + resource.getFilename()); // If this is a questionnaire, run it through the processor to modify it before // returning. diff --git a/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleResult.java b/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleResult.java index 8d94229ef..60c78ae4c 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleResult.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleResult.java @@ -7,6 +7,7 @@ public class CoverageRequirementRuleResult { private Context context; private CoverageRequirementRuleCriteria criteria; private String topic; + private boolean deidentifiedResourceContainsPhi; public Context getContext() { return context; } @@ -28,4 +29,11 @@ public CoverageRequirementRuleResult setTopic(String topic) { this.topic = topic; return this; } + + public boolean getDeidentifiedResourceContainsPhi() { return deidentifiedResourceContainsPhi; } + + public CoverageRequirementRuleResult setDeidentifiedResourceContainsPhi(boolean deidentifiedResourceContainsPhi) { + this.deidentifiedResourceContainsPhi = deidentifiedResourceContainsPhi; + return this; + } } diff --git a/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/components/CardBuilderTest.java b/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/components/CardBuilderTest.java index 5fd37bfcb..fb13d2a93 100644 --- a/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/components/CardBuilderTest.java +++ b/server/src/test/java/org/hl7/davinci/endpoint/cdshooks/components/CardBuilderTest.java @@ -21,7 +21,8 @@ public void testRulesWithNoAuthNeeded() { coverageRequirements.setInfoLink("http://some.link"); coverageRequirements.setSummary("The summary!"); cardResults.setCoverageRequirements(coverageRequirements); - Card card = CardBuilder.transform(CardTypes.COVERAGE, cardResults); + CardBuilder cardBuilder = new CardBuilder(); + Card card = cardBuilder.transform(CardTypes.COVERAGE, cardResults); assertEquals("The summary!", card.getSummary()); assertEquals("Some details.", card.getDetail()); assertEquals("http://some.link", card.getLinks().get(0).getUrl());