Mapping episode of care to patient program

Using the PatientProgram was more of looking for the closest part of our model that could map to EOC. So it was clearly not the ideal. That is why, as you pointed out, we do not support mapping from FHIR EOC to the OpenMRS model.

As you can see here https://openmrs.atlassian.net/wiki/spaces/Archives/pages/25480037/Episodes+of+Care+Design+Page we have always wanted to extend our model to fully support it, but never got to actually doing it.

Therefore, any effort towards eventually doing this right, is a step in the right direction.

Thanks @dkayiwa .. We are working on model expansion. We will propose a model and once we have some working version, we will showcase. If acceptable, we will look forward to inclusion as part of the core.

1 Like

@angshuonline that will be awesome. :+1:

Hello good people,

So, we have a first cut of the Episode of Care enhanced model.

Context: Bahmni already had EOC, which was pretty basic and was associated only during patient program enrollment.

The above should provide a good idea. The source code of the above is here

The following is the suggestion for the enhanced model inline with FHIR Episode of Care. Note, R4 does not have reasons (associated to a list of clinical reasons - condition, service etc) but R6 has them. Therefore, we plan to do so as extension to R4 EOC. Also, EOC can be PLANNED for a patient, hence we introduce a ref to patient and also introduced status.

As explained above, we may store case_id, account info and other attributes for Episode Of care, and hence the reason to extend using attribute model. We are thinking of storing case_id/insurance_id, account info as attribute. In FHIR Episode of Care, we plan to model all these as extension. This is the proposed model for the enhanced EOC

Let us know what you think.

Looks good to me. I like the ability to extend episodes with attributes. :slight_smile:

Did you intentionally leave out the void fields (voided, voided_by, void_reason, etc) for episode_reason?

not particularly, it was attempt to keep inline with FHIR spec (R6). Although I am not sure if removal is a common scenario. We can introduce voided in episode_reason.

Btw, there is also an associated model to keep the status history of EOC. Opinions are sort of divided, but I see no harm in introducing that.

(btw, I think discourse (talk) has plugins to show mermaid diagrams, I am not sure if OMRS talk has the plugin enabled)

It is now:

flowchart TD
    A[Christmas] -->|Get money| B(Go shopping)
    B --> C{Let me think}
    C -->|One| D[Laptop]
    C -->|Two| E[iPhone]
    C -->|Three| F[fa:fa-car Car]

Create a block like:

```mermaid
flowchart TD
    A[Christmas] -->|Get money| B(Go shopping)
    B --> C{Let me think}
    C -->|One| D[Laptop]
    C -->|Two| E[iPhone]
    C -->|Three| F[fa:fa-car Car]
```
1 Like

Should EOC map directly to encounters or to visits? I know FHIR has this as Encounter, but FHIR Encounters also encompass OMRS visits.

I think one other attribute that may be worth considering supporting is the careManager. It can be optional but seems worth having in the data model as the person “primary responsible” for the EOC.

1 Like

Yep, thought about care manager as well. Will introduce as optional.

I think we also should have episode_visit mapping table in addition to episode_encounter mapping table.

Reason being:

  1. As you mentioned, FHIR Encounter can represent either visit or encounter depending on type.
  2. Practical purpose:
  • If a patient is arriving at clinic and the visit itself is for that episode, then its easy to track visits and its associated encounters
  • At the same time, patient can come at the clinic, and only one of the encounter during the visit is associated to particular episode.

IMO, we should leave to the implementing UX to associate whether to visit or to encounter.

Incidentally, we want to have the “_revInclude” as part of the FHIR episode of care retrieval so that we can get list of encounters. Unfortunately, the current framework of SearchQueryInclude forces to me to retrieve using another BundleProvider implementation, which can cause

  1. “n+1” query - effecting performance (especially when its by UUID and not primary key)
  2. Encounter itself does not have entity mapping to Episode, meaning that I will have to override the SearchQueryIncludeImpl to handle that. Not a problem by itself, other than the additional queries mentioned above. Right now, our retrieval pattern is by a particular episode of care - so maybe thats ok.
  3. I already have the references to the Encounter entity at the time of translation episodeTranslator.toFhirResource() from the episode.encounters - but I can’t include them to the resulting bundle. One option is to add them as episodeOfCare.included resources if I were to do from episodeTranslator.toFhirResource(). but that violates the bundle “_revInclude” expectations. Maybe first cut, I would do the simple overriding of SearchQueryIncludeImpl and handle the _revInclude there.

Yeah, the _revInclude stuff is definitely not the most performant. Happy to swap it out with something else that makes better sense.

Would this result into a row in both the episode_visit and episode_encounter tables?

Yes (ideally).

My opinion: Will leave that to FHIR implementation (only).

  1. Episode is in different module and not part of OpenMRS core, where visit and encounter entities are defined.
  2. In the FHIR2 module (if it will have dependency to openmrs episode module), FhirEncounterService can have a postProcess method to handle association to EpisodeOfCare to encounter or visit.
  3. If a visit is already associated to the episode (episode_visit), then the postProcess can subsequently associate any encounter belonging to that visit also to the episode. If visit is not associated with the EpisodeOfCare, then its associates only with encounter (episode_encounter).
  4. Maybe as first cut, alternatively, we only associate episodeOfCare to visit or encounter if encounter.episodeOfCare is specified. And in that case (first cut), it purely depends on the implementing UX to pass on the episodeOfCare ref as part of encounter.

@dkayiwa about the voided attributes. FHIR does not have anything like a change record like behavior. However, we can interpret that when episodeOfCare is updated with a new set of “resaons”, mismatches get voided. However issue is that we wouldn’t know the “voided_reason”.

I can keep the field to be consistent with other OMRS models but other than an extension, I don’t see how that can be implemented without a FHIR extension. (btw, episodeOfCare.reason would already an extension, atleast till R6)

Thanks @angshuonline for the detailed explanation.

So what do we lose, if we do not implement episode_visit, but only associate EOC with encounters as FHIR expects?

In these cases, we usually just set the void_reason to some fixed string. E.g., that’s what happens when we replace an obs with another value. It makes the field less valuable, but at least it can document which process it’s part of “Voided due to user removal from reasons” or something like that.

1 Like

Technically, I don’t think you really lose anything. Because you can always derive the visit from the encounter association. AFAIK, Openmrs does not demand that you must have a Visit before you create an Encounter.

However, I think it does introduce some bit of inconvenience for the implementing UX solution. If a visit is associated with Episode of care (EOC), this is one time activity. Subsequently if any encounter is created within that visit, you don’t have to “ask” the end user to select an EOC. Implementing UX, can check whats the EOC context of the visit, and if EOC is already associated with the visit, then all encounters within the visit will be linked to the EOC as well.

E.g. in Bahmni’s case, in earlier implementation, we would start a EOC for a program enrollment. and we would have to determine if the program is associated with an EOC, and if yes, then any encounter done in context of program will be linked with the same EOC.

With the episode_visit introduced: In FHIR, when the specified encounter is submitted, we can check if the encounter’s visit (encounter.partOf) is associated with an EOC, and if so, automatically associate in episode_encounter

If encounter.episodeOfCare is specified during submission, then its straight forward. Irrespective of whether encounter is in context of a visit or not, just associate the same in episode_encounter. You may argue that there is some potential of inconsistency, that what if encounter.visit is already part of EOC1, but the specified encounter.episodeOfCare is EOC2. Well, I would argue that second case overrides. (instead of throwing an error)

btw, our initial hack idea was to store EOC id as visit attribute. On submission of encounter, we could potentially check if EOC is already associated with the encounter’s visit, and then automatically populate episode_encounter. Its not a hack hack, its custom but not standard!

Please check the following diagram. I am not having enough time, but I have a basic FHIR APIs implented with GET (with revInclude Encounter:episodeOfCare), POST, PUT, DELETE etc. Not returning attributes as extensions as mentioned above yet.

erDiagram
    episode ||--o{ episode_encounter : "has"
    episode ||--o{ episode_patient_program : "has"
    episode ||--o{ episode_visit : "has"
    episode ||--o{ episode_reason : "has"
    episode ||--o{ episode_attribute : "has"
    episode ||--o{ episode_status_history : "tracks"
    episode_attribute_type ||--o{ episode_attribute : "defines"
    
    %% External table relationships
    encounter ||--o{ episode_encounter : "linked to"
    patient_program ||--o{ episode_patient_program : "linked to"
    visit ||--o{ episode_visit : "linked to"
    concept ||--o{ episode_reason : "defines use"
    concept ||--o{ episode_reason : "defines value"
    concept ||--o{ episode : "defines type"
    patient ||--o{ episode : "has"
    users ||--o{ episode : "manages/creates/modifies"
    users ||--o{ episode_reason : "creates/voids"
    users ||--o{ episode_attribute_type : "creates/modifies/retires"
    users ||--o{ episode_attribute : "creates/modifies/voids"
    users ||--o{ episode_status_history : "creates"

    episode {
        int episode_id PK
        int patient_id FK "nullable"
        int concept_id FK "episode type, nullable"
        varchar status "default: UNKNOWN, nullable"
        datetime date_started "nullable"
        datetime date_ended "nullable"
        int care_manager FK "nullable"
        int creator FK "nullable"
        datetime date_created "NOT NULL"
        int changed_by FK "nullable"
        datetime date_changed "nullable"
        boolean voided "default: false, NOT NULL"
        int voided_by FK "nullable"
        datetime date_voided "nullable"
        varchar void_reason "nullable"
        char uuid UK "NOT NULL"
    }

    episode_encounter {
        int episode_id FK "NOT NULL"
        int encounter_id FK "NOT NULL"
    }

    episode_patient_program {
        int episode_id FK "NOT NULL"
        int patient_program_id FK "NOT NULL"
    }

    episode_visit {
        int episode_id FK "NOT NULL"
        int visit_id FK "NOT NULL"
    }

    episode_reason {
        int episode_reason_id PK
        int episode_id FK "NOT NULL"
        int use_concept_id FK "nullable"
        int value_concept_id FK "NOT NULL"
        varchar value_reference "nullable"
        int creator FK "nullable"
        datetime date_created "NOT NULL"
        boolean voided "default: false, NOT NULL"
        int voided_by FK "nullable"
        datetime date_voided "nullable"
        varchar void_reason "nullable"
        char uuid UK "NOT NULL"
    }

    episode_attribute_type {
        int episode_attribute_type_id PK
        varchar name "NOT NULL"
        varchar description "nullable"
        varchar datatype "nullable"
        text datatype_config "nullable"
        varchar preferred_handler "nullable"
        text handler_config "nullable"
        int min_occurs "NOT NULL"
        int max_occurs "nullable"
        int creator FK "NOT NULL"
        datetime date_created "NOT NULL"
        int changed_by FK "nullable"
        datetime date_changed "nullable"
        boolean retired "default: false, NOT NULL"
        int retired_by FK "nullable"
        datetime date_retired "nullable"
        varchar retire_reason "nullable"
        char uuid UK "NOT NULL, UNIQUE"
    }

    episode_attribute {
        int episode_attribute_id PK
        int episode_id FK "NOT NULL"
        int attribute_type_id FK "NOT NULL"
        text value_reference "NOT NULL"
        int creator FK "NOT NULL"
        datetime date_created "NOT NULL"
        int changed_by FK "nullable"
        datetime date_changed "nullable"
        boolean voided "default: false, NOT NULL"
        int voided_by FK "nullable"
        datetime date_voided "nullable"
        varchar void_reason "nullable"
        char uuid UK "NOT NULL, UNIQUE"
    }

    episode_status_history {
        int episode_status_history_id PK
        int episode_id FK "NOT NULL"
        varchar status "default: UNKNOWN, NOT NULL"
        datetime date_started "NOT NULL"
        datetime date_ended "nullable"
        int creator FK "nullable"
        datetime date_created "NOT NULL"
        char uuid "NOT NULL"
    }

    %% External tables (OpenMRS Core)
    encounter {
        int encounter_id PK
    }

    patient_program {
        int patient_program_id PK
    }

    visit {
        int visit_id PK
    }

    concept {
        int concept_id PK
    }

    patient {
        int patient_id PK
    }

    users {
        int user_id PK
    }

Few comments about episodeOfCare.reason, I want to start with a simple concept reference as value (episode_reason.value_concept_id) - but it can also have a reason use. As per the spec, it should be something like

reason" : [
  {
    "use" : { CodeableConcept },
    "value" : [ 
       {
         "concept" : { CodeableConcept }, 
         "reference" : { Reference } 
       }
     ]
  }    
]

I have used a flattened db model for capturing reason including use (episode_reason.use_concept_id) and value (episode_reason.value_concept_id, episode_reason.value_reference). Otherwise, we get too nested db models inside. I would imagine a programatic grouping of values against the reason “use” should be sufficient.

Let me know about your comments.

@ibacher thanks for enabling the mermaid plugin. :slight_smile: