Skip to main content
Glama
intake-utils.ts25.1 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { MedplumClient } from '@medplum/core'; import { addProfileToResource, createReference, getReferenceString, HTTP_HL7_ORG, HTTP_TERMINOLOGY_HL7_ORG, LOINC, SNOMED, } from '@medplum/core'; import type { Address, CodeableConcept, Coding, Consent, HumanName, Observation, Organization, Patient, Questionnaire, QuestionnaireItem, QuestionnaireResponse, QuestionnaireResponseItem, QuestionnaireResponseItemAnswer, Reference, } from '@medplum/fhirtypes'; export const PROFILE_URLS: Record<string, string> = { AllergyIntolerance: `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-allergyintolerance`, CareTeam: `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-careteam`, Coverage: `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-coverage`, Immunization: `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-immunization`, MedicationRequest: `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-medicationrequest`, Patient: `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-patient`, ObservationSexualOrientation: `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-observation-sexual-orientation`, ObservationSmokingStatus: `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-smokingstatus`, }; export const extensionURLMapping: Record<string, string> = { race: HTTP_HL7_ORG + '/fhir/us/core/StructureDefinition/us-core-race', ethnicity: HTTP_HL7_ORG + '/fhir/us/core/StructureDefinition/us-core-ethnicity', veteran: HTTP_HL7_ORG + '/fhir/us/military-service/StructureDefinition/military-service-veteran-status', }; export const observationCodeMapping: Record<string, CodeableConcept> = { housingStatus: { coding: [{ code: '71802-3', system: LOINC, display: 'Housing status' }] }, educationLevel: { coding: [{ code: '82589-3', system: LOINC, display: 'Highest Level of Education' }] }, smokingStatus: { coding: [{ code: '72166-2', system: LOINC, display: 'Tobacco smoking status' }] }, sexualOrientation: { coding: [{ code: '76690-7', system: LOINC, display: 'Sexual orientation' }] }, pregnancyStatus: { coding: [{ code: '82810-3', system: LOINC, display: 'Pregnancy status' }] }, estimatedDeliveryDate: { coding: [{ code: '11778-8', system: LOINC, display: 'Estimated date of delivery' }] }, }; export const observationCategoryMapping: Record<string, CodeableConcept> = { socialHistory: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/observation-category', code: 'social-history', display: 'Social History', }, ], }, sdoh: { coding: [ { system: HTTP_HL7_ORG + '/fhir/us/core/CodeSystem/us-core-tags', code: 'sdoh', display: 'SDOH', }, ], }, }; export const consentScopeMapping: Record<string, CodeableConcept> = { adr: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/consentscope', code: 'adr', display: 'Advanced Care Directive', }, ], }, patientPrivacy: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/consentscope', code: 'patient-privacy', display: 'Patient Privacy', }, ], }, treatment: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/consentscope', code: 'treatment', display: 'Treatment', }, ], }, }; export const consentCategoryMapping: Record<string, CodeableConcept> = { acd: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/consentcategorycodes', code: 'acd', display: 'Advanced Care Directive', }, ], }, nopp: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/v3-ActCode', code: 'nopp', display: 'Notice of Privacy Practices', }, ], }, pay: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/v3-ActCode', code: 'pay', display: 'Payment', }, ], }, med: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/v3-ActCode', code: 'med', display: 'Medical', }, ], }, }; export const consentPolicyRuleMapping: Record<string, CodeableConcept> = { hipaaNpp: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/consentpolicycodes', code: 'hipaa-npp', display: 'HIPAA Notice of Privacy Practices', }, ], }, hipaaSelfPay: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/consentpolicycodes', code: 'hipaa-self-pay', display: 'HIPAA Self-Pay Restriction', }, ], }, cric: { coding: [ { system: HTTP_TERMINOLOGY_HL7_ORG + '/CodeSystem/consentpolicycodes', code: 'cric', display: 'Common Rule Informed Consent', }, ], }, adr: { coding: [ { system: 'http://medplum.com', code: 'BasicADR', display: 'Advanced Care Directive', }, ], }, }; type ObservationQuestionnaireItemType = 'valueCodeableConcept' | 'valueDateTime'; /** * This function takes data about an Observation and creates or updates an existing * resource with the same patient and code. * * @param medplum - A Medplum client * @param patient - A Patient resource that will be stored as the subject * @param code - A code for the observation * @param category - A category for the observation * @param answerType - The value[x] field where the answer should be stored * @param value - The value to be stored in the observation * @param profileUrl - An optional profile URL to be added to the resource */ export async function upsertObservation( medplum: MedplumClient, patient: Patient, code: CodeableConcept, category: CodeableConcept, answerType: ObservationQuestionnaireItemType, value: QuestionnaireResponseItemAnswer | undefined, profileUrl?: string ): Promise<void> { if (!value || !code) { return; } let observation: Observation = { resourceType: 'Observation', status: 'final', subject: createReference(patient), code: code, category: [category], effectiveDateTime: new Date().toISOString(), }; if (profileUrl) { observation = addProfileToResource(observation, profileUrl); } if (answerType === 'valueCodeableConcept') { observation.valueCodeableConcept = { coding: [value], }; } else if (answerType === 'valueDateTime') { observation.valueDateTime = value.valueDateTime; } const coding = code.coding?.[0] as Coding; await medplum.upsertResource(observation, { code: `${coding.system}|${coding.code}`, subject: getReferenceString(patient), }); } type ExtensionQuestionnaireItemType = 'valueCoding' | 'valueBoolean'; /** * Add an extension to a patient * * @param patient - A patient resource * @param url - An URL that identifies the extension * @param answerType - The value[x] field where the answer should be stored * @param answer - The value to be stored in the extension * @param subExtensionKey - A key to identify a sub-extension */ export function addExtension( patient: Patient, url: string, answerType: ExtensionQuestionnaireItemType, answer: QuestionnaireResponseItemAnswer | undefined, subExtensionKey?: string ): void { let value = answer?.[answerType]; // Answer to boolean Questionnaire fields will be set as `undefined` if the check mark is not ticked // so in this case we should interpret it as `false`. if (answerType === 'valueBoolean') { value = Boolean(value); } if (value === undefined) { return; } patient.extension ||= []; if (subExtensionKey) { const subExtensions = [ { url: subExtensionKey, [answerType]: value, }, ]; if (answerType === 'valueCoding' && (value as Coding).display) { subExtensions.push({ url: 'text', valueString: (value as Coding).display as string }); } patient.extension.push({ url, extension: subExtensions, }); } else { patient.extension.push({ url, [answerType]: value, }); } } /** * Add a language to patient's communication attribute or set an existing one as preferred * * @param patient - The patient * @param valueCoding - A Coding with the language data * @param preferred - Whether this language should be set as preferred */ export function addLanguage(patient: Patient, valueCoding: Coding | undefined, preferred: boolean = false): void { if (!valueCoding) { return; } const patientCommunications = patient.communication ?? []; // Checks if the patient already has the language in their list of communications let language = patientCommunications.find( (communication) => communication.language.coding?.[0].code === valueCoding?.code ); // Add the language in case it's not set yet if (!language) { language = { language: { coding: [valueCoding], }, }; patientCommunications.push(language); } if (preferred) { language.preferred = preferred; } patient.communication = patientCommunications; } /** * Adds an AllergyIntolerance resource * * @param medplum - The Medplum client * @param patient - The patient beneficiary of the allergy * @param answers - A list of objects where the keys are the linkIds of the fields used to set up an */ export async function addAllergy( medplum: MedplumClient, patient: Patient, answers: Record<string, QuestionnaireResponseItemAnswer> ): Promise<void> { const code = answers['allergy-substance']?.valueCoding; if (!code) { return; } const reaction = answers['allergy-reaction']?.valueString; const onsetDateTime = answers['allergy-onset']?.valueDateTime; await medplum.upsertResource( { resourceType: 'AllergyIntolerance', meta: { profile: [PROFILE_URLS.AllergyIntolerance], }, clinicalStatus: { text: 'Active', coding: [ { system: 'http://hl7.org/fhir/ValueSet/allergyintolerance-clinical', code: 'active', display: 'Active', }, ], }, verificationStatus: { text: 'Unconfirmed', coding: [ { system: 'http://hl7.org/fhir/ValueSet/allergyintolerance-verification', code: 'unconfirmed', display: 'Unconfirmed', }, ], }, patient: createReference(patient), code: { coding: [code] }, reaction: reaction ? [{ manifestation: [{ text: reaction }] }] : undefined, onsetDateTime: onsetDateTime, }, { patient: getReferenceString(patient), code: `${code.system}|${code.code}`, } ); } /** * Adds a MedicationRequest resource * * @param medplum - The Medplum client * @param patient - The patient beneficiary of the medication * @param answers - A list of objects where the keys are the linkIds of the fields used to set up a * medication (see getGroupRepeatedAnswers) */ export async function addMedication( medplum: MedplumClient, patient: Patient, answers: Record<string, QuestionnaireResponseItemAnswer> ): Promise<void> { const code = answers['medication-code']?.valueCoding; if (!code) { return; } const note = answers['medication-note']?.valueString; await medplum.upsertResource( { resourceType: 'MedicationRequest', meta: { profile: [PROFILE_URLS.MedicationRequest], }, subject: createReference(patient), status: 'active', intent: 'order', requester: createReference(patient), medicationCodeableConcept: { coding: [code] }, note: note ? [{ text: note }] : undefined, }, { subject: getReferenceString(patient), code: `${code.system}|${code.code}`, } ); } /** * Adds a Condition resource * * @param medplum - The Medplum client * @param patient - The patient beneficiary of the condition * @param answers - A list of objects where the keys are the linkIds of the fields used to set up a * condition (see getGroupRepeatedAnswers) */ export async function addCondition( medplum: MedplumClient, patient: Patient, answers: Record<string, QuestionnaireResponseItemAnswer> ): Promise<void> { const code = answers['medical-history-problem']?.valueCoding; if (!code) { return; } const clinicalStatus = answers['medical-history-clinical-status']?.valueCoding; const onsetDateTime = answers['medical-history-onset']?.valueDateTime; await medplum.upsertResource( { resourceType: 'Condition', subject: createReference(patient), code: { coding: [code] }, clinicalStatus: clinicalStatus ? { coding: [clinicalStatus] } : undefined, onsetDateTime: onsetDateTime, }, { subject: getReferenceString(patient), code: `${code?.system}|${code?.code}`, } ); } /** * Adds a FamilyMemberHistory resource * * @param medplum - The Medplum client * @param patient - The patient beneficiary of the family member history * @param answers - A list of objects where the keys are the linkIds of the fields used to set up a * family member history (see getGroupRepeatedAnswers) */ export async function addFamilyMemberHistory( medplum: MedplumClient, patient: Patient, answers: Record<string, QuestionnaireResponseItemAnswer> ): Promise<void> { const condition = answers['family-member-history-problem']?.valueCoding; const relationship = answers['family-member-history-relationship']?.valueCoding; if (!condition || !relationship) { return; } const deceased = answers['family-member-history-deceased']?.valueBoolean; await medplum.upsertResource( { resourceType: 'FamilyMemberHistory', patient: createReference(patient), status: 'completed', relationship: { coding: [relationship] }, condition: [{ code: { coding: [condition] } }], deceasedBoolean: deceased, }, { patient: getReferenceString(patient), code: `${condition.system}|${condition.code}`, relationship: `${relationship.system}|${relationship.code}`, } ); } /** * * @param medplum - The Medplum client * @param patient - The patient beneficiary of the immunization * @param answers - A list of objects where the keys are the linkIds of the fields used to set up an * immunization (see getGroupRepeatedAnswers) */ export async function addImmunization( medplum: MedplumClient, patient: Patient, answers: Record<string, QuestionnaireResponseItemAnswer> ): Promise<void> { const code = answers['immunization-vaccine']?.valueCoding; const occurrenceDateTime = answers['immunization-date']?.valueDateTime; if (!code || !occurrenceDateTime) { return; } await medplum.upsertResource( { resourceType: 'Immunization', meta: { profile: [PROFILE_URLS.Immunization], }, status: 'completed', vaccineCode: { coding: [code] }, patient: createReference(patient), occurrenceDateTime: occurrenceDateTime, }, { status: 'completed', 'vaccine-code': `${code.system}|${code.code}`, patient: getReferenceString(patient), date: occurrenceDateTime, } ); } /** * Adds a CareTeam resource associating the patient with a pharmacy * * @param medplum - The Medplum client * @param patient - The patient beneficiary of the care team * @param pharmacy - The pharmacy to be added to the care team */ export async function addPharmacy( medplum: MedplumClient, patient: Patient, pharmacy: Reference<Organization> ): Promise<void> { await medplum.upsertResource( { resourceType: 'CareTeam', meta: { profile: [PROFILE_URLS.CareTeam], }, status: 'proposed', name: 'Patient Preferred Pharmacy', subject: createReference(patient), participant: [ { member: pharmacy, role: [{ coding: [{ system: SNOMED, code: '76166008', display: 'Practical aid (pharmacy) (occupation)' }] }], }, ], }, { name: 'Patient Preferred Pharmacy', subject: getReferenceString(patient), } ); } /** * Adds a Coverage resource * * @param medplum - The Medplum client * @param patient - The patient beneficiary of the coverage * @param answers - A list of objects where the keys are the linkIds of the fields used to set up a * coverage (see getGroupRepeatedAnswers) */ export async function addCoverage( medplum: MedplumClient, patient: Patient, answers: Record<string, QuestionnaireResponseItemAnswer> ): Promise<void> { const payor = answers['insurance-provider'].valueReference as Reference<Organization>; const subscriberId = answers['subscriber-id'].valueString; const relationshipToSubscriber = answers['relationship-to-subscriber'].valueCoding as Coding; // Create RelatedPerson resource depending on the relationship to the subscriber const relatedPersonAnswers = answers['related-person'] as Record<string, QuestionnaireResponseItemAnswer>; if ( relationshipToSubscriber.code && !['other', 'self', 'injured'].includes(relationshipToSubscriber.code) && relatedPersonAnswers ) { const name = getHumanName(relatedPersonAnswers, 'related-person-'); const relatedPersonRelationship = getRelatedPersonRelationshipFromCoverage(relationshipToSubscriber); await medplum.createResource({ resourceType: 'RelatedPerson', patient: createReference(patient), relationship: relatedPersonRelationship ? [{ coding: [relatedPersonRelationship] }] : undefined, name: name ? [name] : undefined, birthDate: relatedPersonAnswers['related-person-dob']?.valueDate, gender: (relatedPersonAnswers['related-person-gender-identity']?.valueCoding?.code as Patient['gender']) ?? undefined, }); } await medplum.upsertResource( { resourceType: 'Coverage', meta: { profile: [PROFILE_URLS.Coverage], }, status: 'active', beneficiary: createReference(patient), subscriberId: subscriberId, relationship: { coding: [relationshipToSubscriber] }, payor: [payor], }, { beneficiary: getReferenceString(patient), payor: getReferenceString(payor), } ); } export async function addConsent( medplum: MedplumClient, patient: Patient, consentGiven: boolean, scope: CodeableConcept, category: CodeableConcept, policyRule: CodeableConcept | undefined, date: Consent['dateTime'] ): Promise<void> { await medplum.createResource({ resourceType: 'Consent', patient: createReference(patient), status: consentGiven ? 'active' : 'rejected', scope: scope, category: [category], policyRule: policyRule, dateTime: date, }); } /** * Finds a QuestionnaireItem or QuestionnaireResponseItem with the given linkId * * @param items - The array of objects present in the `.item` attribute of a Questionnaire or QuestionnaireResponse * @param linkId - The id to be found * @returns - The found item or undefined in case it's not found */ export function findQuestionnaireItem<T extends QuestionnaireItem | QuestionnaireResponseItem>( items: T[] | undefined, linkId: string ): T | undefined { if (!items) { return undefined; } return items.reduce((foundItem: T | undefined, currentItem: T | undefined) => { // If currentItem is undefined or the item was already found just return it if (foundItem || !currentItem) { return foundItem; } if (currentItem.linkId === linkId) { return currentItem; } else if (currentItem.item) { // This enables traversing nested structures return findQuestionnaireItem(currentItem.item as T[], linkId); } return undefined; }, undefined); } /** * Finds the answers to a group of items that can be repeated. * * @param questionnaire - The Questionnaire resource, used to find the list of linkIds that will compose each group * @param response - The QuestionnaireResponse resource where the answers will be extracted * @param groupLinkId - The linkId of the group that can be repeated in the questionnaire * @returns - An array of objects each containing a set of grouped answers */ export function getGroupRepeatedAnswers( questionnaire: Questionnaire, response: QuestionnaireResponse, groupLinkId: string ): Record<string, QuestionnaireResponseItemAnswer>[] { // Find the questionnaire item based on groupLinkId const questionnaireItem = findQuestionnaireItem(questionnaire.item, groupLinkId) as QuestionnaireItem; if (questionnaireItem.type !== 'group' || !questionnaireItem?.item) { return []; } // Get all response items corresponding to the groupLinkId const responseGroups = response.item?.filter((item) => item.linkId === groupLinkId); if (!responseGroups || responseGroups?.length === 0) { return []; } // It expects that responses will be organized in separate groups with the same groupLinkId. Eg.: // [ // {group-linkId-1: [{answer-with-linkId-1}, {answer-with-linkId-2}, {answer-with-linkId-3}]}, // {group-linkId-1: [{answer-with-linkId-1}, {answer-with-linkId-2}, {answer-with-linkId-3}]} // ] // This function handles scenarios where each group can have some fields filled and some not, while keeping the order intact. Eg.: // [ // {group-linkId-1: [{answer-with-linkId-1}, {answer-with-linkId-3}]}, // {group-linkId-1: [{answer-with-linkId-1}, {answer-with-linkId-2}, {answer-with-linkId-3}]} // ] const groupAnswers = responseGroups.map((responseItem) => { const answers: Record<string, any> = {}; const extractAnswers = (items: QuestionnaireResponseItem[]): void => { items.forEach(({ linkId, answer, item }) => { if (item) { const subGroupAnswers: Record<string, any> = {}; item.forEach((subItem) => { if (subItem.answer) { subGroupAnswers[subItem.linkId] = subItem.answer?.[0] ?? {}; } }); answers[linkId] = subGroupAnswers; } else { answers[linkId] = answer?.[0] ?? {}; } }); }; extractAnswers(responseItem.item || []); return answers; }); return groupAnswers; } export function convertDateToDateTime(date: string | undefined): string | undefined { if (!date) { return undefined; } return new Date(date).toISOString(); } export function getHumanName( answers: Record<string, QuestionnaireResponseItemAnswer>, prefix: string = '' ): HumanName | undefined { const patientName: HumanName = {}; const givenName = []; if (answers[`${prefix}first-name`]?.valueString) { givenName.push(answers[`${prefix}first-name`].valueString as string); } if (answers[`${prefix}middle-name`]?.valueString) { givenName.push(answers[`${prefix}middle-name`].valueString as string); } if (givenName.length > 0) { patientName.given = givenName; } if (answers[`${prefix}last-name`]?.valueString) { patientName.family = answers[`${prefix}last-name`].valueString; } return Object.keys(patientName).length > 0 ? patientName : undefined; } export function getPatientAddress(answers: Record<string, QuestionnaireResponseItemAnswer>): Address | undefined { const patientAddress: Address = {}; if (answers['street']?.valueString) { patientAddress.line = [answers['street'].valueString]; } if (answers['city']?.valueString) { patientAddress.city = answers['city'].valueString; } if (answers['state']?.valueCoding?.code) { patientAddress.state = answers['state'].valueCoding.code; } if (answers['zip']?.valueString) { patientAddress.postalCode = answers['zip'].valueString; } // To simplify the demo, we're assuming the address is always a home address return Object.keys(patientAddress).length > 0 ? { use: 'home', type: 'physical', ...patientAddress } : undefined; } function getRelatedPersonRelationshipFromCoverage(coverageRelationship: Coding): Coding | undefined { // Basic relationship mapping between Coverage and RelatedPerson // Coverage.relationship: http://hl7.org/fhir/ValueSet/subscriber-relationship // RelatedPerson.relationship: http://hl7.org/fhir/ValueSet/relatedperson-relationshiptype switch (coverageRelationship.code) { case 'child': return { system: 'http://terminology.hl7.org/CodeSystem/v3-RoleCode', code: 'PRN', display: 'parent', }; case 'parent': return { system: 'http://terminology.hl7.org/CodeSystem/v3-RoleCode', code: 'CHILD', display: 'child' }; case 'spouse': case 'common': return { system: 'http://terminology.hl7.org/CodeSystem/v3-RoleCode', code: 'SPS', display: 'spouse' }; default: return undefined; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server