Skip to main content
Glama
send-to-candid.ts10.9 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { BotEvent, MedplumClient } from '@medplum/core'; import { CPT, getCodeBySystem, getIdentifier, getReferenceString, ICD10 } from '@medplum/core'; import type { Address, Coverage, CoverageClass, Encounter, Organization, Patient, Practitioner, Reference, } from '@medplum/fhirtypes'; import fetch from 'node-fetch'; const CANDID_API_URL = 'https://api-staging.joincandidhealth.com/api/v1/'; export async function handler(medplum: MedplumClient, event: BotEvent<Encounter>): Promise<any> { const encounter = event.input; // Read the Patient const patient: Patient = await medplum.readReference(encounter.subject as Reference<Patient>); // Encounter.serviceProvider represents the organization that is primarily responsible for this Encounter's services if (!encounter.serviceProvider) { throw new Error('Missing Service Provider'); } const serviceFacility: Organization = await medplum.readReference(encounter.serviceProvider); // Encounter.participant lists all the providers who were part of this encounter. // Here we filter to the primary performer. See the // [participant-type valueset](https://hl7.org/fhir/valueset-encounter-participant-type.html) for more options if (!encounter?.participant || encounter.participant.length === 0) { throw new Error('Missing provider'); } const providerRef = encounter.participant.find( (participant) => participant?.type?.[0] && getCodeBySystem(participant.type[0], 'http://terminology.hl7.org/CodeSystem/v3-ParticipationType') === 'PPRF' )?.individual as Reference<Practitioner>; const provider: Practitioner = await medplum.readReference(providerRef); // Read the Coverage resource, which contains insurance details for the patient const coverage = await medplum.searchOne('Coverage', `subscriber=${getReferenceString(patient)}`); if (!coverage) { throw new Error('Missing Coverage'); } // Craft the Candid CodedEncounter Object const { external_id: _, ...subscriber } = convertPatient(patient); const candidCodedEncounter = { external_id: getReferenceString(encounter), // Use the `Encounter/<id>` as the external id date_of_service: extractDate(encounter.period?.start), end_date_of_service: extractDate(encounter.period?.end), patient_authorized_release: true, benefits_assigned_to_provider: true, provider_accepts_assignment: true, appointment_type: encounter.type?.[0]?.text || '', do_not_bill: false, // In this example, assume the billing and rendering providers are the same billing_provider: { first_name: provider.name?.[0]?.given?.[0], last_name: provider.name?.[0]?.family, address: convertAddress(provider.address?.[0]), tax_id: getIdentifier(provider, 'http://hl7.org/fhir/sid/us-ssn'), npi: getIdentifier(provider, 'http://hl7.org/fhir/sid/us-npi'), }, rendering_provider: { first_name: provider.name?.[0]?.given?.[0], last_name: provider.name?.[0]?.family, address: convertAddress(provider.address?.[0]), npi: getIdentifier(provider, 'http://hl7.org/fhir/sid/us-npi'), }, // Copy the information about the service facility service_facility: { organization_name: serviceFacility.name, address: convertAddress(serviceFacility.address?.[0]), }, pay_to_address: convertAddress(serviceFacility.address?.[0]), patient: convertPatient(patient), subscriber_primary: { ...subscriber, // '18' - 'Self' (see Candid Health API docs) patient_relationship_to_subscriber_code: '18', insurance_card: convertInsuranceCard(coverage), }, diagnoses: convertDiagnoses(encounter), // '10' - '' (see Candid Health API docs) place_of_service_code: '10', service_lines: [ { procedure_code: encounter.type?.[0] && getCodeBySystem(encounter.type?.[0], CPT), quantity: '1', units: 'MJ', charge_amount_cents: 10000, diagnosis_pointers: [0], }, ], synchronicity: 'Synchronous', }; const result = await submitCandidEncounter( candidCodedEncounter, event.secrets['CANDID_API_KEY'].valueString as string, event.secrets['CANDID_API_SECRET'].valueString as string ); console.log('Received Response from Candid:\n', JSON.stringify(result, null, 2)); return result; } /** * Authenticates into the Candid Health API using API key and API secret, and posts the CodedEncounter object to * Candid's /v1/coded_encounters endpoint * @param candidCodedEncounter - A JS representation of the CodedEncounter object * @param apiKey - Candid Health API Key * @param apiSecret - Candid Health API Secret * @returns The Candid Health API response */ async function submitCandidEncounter(candidCodedEncounter: any, apiKey: string, apiSecret: string): Promise<any> { // Get a Bearer Token const authResponse = await fetch(CANDID_API_URL + 'auth/token', { method: 'post', body: JSON.stringify({ client_id: apiKey, client_secret: apiSecret, }), headers: { 'Content-Type': 'application/json' }, }); const bearerToken = ((await authResponse.json()) as any).access_token; // Send the CodedEncounter const encounterResponse = await fetch(CANDID_API_URL + '/coded_encounters', { method: 'post', body: JSON.stringify(candidCodedEncounter), headers: { 'Content-Type': 'application/json', authorization: `Bearer ${bearerToken}` }, }); const candidEncounterResult = await encounterResponse.json(); return candidEncounterResult; } /** * Converts a FHIR patient to a Candid Health patient * @param patient - The FHIR patient. * @returns The Candid Health patient. */ function convertPatient(patient: Patient | undefined): any { if (!patient) { return undefined; } return { first_name: patient.name?.[0]?.given?.[0], last_name: patient.name?.[0]?.family, gender: convertGender(patient.gender), external_id: getReferenceString(patient), date_of_birth: patient.birthDate, address: convertAddress(patient.address?.[0]), }; } function convertInsuranceCard(coverage: Coverage | undefined): object | undefined { if (!coverage) { return undefined; } return { group_number: findCoverageClass(coverage, 'group')?.value, plan_name: findCoverageClass(coverage, 'group')?.name, plan_type: convertCoverageType(coverage.type), insurance_type: 'string', member_id: coverage.identifier?.[0]?.value, payer_name: 'string', payer_id: '00019', rx_bin: findCoverageClass(coverage, 'rxbin')?.value, rx_pcn: findCoverageClass(coverage, 'rxpcn')?.value, image_url_front: 'string', image_url_back: 'string', }; } // https://www.nahdo.org/sites/default/files/2020-12/SourceofPaymentTypologyVersion9_2%20-Dec%2011_2020_Final2.pdf // https://build.fhir.org/ig/HL7/US-Core/Coverage-coverage-example.json.html // Convert the Coverage.type field to the Candid Health Source of Payment code // Assume the that coverage is the in the standard [NAHDO Source of Payment Typology](https://www.nahdo.org/sopt) function convertCoverageType(coverageType: Coverage['type']): string { if (!coverageType) { return 'not_given'; } const code = getCodeBySystem(coverageType, 'https://nahdo.org/sopt'); if (!code) { return 'not_given'; } // Self-pay if (code === '81') { return '09'; } // 11 - Other Non-Federal Programs // 12 - Preferred Provider Organization (PPO) if (code === '512') { return '12'; } // 13 - Point of Service (POS) if (code === '513') { return '13'; } // 14 - Exclusive Provider Organization (EPO) // 15 - Indemnity Insurance if (code.startsWith('52') || code.startsWith('53')) { return '15'; } // 16 - Health Maintenance Organization (HMO) Medicare Risk if (code === '111') { return '16'; } // 17 - Dental Maintenance Organization if (['561', '517'].includes(code)) { return '17'; } // AM - Automobile Medical if (code === '96') { return '13'; } // BL - Blue Cross/Blue Shield if (code.startsWith('6')) { return 'BL'; } // CH - CHAMPUS if (code.startsWith('311')) { return 'CH'; } // CI - Commercial Insurance Co. // DS - Disability if (code === '93') { return 'DS'; } // FI - Federal Employees Program if (code === '391') { return 'FI'; } // HM - Health Maintenance Organization (HMO) if (code === '511') { return 'HM'; } // LM - Liability Medical if (code === '97') { return 'LM'; } // MA - Medicare Part A // MB - Medicare Part B // MC - Medicaid if (code.startsWith('2')) { return 'MC'; } // OF - Other Federal Program // TV - Title V if (code === '341') { return 'MC'; } // VA - Veterans Affairs Plan if (code.startsWith('32')) { return 'VA'; } // WC - Workers' Compensation Health Claim if (code.startsWith('95')) { return 'VA'; } // ZZ - Mutually Defined return 'not_given'; } // Read the diagnosis from the Encounter.reasonCode field. // Assume that the diagnosis is represented as a Cove function convertDiagnoses(encounter: Encounter): any[] { const result: any[] = []; if (!encounter.reasonCode) { return result; } for (const reason of encounter.reasonCode) { const code = reason.coding?.find((c) => c.system === ICD10); if (code) { result.push({ code_type: 'ABK', code: code.code, name: code.display || '', }); } } return result; } /* Data Type Conversions */ function convertAddress(address: Address | undefined): object | undefined { if (!address) { return undefined; } return { address1: address?.line?.[0], address2: address?.line?.[1] || '', city: address?.city, state: address?.state, zip_code: address?.postalCode?.split('-')?.[0], zip_plus_four_code: address?.postalCode?.split('-')?.[1], }; } function convertGender(fhirGender: Patient['gender']): string { if (!fhirGender) { return 'not_given'; } return fhirGender; } // Extract the date part of an ISO formatted date string function extractDate(date: string | undefined): string | undefined { if (!date) { return undefined; } return date.split('T')[0]; } // Find the first value of a Coverage's classification with a given class type // The Coverage.class field contains a suite of underwriter specific classifiers, including: group, plan, rxbin, etc. function findCoverageClass(coverage: Coverage, type: string): CoverageClass | undefined { return coverage.class?.find( (klass) => klass.type && getCodeBySystem(klass.type, 'http://terminology.hl7.org/CodeSystem/coverage-class') === type ); }

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