Lotus AI Logo

Forms and data bindings

Data bindings are a powerful feature that enables seamless synchronization of patient information across different forms and services. By using data bindings, you can ensure that patient data is correctly identified, securely stored, and easily accessible.

This document explains how to use them effectively, and code examples to understand how to debug them, and create new ones.

Overview

The data binding feature allows you to prepopulate form fields with patient data and send new data to external medical records systems.

The Workflow is as follows:

Index

Retrieving Data

When a unique form is requested for a patient, we call the ClaimPower API to fetch the patient data and populate our Medplum store. When the patient opens the form, we show our copy of the data.

When a new form is requested for a patient, we call the Clampower API again to refresh the data.

The Claimpower API returns a Patient FHIR object, and additional objects like Coverage, CareTeam, Organization, DocumentReference, MedicationAdministration, Condition, Observation, and ActivityDefinition.

A set of methods in the file [medplum] src/medplum/patient.ts are used to call the parser methods to attempt to update the patient and related objects.

The file that manages connecting with the Claimpower API is src/utils/claimpower/api.ts.

It uses the claimpower_patient_id and the db_name to make a request to the Claimpower API.


const GetPatient = async (claimpower_patient_id: string, db_name: string): GetPatientData => {};

The Claimpower API responses and the lotus data model implement FHIR objects, they have very similar formats but there are some differences in the spelling of properties, and the location for some of the data.

When the data is retrieved we store as much as possible in the lotus store.

When the Claimpower API changes, the parser.get file must be updated to reflect the new data model, and additional methods might be required to parse and store the data correctly.

When creating new methods in the Claimpower API, or updating the existing ones, follow the FHIR recommendations and consider the medplum documentation to use similar data in both systems.

Prepopulating Form fields

When a patient receives a unique form link, we display the questionnaire and replace each field with the data we have in our database.

The file responsible is src/utils/setInitialFormValues.ts and it includes a method that receives a reference to a questionnaire and it changes it in place.

A questionnaire has an item property, which is an array of QuestionnaireItem objects.

Each item has a linkId property, the dictionary of methods in the file uses the linkId to call a unique method to retrieve the associated data.

When creating a new binding, ensure that the setInitialFormValues file is updated to include the new method.

Use the @medplum/fhir-types to document the FHIR objects and their properties and make sure that the Initial value being created matches the expected data type.

Refreshing the page

For user convenience, when a patient opens the form, we create a record in the localStorage that can be retrieved later.

Every time a patient changes an answer we update the localStorage record, and when the patient refreshes the page, we use the localStorage record to prepopulate the form fields.

The values in the localStorage will take precedence over the ones in our database, as we assume that the patient has the most recent information.

Parsing Form responses

When the patient submits the form, we read the answers provided by the user and use the linkId in each item to identify the fields.

The file that reads the answers has methods like this:


 if (i.linkId?.startsWith('Patient.Coverage')) {
      answer = getInitialFromCoverage(i.linkId, Coverage);
  }

Each binding that is created as a linkId needs a custom function to read it.

The answers can have different data type represented on their value like:


const answer = {
  valueString: '',
  valueBoolean: true,
  valueNumber: 1,
}

Questionnaire Response to Patient

We have a questionnaire that contains an array of items, each identified by a linkId. We use the file src/medplum/translators/questionnaire_response_to_patient.ts to read the answers provided by the user and update the patient and related objects.

After a form is submmited, the front end sends a POST request to the API in this file src/pages/api/claimpower/forms/pdf/index.ts.

The file reads the answers to generate a PDF, update the lotus medplum store, and make a request to the Claimpower API that includes the FHIR objects and the PDF.

Sending data to ClaimPower

To send the data to Claimpower, we combine the data we have in our database with the new answers provided by the user.

The parser.set file helps us remove ambiguity and ensure that the data is correctly stored.

The FHIR specification allows each implementation to have its own interpretation, the Claimpower API enforces different rules and validations that we need to follow.

Each binding must have its own method to guarantee that we're translating the data correctly, and avoid known pitfalls.

The Claimpower data model and the lotus store use very similar formats, but there are some differences in the spelling of properties, and the location for some of the data. Parsing the data proactively ensures that every binding is well documented, and allows for faster integrations when we use the fhir-types and the available parsers.

When creating a new binding, ensure that the parser.set file is updated to include the new method. And add the name of the linkId to the docs/yaml file to keep the documentation up to date.

Additionally, some screenings require a CPT code to be sent to the Claimpower API.

CPT codes are used to describe medical, surgical, and diagnostic services and are used to bill insurance companies for services provided.

Create new bindings

To create a new binding, you need to modify multiple files, as described below. Is important to complete all the steps to create the binding in both directions. Recommended to develop and test each step individually for better modularity and organization.

[get_form_url] src/pages/api/claimpower/patient/get_form_url.ts

The get_form_url api is used to create a unique form link for the patient.

When the link is called with a claimpower_patient_id and a db_name parameter, the function will make a request to the ClaimPower API to fetch the patient data and store it in our database.

It then uses the method replacePatient and createRelatedResources to store the data in our database.

The get_form_url is currently supporting multiple FHIR objects, the Claimpower API might return additional ones that could require a new parser function. To call the parser might require changes in the get_form_url file or the src/medplum/patient file.

[parsers.get] src/utils/claimpower/parser.get.ts

This file includes parser functions for multiple FHIR objects. The file uses @medplum/fhir-types to document the FHIR objects and their properties.

For example Patient is a FHIR object that includes demographic information about a patient. We use the Patient.extension property to store additional information.

The FHIR specification is vague about the implementation, and there are some differences between the Claimpower API response and the Medplum store. We use the parser to remove ambiguity, run validations and ensure that the data is correctly stored.

Extend one of the following functions or create a new one to parse the data from the ClaimPower API:


export const parseCareTeam = (patient: Patient, Organization: Organization): CareTeam => {};
export const parseOrganization = (patient: Patient, response: PatientResponse): Organization[] => {};
export const parseCoverage = (patient: Patient, response: PatientResponse): Coverage[] => {};
export const parseDocumentReference = (patient: Patient, response: PatientResponse): DocumentReference[] => {};
export const parseMedicationAdministration = (patient: Patient, response: PatientResponse): MedicationAdministration[] => {};
export const parseCondition = (patient: Patient, response: PatientResponse): Condition[] => {};
export const parseObservation = (patient: Patient, observations: Observation[]): Observation[] => {};
export const parseActivityDefinition = (patient: Patient, response: PatientResponse): ActivityDefinition[] => {};

[setInitialFormValues] src/utils/setInitialFormValues.ts

The medplum utility file src/medplum/patient includes a method to retrieve all the data we have about a patient.

The method is called in the page that renders the form src/pages/forms/[slug]/[form_id]/[patient_id].tsx.

The method const patientEverything = async (id: string, access_token: string) => {} returns a Bundle with all the resources related to a patient. We use the parseFhirPath utility to easily access the data and retrieve the different FHIR objects and their properties.

Every binding has a method on this file to prepopulate the form fields.

Example for a binding to get data:

Consult the medplum documentation and use @medplum/fhir-types to get insights in your IDE.

Extend one of the following methods, or create a new one:


const getInitialFromCoverage = (key: string, coverage?: Coverage[]): Initial => {};
const getInitialFromOrganization = (key: string, organization: Organization): Initial => {};
const getInitialFromCareTeam = (key: string, patient: Patient, everything?: Bundle): Initial => {};
const getInitialFromDocumentReference = (key: string, patient: Patient): Initial => {};
const formatToPhoneNumber = (phone: string): string => {};
const getInitialFromPatient = (key: string, patient: Patient): Initial => {};
const getInitials = (patient: Patient): Initial => {};
const buildInitialAnswers = (item: QuestionnaireItem[], patient: Patient, everything?: Bundle, resources?: any): QuestionnaireItem[] => {};
const setInitialFormValues = async (};

setInitialFormValues receives the questionnaire as a reference and changes it in place.

We use a dictionary pattern to find the method that corresponds to the linkId of the item, so that is easier to extend every time we need to add a new binding. Create a new method in the dictionary to add a new binding. Review the capitalization and the naming conventions to keep the code consistent.


/**
 * This function receives a linkId and a list of items and returns the item that matches the linkId, traversing the items recursively
 * @param key {string} - The linkId to search for
 * @param coverage {Coverage[]} - The list of coverages to read from
 */
const getInitialFromCoverage = (key: string, coverage?: Coverage[]): Initial => {
  const initial: Initial = { valueString: '' };

  const methods: { [key: string]: () => Initial } = {
    'Coverage.Payor': (): Initial => ({
      valueString: coverage?.[0]?.payor?.[0]?.display || '',
    }),
  }

  if (methods[key]) {
    return methods[key]() || '';
  }

  return initial;
};

Additional resources

Advantages of Using Correct Data Types

Using the correct data types in your project offers several advantages:

Medplum FHIR data types

Improved IDE Support

Enhanced Automated Tests

Easier Collaboration

Faster Development

Further Reading