Skip to content

Commit

Permalink
Merge from main repo: Release Version 2.1.4 (#2968)
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions[bot] committed Oct 3, 2023
1 parent 3e51cd0 commit 2465afd
Show file tree
Hide file tree
Showing 15 changed files with 265 additions and 205 deletions.
2 changes: 1 addition & 1 deletion .turbo/turbo-build.log
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

> [email protected].3 build
> [email protected].4 build
> tsc

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "medplum-demo-bots",
"version": "2.1.3",
"version": "2.1.4",
"description": "Medplum Demo Bots",
"scripts": {
"build": "tsc",
Expand All @@ -13,11 +13,11 @@
"author": "Medplum <[email protected]>",
"license": "Apache-2.0",
"devDependencies": {
"@medplum/cli": "2.1.3",
"@medplum/core": "2.1.3",
"@medplum/eslint-config": "2.1.3",
"@medplum/fhirtypes": "2.1.3",
"@medplum/mock": "2.1.3",
"@medplum/cli": "2.1.4",
"@medplum/core": "2.1.4",
"@medplum/eslint-config": "2.1.4",
"@medplum/fhirtypes": "2.1.4",
"@medplum/mock": "2.1.4",
"@types/node": "20.6.2",
"@types/node-fetch": "2.6.5",
"@types/ssh2-sftp-client": "9.0.0",
Expand Down
11 changes: 7 additions & 4 deletions src/candid-health/send-to-candid.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
CPT,
createReference,
getReferenceString,
ICD10,
indexSearchParameterBundle,
indexStructureDefinitionBundle,
MedplumClient,
SNOMED,
} from '@medplum/core';
import { readJson } from '@medplum/definitions';
import { Bundle, Coverage, Encounter, Patient, SearchParameter } from '@medplum/fhirtypes';
Expand Down Expand Up @@ -87,12 +90,12 @@ describe('Candid Health Tests', () => {
{
coding: [
{
system: 'https://snomed.info/sct',
system: SNOMED,
code: '394701000',
display: 'Asthma follow-up',
},
{
system: 'https://www.ama-assn.org/go/cpt',
system: CPT,
code: '99213',
display: 'Established patient office visit, 20-29 minutes',
},
Expand Down Expand Up @@ -128,12 +131,12 @@ describe('Candid Health Tests', () => {
{
coding: [
{
system: 'https://snomed.info/sct',
system: SNOMED,
code: '195967001',
display: 'Asthma',
},
{
system: 'https://hl7.org/fhir/sid/icd-10',
system: ICD10,
code: 'J45.5',
display: 'Severe persistent asthma',
},
Expand Down
6 changes: 3 additions & 3 deletions src/candid-health/send-to-candid.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BotEvent, getCodeBySystem, getIdentifier, getReferenceString, MedplumClient } from '@medplum/core';
import { BotEvent, CPT, getCodeBySystem, getIdentifier, getReferenceString, ICD10, MedplumClient } from '@medplum/core';
import {
Address,
Coverage,
Expand Down Expand Up @@ -92,7 +92,7 @@ export async function handler(medplum: MedplumClient, event: BotEvent<Encounter>
place_of_service_code: '10',
service_lines: [
{
procedure_code: encounter.type?.[0] && getCodeBySystem(encounter.type?.[0], 'https://www.ama-assn.org/go/cpt'),
procedure_code: encounter.type?.[0] && getCodeBySystem(encounter.type?.[0], CPT),
quantity: '1',
units: 'MJ',
charge_amount_cents: 10000,
Expand Down Expand Up @@ -304,7 +304,7 @@ function convertDiagnoses(encounter: Encounter): any[] {
}

for (const reason of encounter.reasonCode) {
const code = reason.coding?.find((c) => c.system === 'https://hl7.org/fhir/sid/icd-10');
const code = reason.coding?.find((c) => c.system === ICD10);
if (code) {
result.push({
code_type: 'ABK',
Expand Down
16 changes: 11 additions & 5 deletions src/finalize-reports.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { createReference, indexSearchParameterBundle, indexStructureDefinitionBundle } from '@medplum/core';
import {
LOINC,
UCUM,
createReference,
indexSearchParameterBundle,
indexStructureDefinitionBundle,
} from '@medplum/core';
import { readJson } from '@medplum/definitions';
import { Bundle, DiagnosticReport, Observation, Patient, SearchParameter } from '@medplum/fhirtypes';
import { MockClient } from '@medplum/mock';
Expand Down Expand Up @@ -38,7 +44,7 @@ describe('Finalize Report', async () => {
code: {
coding: [
{
system: 'https://loinc.org',
system: LOINC,
code: '39156-5',
display: 'Body Mass Index',
},
Expand All @@ -48,7 +54,7 @@ describe('Finalize Report', async () => {
valueQuantity: {
value: 24.5,
unit: 'kg/m2',
system: 'https://unitsofmeasure.org',
system: UCUM,
code: 'kg/m2',
},
});
Expand Down Expand Up @@ -103,7 +109,7 @@ describe('Finalize Report', async () => {
code: {
coding: [
{
system: 'https://loinc.org',
system: LOINC,
code: '39156-5',
display: 'Body Mass Index',
},
Expand All @@ -113,7 +119,7 @@ describe('Finalize Report', async () => {
valueQuantity: {
value: 24.5,
unit: 'kg/m2',
system: 'https://unitsofmeasure.org',
system: UCUM,
code: 'kg/m2',
},
});
Expand Down
190 changes: 127 additions & 63 deletions src/health-gorilla/receive-from-health-gorilla.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { allOk, BotEvent, getIdentifier, getReferenceString, MedplumClient, normalizeErrorString } from '@medplum/core';
import {
allOk,
BotEvent,
convertContainedResourcesToBundle,
getIdentifier,
getReferenceString,
MedplumClient,
normalizeErrorString,
} from '@medplum/core';
import {
Account,
Bundle,
BundleEntryRequest,
DiagnosticReport,
Observation,
OperationOutcome,
Expand All @@ -27,81 +37,122 @@ const HEALTH_GORILLA_SYSTEM = 'https://www.healthgorilla.com';

const referenceMap = new Map<string, string>();

/**
* Main entry point for the Health Gorilla webhook bot.
*
* This bot receives a Health Gorilla resource and syncs it to Medplum.
*
* Health Gorilla uses "contained resources" to represent related resources.
*
* In Medplum, we recommend that you use references instead of contained resources,
* which is often better for data quality and analytics.
*
* The majority of this bot is dedicated to rewriting references from Health Gorilla.
*
* @param medplum The Medplum client.
* @param event The Bot execution event with a Health Gorilla resource.
* @returns Returns OK OperationOutcome on success, or an error OperationOutcome on failure.
*/
export async function handler(
medplum: MedplumClient,
event: BotEvent<HealthGorillaResource>
): Promise<OperationOutcome> {
try {
const resource = event.input;
const resource = event.input;

if (resource.contained) {
for (const containedResource of resource.contained) {
await syncResource(medplum, containedResource as HealthGorillaResource, true);
}
// Move the Health Gorilla resource ID to an identifier
if (resource.id) {
const identifiers = resource.identifier ?? [];
identifiers.push({ system: HEALTH_GORILLA_SYSTEM, value: resource.id });
resource.identifier = identifiers;
}

// Remove contained resources before syncing the parent resource
resource.contained = undefined;
}
// Convert the Health Gorilla resource to a Medplum bundle.
// This moves contained resources to separate create/update operations.
const healthGorillaBundle = convertContainedResourcesToBundle(resource) as Bundle<HealthGorillaResource>;

// Touch up the bundle before executing
// This adds identifiers and ifNoneExist to the bundle entries
touchUpBundle(healthGorillaBundle);

await syncResource(medplum, resource);
try {
// Rewrite references to other resources
// For example, convert references to Patient and Organization resources
// from Health Gorilla references to Medplum references
await rewriteReferences(medplum, healthGorillaBundle);

// Execute the bundle
const result = await medplum.executeBatch(healthGorillaBundle);
for (const entry of result.entry ?? []) {
if (entry.response) {
console.log(entry.response.status, entry.response.location);
}
}
} catch (err) {
console.log(normalizeErrorString(err));
}

return allOk;
}

export async function syncResource<T extends HealthGorillaResource>(
medplum: MedplumClient,
healthGorillaResource: T,
contained = false
): Promise<void> {
if (healthGorillaResource.resourceType === 'Account' && !healthGorillaResource.status) {
// Health Gorilla drops Account.status which is a required field
healthGorillaResource.status = 'active';
}

// Rewrite references to other resources
// For example, convert references to Patient and Organization resources
// from Health Gorilla references to Medplum references
await rewriteReferences(medplum, healthGorillaResource);

let existingResource: HealthGorillaResource | undefined = undefined;
let healthGorillaId: string | undefined = undefined;
/**
* Touches up the bundle before executing.
*
* As part of the conversion from Health Gorilla to Medplum, we need to add identifiers to the resources,
* so that we can connect/reuse resources that already exist in Medplum.
*
* We also take advantage of the "ifNoneExist" feature of FHIR to avoid creating duplicate resources.
*
* @param bundle The Health Gorilla bundle.
*/
function touchUpBundle(bundle: Bundle<HealthGorillaResource>): void {
for (const entry of bundle.entry ?? []) {
const resource = entry.resource as HealthGorillaResource;
const request = entry.request as BundleEntryRequest;

if (resource.resourceType === 'Account') {
// In Health Gorilla, Account connects a Patient and a payment method
// So we use the combination of those references as the Account identifier
const identifier = resource.guarantor?.[0]?.party?.reference + '-' + resource?.type?.coding?.[0]?.code;
if (!resource.identifier) {
resource.identifier = [];
}
resource.identifier.push({ system: HEALTH_GORILLA_SYSTEM, value: identifier });
request.ifNoneExist = 'identifier=' + identifier;
}

const mergeResourceTypes: HealthGorillaResourceType[] = ['Account'];
if (mergeResourceTypes.includes(healthGorillaResource.resourceType)) {
// For some resource types, we attempt a "merge" operation with existing resources.
// For other resource types, we always create a new resource.
healthGorillaId = getHealthGorillaId(healthGorillaResource, contained) as string;
existingResource = await searchByHealthGorillaId(medplum, healthGorillaResource.resourceType, healthGorillaId);
}
if (resource.resourceType === 'PractitionerRole') {
// In Health Gorilla, PractitionerRole connects a Practitioner and an Organization
// So we use the combination of those references as the PractitionerRole identifier
const identifier = resource.practitioner?.reference + '-' + resource.organization?.reference;
if (!resource.identifier) {
resource.identifier = [];
}
resource.identifier.push({ system: HEALTH_GORILLA_SYSTEM, value: identifier });
request.ifNoneExist = 'identifier=' + identifier;
}

if (existingResource) {
// Update the existing resource
const updatedResource = await medplum.updateResource({
...healthGorillaResource,
id: existingResource.id,
identifier: existingResource.identifier,
});
console.log('Updated', updatedResource.resourceType, updatedResource.id);
if (healthGorillaResource.id) {
referenceMap.set('#' + healthGorillaResource.id, getReferenceString(updatedResource));
if (resource.resourceType === 'RequestGroup' || resource.resourceType === 'DiagnosticReport') {
const identifier = getIdentifier(resource, HEALTH_GORILLA_SYSTEM);
if (identifier) {
request.ifNoneExist = 'identifier=' + identifier;
}
}
} else {
// Create a new resource
const createdResource = await medplum.createResource({
...healthGorillaResource,
id: undefined,
identifier: [{ system: HEALTH_GORILLA_SYSTEM, value: healthGorillaId }],
});
console.log('Created', createdResource.resourceType, createdResource.id);
if (healthGorillaResource.id) {
referenceMap.set('#' + healthGorillaResource.id, getReferenceString(createdResource));

if (resource.resourceType === 'ServiceRequest') {
const requisition = resource.requisition?.value;
if (requisition) {
request.ifNoneExist = 'requisition=' + requisition;
}
}
}
}

/**
* Rewrites Health Gorilla references to Medplum references.
*
* @param medplum The Medplum client.
* @param value An unknown value.
*/
async function rewriteReferences(medplum: MedplumClient, value: unknown): Promise<void> {
if (!value) {
return;
Expand Down Expand Up @@ -145,17 +196,30 @@ async function rewriteReferencesInObject(medplum: MedplumClient, obj: Record<str
}
}

function getHealthGorillaId(resource: HealthGorillaResource, contained: boolean): string | undefined {
if (!contained && resource.id) {
return resource.id;
}
return getIdentifier(resource, HEALTH_GORILLA_SYSTEM);
}

/**
* Tries to find a Medplum resource by Health Gorilla ID.
*
* In most cases, this is a matter of search by "identifier" rather than "id".
*
* There are some special cases where "identifier" is not available.
*
* @param medplum The Medplum client.
* @param resourceType The FHIR resource type.
* @param id The Health Gorilla resource ID.
* @returns The Medplum resource, or undefined if not found.
*/
async function searchByHealthGorillaId(
medplum: MedplumClient,
resourceType: HealthGorillaResourceType,
id: string
): Promise<HealthGorillaResource | undefined> {
return medplum.searchOne(resourceType, { identifier: id });
if (resourceType === 'ServiceRequest') {
// Special case for ServiceRequest - search by requisition instead of identifier
// Because Health Gorilla does not include the identifier
const requisition = id.split('-')[0];
return medplum.searchOne(resourceType, { requisition });
}

// Default case - search by identifier
return medplum.searchOne(resourceType, { identifier: `${HEALTH_GORILLA_SYSTEM}|${id}` });
}
Loading

0 comments on commit 2465afd

Please sign in to comment.