Skip to content
This repository was archived by the owner on Jul 31, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions resources/src/main/java/org/cdshooks/Card.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -26,6 +29,8 @@ public class FhirBundleProcessor {
private List<String> selections;
private List<CoverageRequirementRuleResult> results = new ArrayList<>();

private boolean deidentifiedResourcesContainPhi = false;


public FhirBundleProcessor(FileStore fileStore, String baseUrl, List<String> selections) {
this.fileStore = fileStore;
Expand All @@ -39,14 +44,101 @@ public FhirBundleProcessor(FileStore fileStore, String baseUrl) {

public List<CoverageRequirementRuleResult> 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<Patient> 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<Coverage> 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<DeviceRequest> deviceRequestList = Utilities.getResourcesOfTypeFromBundle(DeviceRequest.class, deviceRequestBundle);
List<Patient> patients = Utilities.getResourcesOfTypeFromBundle(Patient.class, deviceRequestBundle);
logger.info("r4/FhirBundleProcessor::processDeviceRequests: Found " + patients.size() + " patients.");
List<Organization> 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())) {
Expand Down Expand Up @@ -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())) {
Expand Down Expand Up @@ -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())) {
Expand All @@ -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())) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -239,6 +340,7 @@ private void buildExecutionContexts(List<CoverageRequirementRuleCriteria> criter
HashMap<String, Resource> cqlParams = new HashMap<>();
cqlParams.put("Patient", patient);
cqlParams.put(requestType, request);

buildExecutionContexts(criteriaList, cqlParams);
}

Expand All @@ -257,6 +359,7 @@ private void buildExecutionContexts(List<CoverageRequirementRuleCriteria> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -77,14 +79,18 @@ public CqlResultsForCard setRequest(IBaseResource request) {
}
}

public void setDeidentifiedResourcesContainsPhi(boolean deidentifiedResourcesContainPhi) {
this.deidentifiedResourcesContainPhi = deidentifiedResourcesContainPhi;
}

/**
* Transforms a result from the database into a card.
*
* @param cardType
* @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);

Expand All @@ -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;
}
Expand All @@ -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);
}

Expand All @@ -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<Link> links = new ArrayList<Link>(card.getLinks());
links.add(smartAppLaunchLink);
Expand All @@ -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<Link> smartAppLaunchLinks) {
public Card transform(CardTypes cardType, CqlResultsForCard cqlResults, List<Link> smartAppLaunchLinks) {
Card card = transform(cardType, cqlResults);
List<Link> links = new ArrayList<Link>(card.getLinks());
links.addAll(smartAppLaunchLinks);
Expand All @@ -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<Suggestion> suggestionList = new ArrayList<>();
Suggestion alternativeTherapySuggestion = new Suggestion();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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());
Expand All @@ -374,19 +380,18 @@ 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);
logger.warn(msg + "; summary card sent to client");
}
}

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));
Expand All @@ -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;
}
Expand Down
Loading