Skip to main content
Glama
cms1500pdf.ts14.1 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { formatAddress, formatCodeableConcept, formatMoney, formatQuantity, getDisplayString, HTTP_HL7_ORG, } from '@medplum/core'; import type { Address, Claim, HumanName, Practitioner, RelatedPerson } from '@medplum/fhirtypes'; import type { Content, TDocumentDefinitions } from 'pdfmake/interfaces'; import { getAuthenticatedContext } from '../../../context'; import { imageData } from './cms1500.png'; const PAGE_WIDTH = 612; const PAGE_HEIGHT = 792; const insuranceTypes = new Set<string>([ 'MEDICARE', 'MEDICAID', 'TRICARE', 'CHAMPVA', 'GROUP HEALTH PLAN', 'FECA BLK LUNG', 'OTHER', ]); /** * Creates a PDF document definition from a Claim resource. * @param claim - The Claim resource. * @returns The PDF document definition. */ export async function getClaimPDFDocDefinition(claim: Claim): Promise<TDocumentDefinitions> { // Fetch all referenced resources const { repo } = getAuthenticatedContext(); const patient = await repo.readReference(claim.patient); const coverage = await repo.readReference(claim.insurance[0].coverage); const insured = coverage.subscriber ? await repo.readReference(coverage.subscriber) : undefined; const insurer = await repo.readReference(coverage.payor[0]); const provider = await repo.readReference(claim.provider); const otherCoverage = claim.insurance.length > 1 ? await repo.readReference(claim.insurance[1].coverage) : undefined; const otherInsured = otherCoverage?.subscriber ? await repo.readReference(otherCoverage.subscriber) : undefined; const referralRequest = claim.referral ? await repo.readReference(claim.referral) : undefined; const referrer = referralRequest?.requester ? await repo.readReference(referralRequest.requester) : undefined; const insuranceType = coverage.type?.coding?.[0].code ?? coverage.type?.coding?.[0].display ?? ''; const relationship = (insured as RelatedPerson | undefined)?.relationship?.[0]?.coding?.[0].code ?? 'self'; const relatedToEmployment = claim.supportingInfo?.some((info) => info.category.coding?.[0].code === 'employmentimpacted') ?? false; const relatedToAutoAccident = claim.accident?.type?.coding?.some((code) => code.code === 'MVA') ?? false; const accidentLocationState = claim.accident?.locationAddress?.state; const relatedToOtherAccident = !relatedToAutoAccident && !!claim.accident; const dateOfCurrentIllness = claim.supportingInfo?.find( (info) => info.category.coding?.[0].code === 'onset' )?.timingDate; const employmentImpacted = claim.supportingInfo?.find( (info) => info.category.coding?.[0].code === 'employmentimpacted' ); const hospitalization = claim.supportingInfo?.find((info) => info.category.coding?.[0].code === 'hospitalized'); const priorAuthRefNumber = claim.insurance[0]?.preAuthRef?.[0]; const outsideLab = claim.supportingInfo?.find((info) => info.category.coding?.[0].code === 'outsidelab'); const outsideLabCharges = outsideLab ? formatQuantity(outsideLab.valueQuantity) : ''; const diagnosis = (claim.diagnosis ?? []) .map( (d) => d.diagnosisCodeableConcept?.coding?.find((code) => code.system === `${HTTP_HL7_ORG}/fhir/sid/icd-10`)?.code ) .filter(Boolean) as string[]; const resubmissionCode = claim.related?.[0].relationship?.coding?.find((code) => code.code === 'prior')?.display; const originalReference = claim.related?.[0].claim?.display; const patientAccountNumber = claim.supportingInfo?.find( (info) => info.category?.coding?.find((code) => code.code === 'info') && info.code?.coding?.find((code) => code.code === 'patientaccount') )?.valueString; const patientPaid = formatQuantity( claim.supportingInfo?.find( (info) => info.category?.coding?.find((code) => code.code === 'info') && info.code?.coding?.find((code) => code.code === 'patientpaid') )?.valueQuantity ); const taxIdentifier = insurer.identifier?.find((id) => id.type?.coding?.find((code) => code.code === 'TAX')); const docDefinition: TDocumentDefinitions = { defaultStyle: { font: 'Helvetica', fontSize: 8, }, pageSize: { width: PAGE_WIDTH, height: PAGE_HEIGHT }, pageMargins: 0, content: [ { image: imageData, absolutePosition: { x: 0, y: 0 }, width: PAGE_WIDTH, height: PAGE_HEIGHT, }, // 1. Insurance Type createCheckmark(insuranceType === 'MEDICARE', 23, 111), createCheckmark(insuranceType === 'MEDICAID', 71, 111), createCheckmark(insuranceType === 'TRICARE', 122, 111), createCheckmark(insuranceType === 'CHAMPVA', 187, 111), createCheckmark(insuranceType === 'GROUP HEALTH PLAN', 237, 111), createCheckmark(insuranceType === 'FECA BLK LUNG', 295, 111), createCheckmark(insuranceType === 'OTHER' || !insuranceTypes.has(insuranceType), 338, 111), // 1a. Insured's ID Number createText(coverage.identifier?.find((id) => id.use === 'official')?.value, 374, 111), // 2. Patient's Name createText(formatHumanName(patient.name?.[0]), 22, 135), // 3. Patient's Birth Date (MM/DD/YYYY) createDate(patient.birthDate, 236, 135), // Patient's sex createCheckmark(patient.gender === 'male', 316, 135), createCheckmark(patient.gender === 'female', 352, 135), // 4. Insured's Name createText(formatHumanName(insured?.name?.[0]), 374, 135), // 5. Patient's Address createText(patient.address?.[0]?.line?.join(', '), 22, 159), createText(patient.address?.[0]?.city, 22, 182), createText(patient.address?.[0]?.state, 203, 182), createText(patient.address?.[0]?.postalCode, 22, 207), createText(getPhoneAreaCode(patient.telecom?.find((cp) => cp.system === 'phone')?.value), 126, 207), createText(getRemainingPhone(patient.telecom?.find((cp) => cp.system === 'phone')?.value), 153, 207), // 6. Patient's Relationship to Insured createCheckmark(relationship === 'self', 252, 159), createCheckmark(relationship === 'spouse', 289, 159), createCheckmark(relationship === 'child', 317, 159), createCheckmark(relationship === 'other', 353, 159), // 7. Insured's Address createText(insured?.address?.[0]?.line?.join(', '), 374, 159), createText(insured?.address?.[0]?.city, 374, 182), createText(insured?.address?.[0]?.state, 555, 182), createText(insured?.address?.[0]?.postalCode, 374, 207), createText(getPhoneAreaCode(insured?.telecom?.find((cp) => cp.system === 'phone')?.value), 483, 207), createText(getRemainingPhone(insured?.telecom?.find((cp) => cp.system === 'phone')?.value), 511, 207), // 9. Other Insured's Name createText(formatHumanName(otherInsured?.name?.[0]), 22, 231), // 9a. Other Insured's ID Number createText(otherCoverage?.identifier?.find((id) => id.use === 'official')?.value, 22, 254), // 9d. Insurance plan name or program name createText(getDisplayString(insurer), 22, 327), // 10a. Employment? createCheckmark(relatedToEmployment, 267, 255), createCheckmark(!relatedToEmployment, 310, 255), // 10b. Auto accident? createCheckmark(relatedToAutoAccident, 267, 279), createCheckmark(!relatedToAutoAccident, 310, 279), createText(accidentLocationState, 341, 279), // 10c. Other accident? createCheckmark(relatedToOtherAccident, 267, 302), createCheckmark(!relatedToOtherAccident, 310, 302), // 11. Insured's policy group or FECA number createText(coverage.class?.find((c) => c.type?.coding?.[0]?.code === 'group')?.value, 375, 228), // 11a. Insured's date of birth (MM/DD/YYYY) createDate(insured?.birthDate, 396, 255), // Insured's sex createCheckmark(insured?.gender === 'male', 504, 255), createCheckmark(insured?.gender === 'female', 555, 255), // 11b. Other claim ID (designated by NUCC) // 11c. Insurance plan name or program name createText(coverage.class?.find((c) => c.type?.coding?.[0]?.code === 'plan')?.name, 375, 301), // 12. Patient's or authorized person's signature // 14. Date of current illness, injury, or pregnancy (LMP) createDate(dateOfCurrentIllness, 28, 399), // 16. Dates patient unable to work in current occupation createDate(employmentImpacted?.timingPeriod?.start, 404, 399), createDate(employmentImpacted?.timingPeriod?.end, 505, 399), // 17. Name of referring provider or other source createText(referrer ? getDisplayString(referrer) : '', 43, 423), // 17b. NPI number createText(referrer?.identifier?.find((id) => id.system === `${HTTP_HL7_ORG}/fhir/sid/us-npi`)?.value, 247, 423), // 18. Hospitalization dates related to current services createDate(hospitalization?.timingPeriod?.start, 404, 423), createDate(hospitalization?.timingPeriod?.end, 505, 423), // 19. Additional claim information (designated by NUCC) // 20. Outside lab createCheckmark(!!outsideLab, 389, 446), createCheckmark(!outsideLab, 424, 446), createText(outsideLabCharges, 465, 446), // 21. Diagnosis or Nature of Illness or Injury diagnosis.map((diagnosisCode, index) => { const x = 37 + (index % 4) * 94; const y = 471 + Math.floor(index / 4) * 12; return createText(diagnosisCode, x, y); }), // 22. Resubmission code createText(resubmissionCode, 374, 471), // Original ref number createText(originalReference, 456, 471), // 23. Prior authorization number createText(priorAuthRefNumber, 374, 493), // 24. Claim lines claim.item?.map((item, index) => { const y = 542 + index * 25; return [ // 24A. Dates of service createDate(item?.servicedDate, 21, y), // 24B. Place of service createText(item?.locationAddress?.state, 149, y), // 24C. EMG createCheckmark(item.category?.coding?.[0].code === 'EMG', 172, y), // 24D. Procedures, services, or supplies createText(item.productOrService?.coding?.map((code) => code.code).join(', '), 194, y), createText(item.modifier?.[0]?.coding?.map((code) => code.code).join(', '), 246, y), // 24E. Diagnosis pointer createText('', 335, y), // 24F. Charges createText(formatMoney(item.net), 373, y), // 24G. Days or units createText(formatQuantity(item.quantity), 437, y), // 24H. EPSDT family plan createText(formatCodeableConcept(item.programCode?.[0]), 466, y), ].flat(); }), // 25. Federal tax ID number createText(taxIdentifier?.value, 22, 686), // 26. Patient's account number createText(patientAccountNumber, 180, 686), // 28. Total charge createText(formatMoney(claim.total).replace('$', ''), 383, 686), // 29. Amount paid createText(patientPaid.replace('USD', ''), 463, 686), // 32. Service facility location information createText('', 179, 707), createText('', 177, 719), // 32a. Service facility NPI number createText('', 187, 745), // 33. Billing provider info & phone number createText(getPhoneAreaCode(provider?.telecom?.find((cp) => cp.system === 'phone')?.value), 491, 699), createText(getRemainingPhone(provider?.telecom?.find((cp) => cp.system === 'phone')?.value), 518, 699), createText(getDisplayString(provider), 375, 709), createText( (provider as Practitioner).address?.[0] ? formatAddress((provider as Practitioner).address?.[0] as Address) : '', 375, 721 ), // 33a. Billing provider NPI number createText(provider?.identifier?.find((i) => i.system === `${HTTP_HL7_ORG}/fhir/sid/us-npi`)?.value, 380, 745), ] .flat() .filter(Boolean) as Content[], }; return docDefinition; } function createText(text: string | undefined, x: number, y: number): Content | undefined { if (!text) { return undefined; } return { text, absolutePosition: { x, y }, }; } function createCheckmark(checked: boolean, x: number, y: number): Content | undefined { return createText(checked ? 'X' : '', x, y); } function createDate(date: string | undefined, x: number, y: number): (Content | undefined)[] | undefined { return [ createText(date?.substring(5, 7), x, y), createText(date?.substring(8, 10), x + 21, y), createText(date?.substring(0, 4), x + 42, y), ]; } export function getSimplePhone(phone: string | undefined): string | undefined { if (!phone) { return undefined; } let result = phone; if (result.startsWith('tel:')) { result = result.substring(4); } if (result.startsWith('+1')) { result = result.substring(2); } if (result.startsWith('1')) { result = result.substring(1); } // Remove all remaining non-digit characters return result.replaceAll(/\D/g, ''); } function getPhoneAreaCode(phone: string | undefined): string | undefined { return getSimplePhone(phone)?.substring(0, 3); } function getRemainingPhone(phone: string | undefined): string | undefined { const simple = getSimplePhone(phone); if (!simple) { return undefined; } return simple.substring(3, 6) + '-' + simple.substring(6); } export function formatHumanName(name: HumanName | undefined): string { if (!name) { return ''; } const family = name.family; const given = name.given ?? []; if (!family && given.length === 0) { return ''; } const [firstName, ...rest] = given; const middleName = rest.join(' '); const parts = []; if (family) { parts.push(family); } if (firstName) { parts.push(firstName); } if (middleName) { parts.push(middleName); } return parts.join(', '); }

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