Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

The following types of exceptional situations have been identified during early testing of rules with clinicians.

Situation

Error

Cause

Handling

Library rule expects Observation or QuestionnaireResponse

Wrong input

Library linked with activity producing Observation where QuestionnaireResponse expected (or vice versa)

  1. Create task to CareTeam that other PlanDefinition related to Library with matching input should be created or used. 

Library for rule requires reference range, none defined.

No reference range

Could be caused by wrong configuration (by PlanDefinition editor) or because CareTeam for particular CarePlan has removed the reference range.

  1. Rule could have fallback

  2. Create task for CareTeam to add reference range of proper type

Library for rule requires relative reference range type, only absolute reference range currently defined.

No reference range of proper type

Could be caused by wrong configuration (by PlanDefinition editor) or because CareTeam for particular CarePlan has removed the proper reference range.

  1. Rule could have fallback

  2. Create task for CareTeam to add reference range of proper type

Library for rule requires reference ranges for components (Observation.component), none or only one defined.

No reference range for particular component (code)

Could be caused by wrong configuration (by PlanDefinition editor) or because CareTeam for particular CarePlan has removed the proper reference range.

  1. Rule could have fallback

  2. Create task for CareTeam to add reference range of proper type

Library for rule requires reference range, wrong unit defined

Unit mismatch

Could be caused by wrong configuration (by PlanDefinition editor) or because CareTeam for particular CarePlan has updated reference range with wrong unit.

  1. Create task for CareTeam to correct unit

Library for rule is relative and requires an active reference base to compare to.

No reference base / Reference base code mismatch

Could be caused by wrong configuration (by PlanDefinition editor) or because CareTeam for particular CarePlan has removed the reference base.

  1. Rule could have fallback

  2. Create task for CareTeam to add reference base in force

Library for rule is relative and requires an active reference base to compare to.

No active reference base

Could be caused by reference base expiry.

  1. Rule could have fallback

  2. Create task for CareTeam to add reference base in force

Library for rule requires reference base, wrong unit defined

Unit mismatch

Could be caused by wrong configuration (by PlanDefinition editor) or because CareTeam for particular CarePlan has updated reference base with wrong unit.

  1. Create task for CareTeam to correct unit

Library for rule is relative and expects historic (previous) Observation within period of time

No historic Observation found

First Observation submitted or no Observation submitted in period.

  1. Rule could have fallback

  2. Create task to CareTeam that other PlanDefinition with other related Library with other period of time should be considered/be applied to Patient

    1. This does not handle first Observation submitted

Library for rule expects QuestionnaireResponse with specific, required question/answer

Question/answer required by rule cannot be found

Either:

  • Rule is requiring answer for a question which is not enabled (or in path) or the question is optional

  • Rule is used with a Questionnaire where the required question/answer has been removed.

  1. Rule could have fallback, alternative handling

  2. Create task to CareTeam that answer required by the rule has not been provided.

Coding error in rule/unforeseen and unhandled error situation

NullPointerException

Missing guards

Log entry

To help handle these situtions a number of RuleExecutionExcptions has been defined and made available to the rules:

...

Code Block
function void throwMismatchedInputException(String ruleDescription, String focus) {
    throw new MismatchedInputException(ruleDescription, focus);
}

rule "COPDQuestionnaireFUTTriageRule"
dialect "java"
    when
        $qrs : Collection()
        $listQrs : ArrayList() from collect (QuestionnaireResponse() from $qrs)
        $questionnaire : Questionnaire()
        $procedureRequest$serviceRequest : ProcedureRequestServiceRequest()

    then
        if ($listQrs.isEmpty()) {
            throwMismatchedInputException("Manglende input. Forventede én spørgeskemabesvarelse", $procedureRequest$serviceRequest.getId());
        }

This rule expects a QuestionnaireResponse as primary input. If it is not found - for example because an observation was provided instead - then a MismatchedInputException is thrown with a detailed error description and the procedureRequest ServiceRequest as focus.

DefinedQuestions in rules for QuestionnaireResponses example

...

Expand
titleComplete rule code
Code Block
package rules
import org.hl7.fhir.dstu3.model.QuestionnaireResponse
import org.hl7.fhir.dstu3.model.Questionnaire
import org.hl7.fhir.dstu3.model.ProcedureRequestServiceRequest
import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import com.systematic.ehealth.automatedprocessing.ClinicalImpressionDTO
import com.systematic.ehealth.automatedprocessing.TaskDTO
import com.systematic.ehealth.automatedprocessing.CommunicationDTO
import com.systematic.ehealth.automatedprocessing.CodingDTO
import java.util.ArrayList
import java.util.Collection
import java.util.List
import java.util.Collections
import java.util.HashMap
import java.util.Map
import com.systematic.ehealth.exceptions.MismatchedInputException

global com.systematic.ehealth.automatedprocessing.AutomatedProcessingDTO ruleResult

function Map<String, List<String>> getRedTriggerMap() {
    Map<String, List<String>> result = new HashMap<>();
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/f2199b4f-f454-414f-9b27-c1ab80f5ae48",  List.of("En del", "Meget")); // S5.b
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/002efb9b-0efc-49c7-81d5-3f4542a5658f", List.of("En del", "Meget")); // S6.b
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/b9610f7d-df61-4988-8566-b58c1d3b9b14",  List.of("En del", "Meget")); // S7.b
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/cf55d29c-eabd-4909-9824-69d4690f2332", List.of("Mørkt")); // S9.a
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/843302ef-6623-42f1-b4e3-95fcb74e3108",  List.of("En del", "Meget")); // S10.b
    return result;
}

function Map<String, List<String>> getYellowTriggerMap() {
    Map<String, List<String>> result = new HashMap<>();
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/1967378e-f98d-11ea-adc1-0242ac120002",  List.of("Ja")); // S1.a
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/e66607e4-f99a-11ea-adc1-0242ac120002",  List.of("Nej")); // S2.a
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/53ea7be9-0b26-41d1-aebb-5b73418d86ed",  List.of("Lidt", "En del", "Meget")); // S5.a
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/f2199b4f-f454-414f-9b27-c1ab80f5ae48",  List.of("Lidt")); // S5.b
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/caccac4a-9632-4eb1-9266-c4cdccfa1b57", List.of("Lidt", "En del", "Meget")); // S6.a
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/002efb9b-0efc-49c7-81d5-3f4542a5658f", List.of("Lidt")); // S6.b
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/e4ab995c-f0c6-4a86-b505-9180a27bd04b",  List.of("Lidt", "En del", "Meget")); // S7.a
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/b9610f7d-df61-4988-8566-b58c1d3b9b14",  List.of("Lidt")); // S7.b
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/1c18e70e-c726-4a66-bf4c-a98877179cd7",  List.of("Lidt", "En del", "Meget")); // S8.a
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/5221408a-22ea-46f0-aaaf-4d417ef9fc2a",  List.of("Lidt", "En del", "Meget")); // S10.a
    result.put("http://ehealth.sundhed.dk/DefinedQuestion/843302ef-6623-42f1-b4e3-95fcb74e3108",  List.of("Lidt")); // S10.b
    return result;
}

function void add(Map triggeredQuestionAnswers, String itemId, String answer) {
    List<String> answers = (List<String>) triggeredQuestionAnswers.get(itemId);
    if (answers == null) {
        triggeredQuestionAnswers.put(itemId, List.of(answer));
    } else {
        answers.add(answer);
    }
}

function Map<String, List<String>> getTriggeredQuestionAnswers(QuestionnaireResponse.QuestionnaireResponseItemComponent item, Map triggerMap, Questionnaire q) {
    Map<String, List<String>> triggers = (Map<String, List<String>>)triggerMap;
    Map<String, List<String>> result = new HashMap<>();
    String definition = getQuestionDefinition(q, item.getLinkId());
    if (definition != null) {
        definition = getVersionlessDefinition (definition);
        List<String> answers = triggers.get(definition);
        if (answers != null) {
            for (QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent answer : item.getAnswer()) {
                if (answer.hasValueBooleanType() && answers.contains(answer.getValueBooleanType().booleanValue() ? "true" : "false")) {
                    add(result, item.getLinkId(), answer.getValueBooleanType().booleanValue() ? "true" : "false");
                } else if (answer.hasValueStringType() && answers.contains(answer.getValueStringType().getValue())) {
                    add(result, item.getLinkId(), answer.getValueStringType().getValue());
                }
            }
        }
    }
    for (QuestionnaireResponse.QuestionnaireResponseItemComponent subItem : item.getItem()) {
        result.putAll(getTriggeredQuestionAnswers(subItem, triggers, q));
    }
    return result;
}

function String getVersionlessDefinition(String definition) {
    if (definition.contains("|")) {
        return definition.substring(0,definition.indexOf("|"));
    }
    return definition;
}

function Map<String, List<String>> getAnswersMatchingInQuestionnaireResponse(QuestionnaireResponse qr, Map triggerMap, Questionnaire q) {
    Map<String, List<String>> triggers = (Map<String, List<String>>)triggerMap;
    Map<String, List<String>> result = new HashMap<>();
    for (QuestionnaireResponse.QuestionnaireResponseItemComponent item : qr.getItem()) {
        result.putAll(getTriggeredQuestionAnswers(item, triggers, q));
    }
    return result;
}

function String getQuestionTextFromItem(Questionnaire.QuestionnaireItemComponent item, String linkId) {
    if (linkId.equals(item.getLinkId())) {
        return item.getText();
    }
    for (Questionnaire.QuestionnaireItemComponent subItem : item.getItem()) {
        String question = getQuestionTextFromItem(subItem, linkId);
        if (question != null) {
            return question;
        }
    }
    return null;
}

function String getQuestionDefinitionFromItem(Questionnaire.QuestionnaireItemComponent item, String linkId) {
    if (linkId.equals(item.getLinkId())) {
        return item.getDefinition();
    }
    for (Questionnaire.QuestionnaireItemComponent subItem : item.getItem()) {
        String definition = getQuestionDefinitionFromItem(subItem, linkId);
        if (definition != null) {
            return definition;
        }
    }
    return null;
}

function String getQuestionText(Questionnaire questionnaire, String linkId) {
    for (Questionnaire.QuestionnaireItemComponent item : questionnaire.getItem()) {
        String question = getQuestionTextFromItem(item, linkId);
        if (question != null) {
            return question;
        }
    }
    return null;
}

function String getQuestionDefinition(Questionnaire questionnaire, String linkId) {
    for (Questionnaire.QuestionnaireItemComponent item : questionnaire.getItem()) {
        String definition = getQuestionDefinitionFromItem(item, linkId);
        if (definition != null) {
            return definition;
        }
    }
    return null;
}

function String getDescriptionFromMap( Map triggeredMap, Questionnaire questionnaire) {
    Map<String, List<String>> answers = (Map<String, List<String>>)triggeredMap;
    StringBuffer sb = new StringBuffer();
    for (String linkId : answers.keySet()) {
            if (sb.length() > 0) {
                sb.append("\n");
            }
            sb.append(getQuestionText(questionnaire, linkId));
            sb.append(": ");
            List<String> answersList = answers.get(linkId);
            for (int i = 0; i < answersList.size(); i++) {
                if (i > 0) {
                    sb.append(", ");
                }
                sb.append(answersList.get(i));
            }
    }
    return sb.toString();
}

function String getDescription(Map redAlarmAnswersMap, Map yellowAlarmAnswersMap, Questionnaire questionnaire) {
    Map<String, List<String>> redAlarmAnswers = (Map<String, List<String>>)redAlarmAnswersMap;
    StringBuffer sb = new StringBuffer();
    if (!redAlarmAnswers.isEmpty()) {
        sb.append("Der er rød alarm grundet følgende besvarelser af spørgsmål (givet som spørgsmål: svar)\n");
        sb.append(getDescriptionFromMap(redAlarmAnswers, questionnaire));
    }
    Map<String, List<String>> yellowAlarmAnswers = (Map<String, List<String>>)yellowAlarmAnswersMap;
    if (!yellowAlarmAnswers.isEmpty()) {
        if (sb.length() > 0) {
            sb.append("\n");
        }
        sb.append("Der er gul alarm grundet følgende besvarelser af spørgsmål (givet som spørgsmål: svar)\n");
        sb.append(getDescriptionFromMap(yellowAlarmAnswers, questionnaire));
    }
    if (yellowAlarmAnswers.isEmpty() && redAlarmAnswers.isEmpty()) {
        if (sb.length() > 0) {
            sb.append("\n");
        }
        sb.append("Der er ingen røde eller gule alarmer.");
    }

    return sb.toString();
}

function List<CodingDTO> getFindings(Map redAlarmAnswersMap, Map yellowAlarmAnswersMap) {
    List<CodingDTO> findings = new ArrayList<CodingDTO>();
    if (!redAlarmAnswersMap.isEmpty()) {
          findings.add(new CodingDTO("http://ehealth.sundhed.dk/cs/clinicalimpression-finding-codes", "red-question-answer", "Rød spørgsmål/svar-kombination fundet i spørgeskemabesvarelse"));
    }
    if (!yellowAlarmAnswersMap.isEmpty()) {
        findings.add(new CodingDTO("http://ehealth.sundhed.dk/cs/clinicalimpression-finding-codes", "yellow-question-answer", "Gul spørgsmål/svar-kombination fundet i spørgeskemabesvarelse"));
    }
    if (yellowAlarmAnswersMap.isEmpty() && redAlarmAnswersMap.isEmpty()) {
        findings.add(new CodingDTO("http://ehealth.sundhed.dk/cs/clinicalimpression-finding-codes", "green-question-answer", "Grøn spørgsmål/svar-kombination fundet i spørgeskemabesvarelse. Når anført som opsummering for hel spørgeskemabesvarelse er der ikke fundet røde eller gule spørgsmål/svar-kombinationer."));
    }
    return findings;
}

function TaskDTO createRoutineTask() {
   CodingDTO taskCategory = new CodingDTO("http://ehealth.sundhed.dk/cs/task-category", "MeasurementForAssessment", "Need assessment of measurement");
   String taskDescription = "Spørgeskemabesvarelse til evaluering";
   String priority = "routine";
   CodingDTO restrictionCategory = new CodingDTO("http://ehealth.sundhed.dk/cs/restriction-category", "measurement-monitoring", "Monitoring of measurement(s)");
   return new TaskDTO(taskCategory, taskDescription, priority, List.of(restrictionCategory), Collections.emptyList());
}

function ClinicalImpressionDTO createClinicalImpression(String description, List findings, String priority) {
   CodingDTO clinicalImpressionCode = new CodingDTO("http://ehealth.sundhed.dk/cs/clinicalimpression-codes", "TriagingResult", "Result of triaging");
   ClinicalImpressionDTO clinicalImpressionDTO = new ClinicalImpressionDTO(clinicalImpressionCode, description, findings, List.of());

   CodingDTO taskCategory = new CodingDTO("http://ehealth.sundhed.dk/cs/task-category", "MeasurementForAssessment", "Need assessment of measurement");
   CodingDTO restrictionCategory = new CodingDTO("http://ehealth.sundhed.dk/cs/restriction-category", "measurement-monitoring", "Monitoring of measurement(s)");
   clinicalImpressionDTO.setTasks(List.of(new TaskDTO(taskCategory, description, priority, List.of(restrictionCategory), Collections.emptyList())));
   return clinicalImpressionDTO;
}

function String determinePriority(Map redAlarmAnswers, Map yellowAlarmAnswers) {
    if (!redAlarmAnswers.isEmpty()) {
       return "asap";
    }

    if (!yellowAlarmAnswers.isEmpty()) {
       return "urgent";
    }

    return "routine";
}

function void throwMismatchedInputException(String ruleDescription, String focus) {
    throw new MismatchedInputException(ruleDescription, focus);
}

rule "COPDQuestionnaireFUTTriageRule"
dialect "java"
    when
        $qrs : Collection()
        $listQrs : ArrayList() from collect (QuestionnaireResponse() from $qrs)
        $questionnaire : Questionnaire()
        $procedureRequest$serviceRequest : ProcedureRequestServiceRequest()

    then
        if ($listQrs.isEmpty()) {
            throwMismatchedInputException("Manglende input. Forventede én spørgeskemabesvarelse", $procedureRequest$serviceRequest.getId());
        }
        if ($listQrs.size() > 1) {
            throwMismatchedInputException("Flere input. Forventede én spørgeskemabesvarelse", $procedureRequest$serviceRequest.getId());
        }
        QuestionnaireResponse qr = (QuestionnaireResponse)$listQrs.get(0);

        Map<String, List<String>> redAlarmAnswers = getAnswersMatchingInQuestionnaireResponse(qr,  getRedTriggerMap(), $questionnaire);
        Map<String, List<String>> yellowAlarmAnswers = getAnswersMatchingInQuestionnaireResponse(qr,  getYellowTriggerMap(), $questionnaire);

        String taskPriority = determinePriority(redAlarmAnswers, yellowAlarmAnswers);
        String description = getDescription(redAlarmAnswers, yellowAlarmAnswers, $questionnaire);
        List<CodingDTO> findings = getFindings(redAlarmAnswers, yellowAlarmAnswers);
        ruleResult.setClinicalImpressions(List.of(createClinicalImpression(description, findings, taskPriority)));

end;