Repository: medplum/medplum
Pull request #6372: Claim $export CMS 1500 pdf 
Author: login: techdavidy
Labels: name: fhir-datastore
description: Related to the FHIR datastore, includes API and FHIR operations
Created: 2025-04-15T19:03:46Z | Merged: 2025-04-19T01:44:42Z

Expose  `/fhir/R4/Claim/${claim.id}/$export` to generate  CMS 1500 pdf


Diff:
diff --git a/packages/server/src/fhir/operations/claimexport.test.ts b/packages/server/src/fhir/operations/claimexport.test.ts
new file mode 100644
index 00000000000..8f86378fd60
--- /dev/null
+++ b/packages/server/src/fhir/operations/claimexport.test.ts
@@ -0,0 +1,707 @@
+import { Bundle, Claim } from '@medplum/fhirtypes';
+import { initApp, shutdownApp } from '../../app';
+import { loadTestConfig } from '../../config/loader';
+import { initTestAuth } from '../../test.setup';
+import express from 'express';
+import request from 'supertest';
+
+const app = express();
+let accessToken: string;
+
+describe('CMS 1500 PDF Bot', () => {
+  beforeAll(async () => {
+    const config = await loadTestConfig();
+    await initApp(app, config);
+    accessToken = await initTestAuth();
+  });
+
+  afterAll(async () => {
+    await shutdownApp();
+  });
+
+  test('Fully answered CMS1500 pdf', async () => {
+    const bundleRes = await request(app)
+      .post(`/fhir/R4`)
+      .set('Authorization', 'Bearer ' + accessToken)
+      .set('Content-Type', 'application/fhir+json')
+      .send(fullAnswer);
+    expect(bundleRes.status).toBe(200);
+    expect(bundleRes.body.resourceType).toBe('Bundle');
+    expect(bundleRes.body.type).toBe('transaction-response');
+
+    const searchRes = await request(app)
+      .get(`/fhir/R4/Claim?identifier=example-claim-cms1500`)
+      .set('Authorization', 'Bearer ' + accessToken)
+      .set('Accept', 'application/fhir+json');
+    expect(searchRes.status).toBe(200);
+    expect(searchRes.body.resourceType).toBe('Bundle');
+    expect(searchRes.body.entry.length).toBeGreaterThan(0);
+
+    const claim = searchRes.body.entry[0].resource as Claim;
+    expect(claim.resourceType).toBe('Claim');
+    expect(claim.identifier?.some((id) => id.value === 'example-claim-cms1500')).toBe(true);
+
+    const response = await request(app)
+      .get(`/fhir/R4/Claim/${claim.id}/$export`)
+      .set('Authorization', 'Bearer ' + accessToken)
+      .set('Accept', 'application/fhir+json');
+
+    expect(response).toBeDefined();
+    expect(response.status).toBe(200);
+    expect(response.body.resourceType).toBe('Media');
+    expect(response.body.content.contentType).toBe('application/pdf');
+  });
+
+  test('Bad request - missing claim ID', async () => {
+    const response = await request(app)
+      .get(`/fhir/R4/Claim/$export`)
+      .set('Authorization', 'Bearer ' + accessToken)
+      .set('Accept', 'application/fhir+json');
+
+    expect(response.status).toBe(404);
+    expect(response.body.resourceType).toBe('OperationOutcome');
+    expect(response.body.issue[0].severity).toBe('error');
+  });
+
+  test('Bad request - claim not found', async () => {
+    const response = await request(app)
+      .get(`/fhir/R4/Claim/non-existent-id/$export`)
+      .set('Authorization', 'Bearer ' + accessToken)
+      .set('Accept', 'application/fhir+json');
+
+    expect(response.status).toBe(400);
+    expect(response.body.resourceType).toBe('OperationOutcome');
+    expect(response.body.issue[0].severity).toBe('error');
+  });
+});
+
+export const fullAnswer: Bundle = {
+  resourceType: 'Bundle',
+  type: 'transaction',
+  entry: [
+    {
+      fullUrl: 'urn:uuid:8796eac7-4ee2-4b9a-84f7-68691173e945',
+      request: { method: 'PUT', url: 'Patient?name=Homer%20Simpson' },
+      resource: {
+        resourceType: 'Patient',
+        name: [
+          {
+            given: ['Homer'],
+            family: 'Simpson',
+          },
+        ],
+        birthDate: '1956-05-12',
+        gender: 'male',
+        address: [
+          {
+            line: ['742 Evergreen Terrace'],
+            city: 'Springfield',
+            state: 'IL',
+            postalCode: '62704',
+          },
+        ],
+        telecom: [
+          {
+            system: 'phone',
+            use: 'mobile',
+            value: '555-555-6392',
+          },
+        ],
+      },
+    },
+    {
+      fullUrl: 'urn:uuid:7914fb49-05e1-45cc-bffc-a3fdff1b72e1',
+      request: { method: 'PUT', url: 'RelatedPerson?name=Marge%20Simpson' },
+      resource: {
+        resourceType: 'RelatedPerson',
+        patient: { reference: 'urn:uuid:8796eac7-4ee2-4b9a-84f7-68691173e945' },
+        name: [{ given: ['Marge'], family: 'Simpson' }],
+        birthDate: '1960-08-12',
+        gender: 'female',
+        address: [
+          {
+            line: ['742 Evergreen Terrace'],
+            city: 'Springfield',
+            state: 'IL',
+            postalCode: '62704',
+          },
+        ],
+        telecom: [
+          {
+            system: 'phone',
+            use: 'mobile',
+            value: '555-555-6393',
+          },
+        ],
+      },
+    },
+    {
+      fullUrl: 'urn:uuid:e2523da4-9d6e-442c-99aa-fcfe3353c9e3',
+      request: { method: 'PUT', url: 'RelatedPerson?name=Abraham%20Simpson' },
+      resource: {
+        resourceType: 'RelatedPerson',
+        patient: { reference: 'urn:uuid:8796eac7-4ee2-4b9a-84f7-68691173e945' },
+        name: [{ given: ['Abraham'], family: 'Simpson' }],
+        birthDate: '1927-06-04',
+        gender: 'male',
+        address: [
+          {
+            line: ['Springfield Retirement Castle'],
+            city: 'Springfield',
+            state: 'IL',
+            postalCode: '62704',
+          },
+        ],
+      },
+    },
+    {
+      fullUrl: 'urn:uuid:706245c5-5f9d-45eb-bf90-cee2fac3f52c',
+      request: { method: 'PUT', url: 'Practitioner?identifier=2490433892' },
+      resource: {
+        resourceType: 'Practitioner',
+        identifier: [
+          {
+            system: 'http://hl7.org/fhir/sid/us-npi',
+            value: '2490433892',
+          },
+        ],
+        name: [
+          {
+            given: ['Kevin'],
+            family: 'Smith',
+          },
+        ],
+        address: [
+          {
+            line: ['2904 Main Street'],
+            city: 'Elizabeth',
+            state: 'MD',
+            country: 'US',
+            postalCode: '21219',
+          },
+        ],
+        telecom: [
+          {
+            use: 'work',
+            system: 'phone',
+            value: '555-555-9391',
+          },
+        ],
+      },
+    },
+    {
+      fullUrl: 'urn:uuid:f1ed24bf-7efa-44a1-b5ea-cc46414a282d',
+      request: { method: 'PUT', url: 'Organization?identifier=7911621876' },
+      resource: {
+        resourceType: 'Organization',
+        identifier: [
+          {
+            use: 'official',
+            system: 'http://hl7.org/fhir/sid/us-npi',
+            value: '7911621876',
+          },
+          {
+            use: 'official',
+            type: {
+              coding: [
+                {
+                  system: 'http://example.org/org-id',
+                  code: 'TAX',
+                },
+              ],
+            },
+            value: '5551844680',
+            system: 'http://example-systemt.org/tax',
+          },
+        ],
+        name: 'Independence Blue Cross Blue Shield',
+        address: [
+          {
+            line: ['1901 Market Street'],
+            city: 'Philadelphia',
+            state: 'PA',
+            postalCode: '19103',
+            type: 'both',
+          },
+        ],
+        telecom: [
+          {
+            system: 'phone',
+            use: 'work',
+            value: '555-555-4321',
+          },
+        ],
+      },
+    },
+    {
+      fullUrl: 'urn:uuid:4bc72375-425d-4fb0-be6e-38bc6144d1e5',
+      request: { method: 'PUT', url: 'Organization?identifier=5746217289' },
+      resource: {
+        resourceType: 'Organization',
+        identifier: [
+          {
+            use: 'official',
+            system: 'http://hl7.org/fhir/sid/us-npi',
+            value: '5746217289',
+          },
+          {
+            use: 'official',
+            type: {
+              coding: [
+                {
+                  system: 'http://example.org/org-id',
+                  code: 'TAX',
+                },
+              ],
+            },
+            value: '5554404734',
+            system: 'http://example-systemt.org/tax',
+          },
+        ],
+        name: 'Medicare Insurance',
+        address: [
+          {
+            line: ['2578 Elmwood Avenue'],
+            city: 'Chicago',
+            state: 'IL',
+            postalCode: '60610',
+            type: 'both',
+          },
+        ],
+      },
+    },
+    {
+      fullUrl: 'urn:uuid:72ec6c80-6dab-41e0-adff-a8670a5b4363',
+      request: { method: 'PUT', url: 'Coverage?identifier=89442808' },
+      resource: {
+        resourceType: 'Coverage',
+        identifier: [
+          {
+            use: 'official',
+            value: '89442808',
+          },
+        ],
+        status: 'active',
+        beneficiary: {
+          reference: 'urn:uuid:8796eac7-4ee2-4b9a-84f7-68691173e945',
+          display: 'Homer Simpson',
+        },
+        payor: [
+          {
+            reference: 'urn:uuid:f1ed24bf-7efa-44a1-b5ea-cc46414a282d',
+            display: 'Independence Blue Cross Blue Shield',
+          },
+        ],
+        type: {
+          coding: [
+            {
+              system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
+              code: 'HIP',
+              display: 'health insurance plan policy',
+            },
+          ],
+        },
+        relationship: {
+          coding: [
+            {
+              system: 'http://hl7.org/fhir/ValueSet/subscriber-relationship',
+              code: 'spouse',
+              display: 'Spouse',
+            },
+          ],
+        },
+        subscriber: {
+          reference: 'urn:uuid:7914fb49-05e1-45cc-bffc-a3fdff1b72e1',
+          display: 'Marge Simpson',
+        },
+        class: [
+          {
+            type: {
+              coding: [
+                {
+                  system: 'http://terminology.hl7.org/CodeSystem/coverage-class',
+                  code: 'plan',
+                },
+              ],
+            },
+            value: 'B37FC',
+            name: 'Independence Blue Full Coverage',
+          },
+          {
+            type: {
+              coding: [
+                {
+                  system: 'http://terminology.hl7.org/CodeSystem/coverage-class',
+                  code: 'group',
+                },
+              ],
+            },
+            value: '123456789',
+            name: 'Independence Blue Group Plan',
+          },
+        ],
+      },
+    },
+    {
+      fullUrl: 'urn:uuid:10e49ae1-acf1-4288-93d3-5bbb504957aa',
+      request: { method: 'PUT', url: 'Coverage?identifier=21173018' },
+      resource: {
+        resourceType: 'Coverage',
+        identifier: [
+          {
+            use: 'official',
+            value: '21173018',
+          },
+        ],
+        status: 'active',
+        beneficiary: {
+          reference: 'urn:uuid:8796eac7-4ee2-4b9a-84f7-68691173e945',
+          display: 'Homer Simpson',
+        },
+        payor: [
+          {
+            reference: 'urn:uuid:4bc72375-425d-4fb0-be6e-38bc6144d1e5',
+            display: 'Medicare Insurance',
+          },
+        ],
+        type: {
+          coding: [
+            {
+              system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
+              code: 'EHCPOL',
+              display: 'extended healthcare',
+            },
+          ],
+        },
+        relationship: {
+          coding: [
+            {
+              system: 'http://hl7.org/fhir/ValueSet/subscriber-relationship',
+              code: 'child',
+              display: 'Child',
+            },
+          ],
+        },
+        subscriber: {
+          reference: 'urn:uuid:e2523da4-9d6e-442c-99aa-fcfe3353c9e3',
+          display: 'Abraham Simpson',
+        },
+        class: [
+          {
+            type: {
+              coding: [
+                {
+                  system: 'http://terminology.hl7.org/CodeSystem/coverage-class',
+                  code: 'plan',
+                },
+              ],
+            },
+            value: '11461128',
+            name: 'Medicare Gold Plus',
+          },
+        ],
+      },
+    },
+    {
+      fullUrl: 'urn:uuid:30192746-113d-4c78-af02-3090ce22996f',
+      request: { method: 'PUT', url: 'ServiceRequest?identifier=4839201756' },
+      resource: {
+        resourceType: 'ServiceRequest',
+        identifier: [
+          {
+            system: 'http://hl7.org/fhir/sid/us-npi',
+            value: '4839201756',
+          },
+        ],
+        status: 'active',
+        intent: 'order',
+        category: [
+          {
+            coding: [
+              { system: 'http://snomed.info/sct', code: '103696004', display: 'Patient referral to specialist' },
+            ],
+          },
+        ],
+        subject: { reference: 'urn:uuid:8796eac7-4ee2-4b9a-84f7-68691173e945', display: 'Homer Simpson' },
+        requester: { reference: 'urn:uuid:706245c5-5f9d-45eb-bf90-cee2fac3f52c', display: 'Kevin Smith' },
+      },
+    },
+    {
+      fullUrl: 'urn:uuid:6a86eda3-ce9e-472c-ba6b-1855e518d779',
+      request: { method: 'PUT', url: 'Claim?identifier=example-claim-cms1500' },
+      resource: {
+        resourceType: 'Claim',
+        identifier: [
+          {
+            system: 'http://example.org/claims',
+            value: 'example-claim-cms1500',
+          },
+        ],
+        status: 'active',
+        type: {
+          coding: [
+            {
+              system: 'http://terminology.hl7.org/CodeSystem/claim-type',
+              code: 'professional',
+              display: 'Professional',
+            },
+          ],
+        },
+        use: 'claim',
+        patient: { reference: 'urn:uuid:8796eac7-4ee2-4b9a-84f7-68691173e945', display: 'Homer Simpson' },
+        created: '2020-04-17T11:15:12.614Z',
+        priority: {
+          coding: [
+            {
+              system: 'http://hl7.org/fhir/ValueSet/process-priority',
+              code: 'normal',
+            },
+          ],
+        },
+        referral: { reference: 'urn:uuid:30192746-113d-4c78-af02-3090ce22996f' },
+        insurance: [
+          {
+            coverage: {
+              reference: 'urn:uuid:72ec6c80-6dab-41e0-adff-a8670a5b4363',
+              display: 'Homer Simpson Health Insurance Plan',
+            },
+            sequence: 1,
+            focal: true,
+            preAuthRef: ['0923092390'],
+          },
+          {
+            coverage: {
+              reference: 'urn:uuid:10e49ae1-acf1-4288-93d3-5bbb504957aa',
+              display: 'Abraham Simpson Extended Healthcare Plan',
+            },
+            sequence: 2,
+            focal: false,
+          },
+        ],
+        provider: { reference: 'urn:uuid:706245c5-5f9d-45eb-bf90-cee2fac3f52c', display: 'Kevin Smith' },
+        supportingInfo: [
+          {
+            category: {
+              coding: [
+                {
+                  code: 'employmentimpacted',
+                  system: 'http://terminology.hl7.org/CodeSystem/claiminformationcategory',
+                },
+              ],
+            },
+            sequence: 1,
+            timingPeriod: {
+              start: '2024-04-02',
+              end: '2024-04-20',
+            },
+          },
+          {
+            category: {
+              coding: [{ code: 'info', system: 'http://terminology.hl7.org/CodeSystem/claiminformationcategory' }],
+            },
+            sequence: 2,
+            code: {
+              coding: [{ system: 'http://example.org/info-codes', code: 'patientaccount' }],
+            },
+            valueString: '429802409',
+          },
+          {
+            category: {
+              coding: [{ system: 'http://terminology.hl7.org/CodeSystem/claiminformationcategory', code: 'onset' }],
+            },
+            sequence: 3,
+            timingDate: '2024-02-02',
+          },
+          {
+            category: {
+              coding: [
+                { system: 'http://terminology.hl7.org/CodeSystem/claiminformationcategory', code: 'hospitalized' },
+              ],
+            },
+            sequence: 4,
+            timingPeriod: {
+              start: '2024-03-30',
+              end: '2024-05-21',
+            },
+          },
+          {
+            category: {
+              coding: [{ code: 'info', system: 'http://terminology.hl7.org/CodeSystem/claiminformationcategory' }],
+            },
+            code: {
+              coding: [{ system: 'http://example.org/info-codes', code: 'patientpaid' }],
+            },
+            sequence: 5,
+            valueQuantity: {
+              value: 320,
+              unit: 'USD',
+            },
+          },
+          {
+            category: {
+              coding: [
+                { system: 'http://terminology.hl7.org/CodeSystem/claiminformationcategory', code: 'outsidelab' },
+              ],
+            },
+            sequence: 6,
+            valueQuantity: {
+              value: 125,
+              unit: 'USD',
+            },
+          },
+        ],
+        total: {
+          currency: 'USD',
+          value: 1000,
+        },
+        diagnosis: [
+          {
+            sequence: 1,
+            diagnosisCodeableConcept: {
+              coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'J20', display: 'Acute bronchitis' }],
+            },
+          },
+          {
+            sequence: 2,
+            diagnosisCodeableConcept: {
+              coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'G89.4', display: 'Chronic pain syndrome' }],
+            },
+          },
+          {
+            sequence: 3,
+            diagnosisCodeableConcept: { coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'XYZ3' }] },
+          },
+          {
+            sequence: 4,
+            diagnosisCodeableConcept: { coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'XYZ4' }] },
+          },
+          {
+            sequence: 5,
+            diagnosisCodeableConcept: { coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'XYZ5' }] },
+          },
+          {
+            sequence: 6,
+            diagnosisCodeableConcept: { coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'XYZ6' }] },
+          },
+          {
+            sequence: 7,
+            diagnosisCodeableConcept: { coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'XYZ7' }] },
+          },
+          {
+            sequence: 8,
+            diagnosisCodeableConcept: { coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'XYZ8' }] },
+          },
+          {
+            sequence: 9,
+            diagnosisCodeableConcept: { coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'XYZ9' }] },
+          },
+          {
+            sequence: 10,
+            diagnosisCodeableConcept: { coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: 'XYZ10' }] },
+          },
+        ],
+        related: [
+          {
+            relationship: {
+              coding: [
+                {
+                  system: 'http://terminology.hl7.org/CodeSystem/ex-relatedclaimrelationship',
+                  code: 'prior',
+                  display: 'Prior Claim',
+                },
+              ],
+            },
+          },
+        ],
+        accident: {
+          date: '2024-03-30',
+          locationAddress: {
+            line: ['39 Green Lane'],
+            city: 'Wichita',
+            state: 'KS',
+            country: 'US',
+          },
+          type: {
+            coding: [
+              {
+                code: 'MVA',
+                system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
+                display: 'Motor vehicle accident',
+              },
+            ],
+          },
+        },
+        item: [
+          {
+            servicedDate: '2024-04-14',
+            sequence: 1,
+            productOrService: {
+              coding: [{ system: 'http://hl7.org/fhir/ValueSet/service-uscls', code: '1201', display: 'Exam, recall' }],
+            },
+            locationAddress: {
+              line: ['289 Johnson Street'],
+              city: 'Ames',
+              state: 'IA',
+            },
+            category: {
+              coding: [{ system: 'http://example.org/claim-item-category', code: 'EMG', display: 'Emergency' }],
+            },
+            modifier: [
+              { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/modifiers', code: 'x', display: 'None' }] },
+            ],
+            diagnosisSequence: [1, 2],
+            net: {
+              currency: 'USD',
+              value: 1000,
+            },
+            quantity: {
+              unit: 'days',
+              value: 20,
+            },
+            programCode: [
+              {
+                coding: [
+                  { code: 'none', system: 'http://terminology.hl7.org/CodeSystem/ex-programcode', display: 'None' },
+                ],
+              },
+            ],
+          },
+          {
+            servicedDate: '2024-05-15',
+            sequence: 1,
+            productOrService: {
+              coding: [{ system: 'http://hl7.org/fhir/ValueSet/service-uscls', code: '1201', display: 'Exam, recall' }],
+            },
+            locationAddress: {
+              line: ['289 Johnson Street'],
+              city: 'Ames',
+              state: 'IA',
+            },
+            category: {
+              coding: [{ system: 'http://example.org/claim-item-category', code: 'EMG', display: 'Emergency' }],
+            },
+            modifier: [
+              { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/modifiers', code: 'x', display: 'None' }] },
+            ],
+            diagnosisSequence: [1, 2],
+            net: {
+              currency: 'USD',
+              value: 2000,
+            },
+            quantity: {
+              unit: 'days',
+              value: 10,
+            },
+            programCode: [
+              {
+                coding: [
+                  { code: 'none', system: 'http://terminology.hl7.org/CodeSystem/ex-programcode', display: 'None' },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+    },
+  ],
+};
diff --git a/packages/server/src/fhir/operations/claimexport.ts b/packages/server/src/fhir/operations/claimexport.ts
new file mode 100644
index 00000000000..54a4ee46c1b
--- /dev/null
+++ b/packages/server/src/fhir/operations/claimexport.ts
@@ -0,0 +1,85 @@
+import { allOk, badRequest, getReferenceString, normalizeErrorString } from '@medplum/core';
+import { FhirRequest, FhirResponse } from '@medplum/fhir-router';
+import { Binary, Claim, Media, OperationDefinition } from '@medplum/fhirtypes';
+import { getAuthenticatedContext } from '../../context';
+import { createPdf } from '../../util/pdf';
+import { getClaimPDFDocDefinition } from './utils/cms1500pdf';
+import { getBinaryStorage } from '../../storage/loader';
+
+/**
+ * Operation definition for the Claim $export operation.
+ * This operation exports a claim as a PDF document.
+ */
+export const operation: OperationDefinition = {
+  resourceType: 'OperationDefinition',
+  name: 'claim-export',
+  status: 'active',
+  kind: 'operation',
+  code: 'export',
+  experimental: true,
+  resource: ['Claim'],
+  system: false,
+  type: false,
+  instance: true,
+  parameter: [
+    {
+      use: 'out',
+      name: 'return',
+      type: 'Media',
+      min: 1,
+      max: '1',
+      documentation: 'A Media resource containing the PDF document',
+    },
+  ],
+};
+
+/**
+ * Handles HTTP requests for the Claim $export operation.
+ *
+ * Reads the claim and generates a PDF document based on its contents.
+ * Returns a Binary resource containing the PDF document directly.
+ *
+ * Endpoint:
+ * [fhir base]/Claim/{id}/$export
+ *
+ * @param req - The FHIR request.
+ * @returns The FHIR response with a Binary resource containing the PDF.
+ */
+export async function claimExportHandler(req: FhirRequest): Promise<FhirResponse> {
+  const { repo } = getAuthenticatedContext();
+  const claimId = req.params.id;
+  if (!claimId) {
+    return [badRequest('Claim ID is required')];
+  }
+
+  try {
+    const claim = await repo.readResource<Claim>('Claim', claimId);
+    const docDefinition = await getClaimPDFDocDefinition(claim);
+    const pdfBuffer = await createPdf(docDefinition);
+    const binary = await repo.createResource<Binary>({
+      resourceType: 'Binary',
+      contentType: 'application/pdf',
+    });
+    await getBinaryStorage().writeBinary(binary, 'cms-1500.pdf', 'application/pdf', pdfBuffer.toString('base64'));
+
+    const media: Media = {
+      resourceType: 'Media',
+      status: 'completed',
+      subject: {
+        reference: getReferenceString(claim.patient),
+      },
+      operator: {
+        reference: getReferenceString(claim.provider),
+      },
+      issued: new Date().toISOString(),
+      content: {
+        contentType: 'application/pdf',
+        url: getReferenceString(binary),
+        title: 'cms-1500.pdf',
+      },
+    };
+    return [allOk, media];
+  } catch (error) {
+    return [badRequest(normalizeErrorString(error))];
+  }
+}
diff --git a/packages/server/src/fhir/operations/utils/cms1500pdf.test.ts b/packages/server/src/fhir/operations/utils/cms1500pdf.test.ts
new file mode 100644
index 00000000000..16508bd09d9
--- /dev/null
+++ b/packages/server/src/fhir/operations/utils/cms1500pdf.test.ts
@@ -0,0 +1,87 @@
+import { HumanName } from '@medplum/fhirtypes';
+import { formatHumanName, getSimplePhone } from './cms1500pdf';
+
+describe('CMS 1500 PDF Utils', () => {
+  test('formats full name with middle name', () => {
+    const name: HumanName = {
+      family: 'Smith',
+      given: ['John', 'Michael'],
+    };
+    expect(formatHumanName(name)).toBe('Smith, John, Michael');
+  });
+
+  test('returns empty string when name has no family and no given names', () => {
+    const name: HumanName = {
+      family: undefined,
+      given: [],
+    };
+    expect(formatHumanName(name)).toBe('');
+  });
+
+  test('formats name without middle name', () => {
+    const name: HumanName = {
+      family: 'Smith',
+      given: ['John'],
+    };
+    expect(formatHumanName(name)).toBe('Smith, John');
+  });
+
+  test('returns empty string when name is undefined', () => {
+    expect(formatHumanName(undefined)).toBe('');
+  });
+
+  test('formats multiple middle names', () => {
+    const name: HumanName = {
+      family: 'Smith',
+      given: ['John', 'Michael', 'Robert'],
+    };
+    expect(formatHumanName(name)).toBe('Smith, John, Michael Robert');
+  });
+
+  test('formats family name only', () => {
+    const name: HumanName = {
+      family: 'Smith',
+    };
+    expect(formatHumanName(name)).toBe('Smith');
+  });
+
+  test('formats given names only', () => {
+    const name: HumanName = {
+      given: ['John', 'Michael'],
+    };
+    expect(formatHumanName(name)).toBe('John, Michael');
+  });
+
+  test('handles empty name', () => {
+    const name: HumanName = {};
+    expect(formatHumanName(name)).toBe('');
+  });
+
+  test('handles undefined fields', () => {
+    const name: HumanName = {
+      family: undefined,
+      given: undefined,
+    };
+    expect(formatHumanName(name)).toBe('');
+  });
+
+  test('returns undefined when input is undefined', () => {
+    expect(getSimplePhone(undefined)).toBeUndefined();
+  });
+
+  test('returns undefined when input is empty string', () => {
+    expect(getSimplePhone('')).toBeUndefined();
+  });
+
+  test('removes tel: prefix from the beginning of phone numbers', () => {
+    expect(getSimplePhone('tel:1234567890')).toBe('234567890');
+  });
+
+  test('removes +1 prefix from the beginning of phone numbers', () => {
+    expect(getSimplePhone('+11234567890')).toBe('234567890');
+  });
+
+  test('removes standalone 1 prefix from the beginning of phone numbers', () => {
+    expect(getSimplePhone('11234567890')).toBe('1234567890');
+  });
+});
diff --git a/packages/server/src/fhir/operations/utils/cms1500pdf.ts b/packages/server/src/fhir/operations/utils/cms1500pdf.ts
new file mode 100644
index 00000000000..90459ce75a2
--- /dev/null
+++ b/packages/server/src/fhir/operations/utils/cms1500pdf.ts
@@ -0,0 +1,394 @@
+import {
+  formatAddress,
+  formatCodeableConcept,
+  formatMoney,
+  formatQuantity,
+  getDisplayString,
+  HTTP_HL7_ORG,
+} from '@medplum/core';
+import { Address, Claim, HumanName, Practitioner, RelatedPerson } from '@medplum/fhirtypes';
+import { Content, TDocumentDefinitions } from 'pdfmake/interfaces';
+import { getAuthenticatedContext } from '../../../context';
+import { join } from 'path';
+
+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: join(__dirname, '../../../../static/cms1500.png'),
+        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(formatCodeableConcept(item.productOrService), 194, y),
+          createText(formatCodeableConcept(item.modifier?.[0]), 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.replace(/\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(', ');
+}
diff --git a/packages/server/src/fhir/routes.ts b/packages/server/src/fhir/routes.ts
index e0725d371ad..bd8fa131c01 100644
--- a/packages/server/src/fhir/routes.ts
+++ b/packages/server/src/fhir/routes.ts
@@ -51,6 +51,7 @@ import { sendOutcome } from './outcomes';
 import { ResendSubscriptionsOptions } from './repo';
 import { sendFhirResponse } from './response';
 import { smartConfigurationHandler, smartStylingHandler } from './smart';
+import { claimExportHandler } from './operations/claimexport';
 
 export const fhirRouter = Router();
 
@@ -238,6 +239,9 @@ function initInternalFhirRouter(): FhirRouter {
   // Bot $deploy operation
   router.add('POST', '/Bot/:id/$deploy', deployHandler);
 
+  // Claim $export operation
+  router.add('GET', '/Claim/:id/$export', claimExportHandler);
+
   // Group $export operation
   router.add('GET', '/Group/:id/$export', groupExportHandler);
 
diff --git a/packages/server/src/img/cms1500.png b/packages/server/src/img/cms1500.png
new file mode 100644
index 00000000000..eed6fa4ecb0
Binary files /dev/null and b/packages/server/src/img/cms1500.png differ
diff --git a/packages/server/src/util/pdf.test.ts b/packages/server/src/util/pdf.test.ts
new file mode 100644
index 00000000000..a6a40cb0be8
--- /dev/null
+++ b/packages/server/src/util/pdf.test.ts
@@ -0,0 +1,26 @@
+import { createPdf } from './pdf';
+import { TDocumentDefinitions } from 'pdfmake/interfaces';
+
+describe('createPdf', () => {
+  test('creates a basic PDF with default fonts', async () => {
+    const docDefinition: TDocumentDefinitions = {
+      content: [
+        {
+          text: 'Test Document',
+          font: 'Helvetica',
+          style: 'header',
+        },
+        {
+          text: 'This is a paragraph of text.',
+          font: 'Helvetica',
+        },
+      ],
+    };
+
+    const pdfBuffer = await createPdf(docDefinition);
+
+    expect(pdfBuffer).toBeInstanceOf(Buffer);
+    expect(pdfBuffer.length).toBeGreaterThan(0);
+    expect(pdfBuffer.toString().substring(0, 5)).toBe('%PDF-'); // Check PDF header
+  });
+});
diff --git a/packages/server/src/util/pdf.ts b/packages/server/src/util/pdf.ts
new file mode 100644
index 00000000000..7bb7cd812f2
--- /dev/null
+++ b/packages/server/src/util/pdf.ts
@@ -0,0 +1,35 @@
+import { TDocumentDefinitions, TFontDictionary, CustomTableLayout } from 'pdfmake/interfaces';
+import PdfPrinter from 'pdfmake';
+
+/**
+ * Generates a PDF buffer from a document definition.
+ *
+ * @param docDefinition - The PDF document definition.
+ * @param tableLayouts - Optional custom table layouts.
+ * @param fonts - Optional custom fonts (uses default fonts if not provided).
+ * @returns Promise that resolves to a Buffer containing the PDF.
+ */
+export function createPdf(
+  docDefinition: TDocumentDefinitions,
+  tableLayouts?: Record<string, CustomTableLayout>,
+  fonts?: TFontDictionary
+): Promise<Buffer> {
+  return new Promise((resolve, reject) => {
+    const defaultFonts: TFontDictionary = {
+      Helvetica: {
+        normal: 'Helvetica',
+        bold: 'Helvetica-Bold',
+        italics: 'Helvetica-Oblique',
+        bolditalics: 'Helvetica-BoldOblique',
+      },
+    };
+
+    const printer = new PdfPrinter(fonts ?? defaultFonts);
+    const pdfDoc = printer.createPdfKitDocument(docDefinition, { tableLayouts });
+    const chunks: Uint8Array[] = [];
+    pdfDoc.on('data', (chunk: Uint8Array) => chunks.push(chunk));
+    pdfDoc.on('end', () => resolve(Buffer.concat(chunks)));
+    pdfDoc.on('error', reject);
+    pdfDoc.end();
+  });
+}
diff --git a/packages/server/static/cms1500.png b/packages/server/static/cms1500.png
new file mode 100644
index 00000000000..eed6fa4ecb0
Binary files /dev/null and b/packages/server/static/cms1500.png differ


Reviews:
user: login: ThatOneBro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T21:31:15Z
user: login: ThatOneBro
body: Looks good for the most part, some small comments
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T21:33:39Z
user: login: ThatOneBro
body: Think this optimization is worth making, other than that I think everything is good
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T20:52:29Z
user: login: ThatOneBro
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T03:51:06Z
user: login: codyebberson
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-18T17:42:27Z
user: login: codyebberson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T17:45:37Z
user: login: codyebberson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T17:46:28Z
user: login: codyebberson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T17:49:27Z
user: login: codyebberson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T17:49:31Z
user: login: codyebberson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T21:09:43Z
user: login: codyebberson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T21:19:31Z
user: login: codyebberson
body: Nice, thanks @techdavidy 💪 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-19T01:31:17Z

Review comments:
diff_hunk: @@ -0,0 +1,85 @@
+import { allOk, badRequest, getReferenceString } from '@medplum/core';
+import { FhirRequest, FhirResponse } from '@medplum/fhir-router';
+import { Binary, Claim, Media, OperationDefinition } from '@medplum/fhirtypes';
+import { getAuthenticatedContext } from '../../context';
+import { createPdf } from '../../pdf/pdf';
+import { getClaimPDFDocDefinition } from './utils/cms1500pdf';
+import { getBinaryStorage } from '../../storage/loader';
+
+/**
+ * Operation definition for the Claim $export operation.
+ * This operation exports a claim as a PDF document.
+ */
+export const operation: OperationDefinition = {
+  resourceType: 'OperationDefinition',
+  name: 'claim-export',
+  status: 'active',
+  kind: 'operation',
+  code: 'export',
+  experimental: true,
+  resource: ['Claim'],
+  system: false,
+  type: false,
+  instance: true,
+  parameter: [
+    {
+      use: 'out',
+      name: 'return',
+      type: 'Binary',
path: packages/server/src/fhir/operations/claimexport.ts
user: login: ThatOneBro
body: I think I may have lead you astray, since we are returning a `Media` here instead
created_at: 2025-04-16T21:23:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,85 @@
+import { allOk, badRequest, getReferenceString } from '@medplum/core';
+import { FhirRequest, FhirResponse } from '@medplum/fhir-router';
+import { Binary, Claim, Media, OperationDefinition } from '@medplum/fhirtypes';
+import { getAuthenticatedContext } from '../../context';
+import { createPdf } from '../../pdf/pdf';
+import { getClaimPDFDocDefinition } from './utils/cms1500pdf';
+import { getBinaryStorage } from '../../storage/loader';
+
+/**
+ * Operation definition for the Claim $export operation.
+ * This operation exports a claim as a PDF document.
+ */
+export const operation: OperationDefinition = {
+  resourceType: 'OperationDefinition',
+  name: 'claim-export',
+  status: 'active',
+  kind: 'operation',
+  code: 'export',
+  experimental: true,
+  resource: ['Claim'],
+  system: false,
+  type: false,
+  instance: true,
+  parameter: [
+    {
+      use: 'out',
+      name: 'return',
+      type: 'Binary',
+      min: 1,
+      max: '1',
+      documentation: 'A Binary resource containing the PDF document',
+    },
+  ],
+};
+
+/**
+ * Handles HTTP requests for the Claim $export operation.
+ *
+ * Reads the claim and generates a PDF document based on its contents.
+ * Returns a Binary resource containing the PDF document directly.
+ *
+ * Endpoint:
+ * [fhir base]/Claim/{id}/$export
+ *
+ * @param req - The FHIR request.
+ * @returns The FHIR response with a Binary resource containing the PDF.
+ */
+export async function claimExportHandler(req: FhirRequest): Promise<FhirResponse> {
+  const { repo } = getAuthenticatedContext();
+  const claimId = req.params.id;
+  if (!claimId) {
+    return [badRequest('Claim ID is required')];
+  }
+
+  try {
+    const claim = await repo.readResource<Claim>('Claim', claimId);
+    const docDefinition = await getClaimPDFDocDefinition(claim);
+    const pdfBuffer = await createPdf(docDefinition);
+    const binary = await repo.createResource<Binary>({
+      resourceType: 'Binary',
+      contentType: 'application/pdf',
+    });
+    await getBinaryStorage().writeBinary(binary, 'cms-1500.pdf', 'application/pdf', pdfBuffer.toString('base64'));
+
+    const media: Media = {
+      resourceType: 'Media',
+      status: 'completed',
+      subject: {
+        reference: getReferenceString(claim.patient),
+      },
+      operator: {
+        reference: getReferenceString(claim.provider),
+      },
+      issued: new Date().toISOString(),
+      content: {
+        contentType: 'application/pdf',
+        url: getReferenceString(binary),
+        title: 'cms-1500.pdf',
+      },
+    };
+    return [allOk, media];
+  } catch (error) {
+    return [badRequest(`Error exporting claim: ${(error as Error).message}`)];
path: packages/server/src/fhir/operations/claimexport.ts
user: login: ThatOneBro
body: We have the util `normalizeErrorString` which would be good here too 👍 
created_at: 2025-04-16T21:33:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,85 @@
+import { allOk, badRequest, getReferenceString } from '@medplum/core';
+import { FhirRequest, FhirResponse } from '@medplum/fhir-router';
+import { Binary, Claim, Media, OperationDefinition } from '@medplum/fhirtypes';
+import { getAuthenticatedContext } from '../../context';
+import { createPdf } from '../../pdf/pdf';
+import { getClaimPDFDocDefinition } from './utils/cms1500pdf';
+import { getBinaryStorage } from '../../storage/loader';
+
+/**
+ * Operation definition for the Claim $export operation.
+ * This operation exports a claim as a PDF document.
+ */
+export const operation: OperationDefinition = {
+  resourceType: 'OperationDefinition',
+  name: 'claim-export',
+  status: 'active',
+  kind: 'operation',
+  code: 'export',
+  experimental: true,
+  resource: ['Claim'],
+  system: false,
+  type: false,
+  instance: true,
+  parameter: [
+    {
+      use: 'out',
+      name: 'return',
+      type: 'Binary',
+      min: 1,
+      max: '1',
+      documentation: 'A Binary resource containing the PDF document',
+    },
+  ],
+};
+
+/**
+ * Handles HTTP requests for the Claim $export operation.
+ *
+ * Reads the claim and generates a PDF document based on its contents.
+ * Returns a Binary resource containing the PDF document directly.
+ *
+ * Endpoint:
+ * [fhir base]/Claim/{id}/$export
+ *
+ * @param req - The FHIR request.
+ * @returns The FHIR response with a Binary resource containing the PDF.
+ */
+export async function claimExportHandler(req: FhirRequest): Promise<FhirResponse> {
+  const { repo } = getAuthenticatedContext();
+  const claimId = req.params.id;
+  if (!claimId) {
+    return [badRequest('Claim ID is required')];
+  }
+
+  try {
+    const claim = await repo.readResource<Claim>('Claim', claimId);
+    const docDefinition = await getClaimPDFDocDefinition(claim);
+    const pdfBuffer = await createPdf(docDefinition);
+    const binary = await repo.createResource<Binary>({
+      resourceType: 'Binary',
+      contentType: 'application/pdf',
+    });
+    await getBinaryStorage().writeBinary(binary, 'cms-1500.pdf', 'application/pdf', pdfBuffer.toString('base64'));
+
+    const media: Media = {
+      resourceType: 'Media',
+      status: 'completed',
+      subject: {
+        reference: getReferenceString(claim.patient),
+      },
+      operator: {
+        reference: getReferenceString(claim.provider),
+      },
+      issued: new Date().toISOString(),
+      content: {
+        contentType: 'application/pdf',
+        url: getReferenceString(binary),
+        title: 'cms-1500.pdf',
+      },
+    };
+    return [allOk, media];
+  } catch (error) {
+    return [badRequest(`Error exporting claim: ${(error as Error).message}`)];
path: packages/server/src/fhir/operations/claimexport.ts
user: login: ThatOneBro
body: Also can we get coverage for that branch?
created_at: 2025-04-16T21:33:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,406 @@
+import {
+  formatAddress,
+  formatCodeableConcept,
+  formatMoney,
+  formatQuantity,
+  getDisplayString,
+  HTTP_HL7_ORG,
+} from '@medplum/core';
+import { Address, Claim, HumanName, Practitioner, RelatedPerson } from '@medplum/fhirtypes';
+import { Content, TDocumentDefinitions } from 'pdfmake/interfaces';
+import { getAuthenticatedContext } from '../../../context';
+import fs from 'fs';
+
+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: 'data:image/png;base64,' + imageToBase64(__dirname + '/cms1500.png'),
path: packages/server/src/fhir/operations/utils/cms1500pdf.ts
user: login: ThatOneBro
body: I think we can cache this in the module scope since this doesn't change for the lifetime of the server, ie.
```ts
const CMS_1500_IMG_DATA_URL = 'data:image/png;base64,' + imageToBase64(__dirname + '/cms1500.png');
```
at the top of the file and then:

```suggestion
        image: CMS_1500_IMG_DATA_URL,
```
created_at: 2025-04-17T20:51:48Z
author_association: MEMBER
diff_hunk: @@ -14,7 +14,7 @@
   "license": "Apache-2.0",
   "author": "Medplum <hello@medplum.com>",
   "scripts": {
-    "build": "npm run clean && tsc && tsc --project tsconfig.build.json",
+    "build": "npm run clean && tsc && tsc --project tsconfig.build.json && node -e \"require('node:fs').cpSync('src/img', 'dist/img', { recursive: true });\"",
path: packages/server/package.json
user: login: ThatOneBro
body: Added an `img` dir in `src` that we will always copy over. Using `node` to be platform agnostic
created_at: 2025-04-18T03:50:50Z
author_association: MEMBER
diff_hunk: @@ -14,7 +14,7 @@
   "license": "Apache-2.0",
   "author": "Medplum <hello@medplum.com>",
   "scripts": {
-    "build": "npm run clean && tsc && tsc --project tsconfig.build.json",
+    "build": "npm run clean && tsc && tsc --project tsconfig.build.json && node -e \"require('node:fs').cpSync('src/img', 'dist/img', { recursive: true });\"",
path: packages/server/package.json
user: login: codyebberson
body: Hmm... I don't think we want to copy this into `dist`

We should just add a `public` or `static` directory, and make sure that ends up in the Docker image.

I'd recommend handling this in `scripts/build-docker.sh`
created_at: 2025-04-18T17:38:45Z
author_association: MEMBER
path: packages/server/src/img/cms1500.png
user: login: codyebberson
body: I recommend moving this to a new directory such as `/package/server/static`
created_at: 2025-04-18T17:42:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,410 @@
+import {
+  formatAddress,
+  formatCodeableConcept,
+  formatMoney,
+  formatQuantity,
+  getDisplayString,
+  HTTP_HL7_ORG,
+} from '@medplum/core';
+import { Address, Claim, HumanName, Practitioner, RelatedPerson } from '@medplum/fhirtypes';
+import fs from 'node:fs';
+import path from 'node:path';
+import { Content, TDocumentDefinitions } from 'pdfmake/interfaces';
+import { getAuthenticatedContext } from '../../../context';
+
+const PAGE_WIDTH = 612;
+const PAGE_HEIGHT = 792;
+
+const insuranceTypes = new Set<string>([
+  'MEDICARE',
+  'MEDICAID',
+  'TRICARE',
+  'CHAMPVA',
+  'GROUP HEALTH PLAN',
+  'FECA BLK LUNG',
+  'OTHER',
+]);
+
+const CMS_1500_IMG_DATA_URL =
path: packages/server/src/fhir/operations/utils/cms1500pdf.ts
user: login: codyebberson
body: We can avoid this.

See: https://pdfmake.github.io/docs/0.1/document-definition-object/images/

We can refer to the static file inline
created_at: 2025-04-18T17:45:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,410 @@
+import {
+  formatAddress,
+  formatCodeableConcept,
+  formatMoney,
+  formatQuantity,
+  getDisplayString,
+  HTTP_HL7_ORG,
+} from '@medplum/core';
+import { Address, Claim, HumanName, Practitioner, RelatedPerson } from '@medplum/fhirtypes';
+import fs from 'node:fs';
+import path from 'node:path';
+import { Content, TDocumentDefinitions } from 'pdfmake/interfaces';
+import { getAuthenticatedContext } from '../../../context';
+
+const PAGE_WIDTH = 612;
+const PAGE_HEIGHT = 792;
+
+const insuranceTypes = new Set<string>([
+  'MEDICARE',
+  'MEDICAID',
+  'TRICARE',
+  'CHAMPVA',
+  'GROUP HEALTH PLAN',
+  'FECA BLK LUNG',
+  'OTHER',
+]);
+
+const CMS_1500_IMG_DATA_URL =
+  'data:image/png;base64,' + imageToBase64(path.resolve(__dirname, '../../../img', 'cms1500.png'));
+
+/**
+ * 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: CMS_1500_IMG_DATA_URL,
path: packages/server/src/fhir/operations/utils/cms1500pdf.ts
user: login: codyebberson
body: This can simply be the file name.

See: https://pdfmake.github.io/docs/0.1/document-definition-object/images/

```suggestion
        image: 'static/cms-1500.png',
```
created_at: 2025-04-18T17:46:28Z
author_association: MEMBER
path: packages/server/src/pdf/pdf.test.ts
user: login: codyebberson
body: Recommend moving this to `packages/server/src/util` rather than creating new `pdf` directory.
created_at: 2025-04-18T17:49:27Z
author_association: MEMBER
path: packages/server/src/pdf/pdf.ts
user: login: codyebberson
body: Recommend moving this to `packages/server/src/util` rather than creating new `pdf` directory.
created_at: 2025-04-18T17:49:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,393 @@
+import {
+  formatAddress,
+  formatCodeableConcept,
+  formatMoney,
+  formatQuantity,
+  getDisplayString,
+  HTTP_HL7_ORG,
+} from '@medplum/core';
+import { Address, Claim, HumanName, Practitioner, RelatedPerson } from '@medplum/fhirtypes';
+import { Content, TDocumentDefinitions } from 'pdfmake/interfaces';
+import { getAuthenticatedContext } from '../../../context';
+
+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: 'static/cms1500.png',
path: packages/server/src/fhir/operations/utils/cms1500pdf.ts
user: login: codyebberson
body: Actually, I was wrong before, we're going to need to make this a relative path.

When we run on local dev machines, we are typically running from the `server` directory, so `static/cms1500.png` is correct.

When we run on the Docker image, we are running in the project root directory, so it needs to be `packages/server/static/cms1500.png`.

We can deal with that using `join(__dirname, '../../../static/cms1500.png')` or whatever
created_at: 2025-04-18T21:09:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,85 @@
+import { allOk, badRequest, getReferenceString, normalizeErrorString } from '@medplum/core';
+import { FhirRequest, FhirResponse } from '@medplum/fhir-router';
+import { Binary, Claim, Media, OperationDefinition } from '@medplum/fhirtypes';
+import { getAuthenticatedContext } from '../../context';
+import { createPdf } from '../../util/pdf';
path: packages/server/src/fhir/operations/claimexport.ts
user: login: codyebberson
body: Looks like this file didn't get re-added

![image](https://github.com/user-attachments/assets/4fd9241e-560b-4d93-af68-b93a2df60064)

created_at: 2025-04-18T21:19:31Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-04-15T19:03:50Z
body: [vc]: #F6F2td8dIgc5B3+zjBLzUcvjPRFG/+vf5df3R1J1u78=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJtZWRwbHVtLXN0b3J5Ym9vayIsInJvb3REaXJlY3RvcnkiOiJwYWNrYWdlcy9yZWFjdCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL21lZHBsdW0vbWVkcGx1bS1zdG9yeWJvb2svQkhTZnBZeEF0alBiNmJ5UlRHQ1dkbjgxcUpGeSIsInByZXZpZXdVcmwiOiIiLCJuZXh0Q29tbWl0U3RhdHVzIjoiU0tJUFBFRCJ9LHsibmFtZSI6Im1lZHBsdW0td3d3Iiwicm9vdERpcmVjdG9yeSI6InBhY2thZ2VzL2RvY3MiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9tZWRwbHVtL21lZHBsdW0td3d3LzRWd2hES3pIRFFYSnVjNXFDWHFpSHhvclZYUEgiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQifSx7Im5hbWUiOiJtZWRwbHVtLWFwcCIsInJvb3REaXJlY3RvcnkiOiJwYWNrYWdlcy9hcHAiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9tZWRwbHVtL21lZHBsdW0tYXBwLzlRQVc2UWJkNTJQTHNQOEViTUVlZ2NwaW1rRVciLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQifSx7Im5hbWUiOiJtZWRwbHVtLXByb3ZpZGVyIiwicm9vdERpcmVjdG9yeSI6ImV4YW1wbGVzL21lZHBsdW0tcHJvdmlkZXIvIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbWVkcGx1bS9tZWRwbHVtLXByb3ZpZGVyLzY5eWN3bW54WTh4dVFwUEx0cWMzd0FVdmtZS1UiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>4 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **medplum-app** | ⬜️ Skipped ([Inspect](https://vercel.com/medplum/medplum-app/9QAW6Qbd52PLsP8EbMEegcpimkEW)) |  |  | Apr 19, 2025 0:03am |
| **medplum-provider** | ⬜️ Skipped ([Inspect](https://vercel.com/medplum/medplum-provider/69ycwmnxY8xuQpPLtqc3wAUvkYKU)) |  |  | Apr 19, 2025 0:03am |
| **medplum-storybook** | ⬜️ Skipped ([Inspect](https://vercel.com/medplum/medplum-storybook/BHSfpYxAtjPb6byRTGCWdn81qJFy)) |  |  | Apr 19, 2025 0:03am |
| **medplum-www** | ⬜️ Skipped ([Inspect](https://vercel.com/medplum/medplum-www/4VwhDKzHDQXJuc5qCXqiHxorVXPH)) |  |  | Apr 19, 2025 0:03am |
</details>





author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-19T00:28:47Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=medplum_medplum&pullRequest=6372) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=medplum_medplum&pullRequest=6372&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=medplum_medplum&pullRequest=6372&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=medplum_medplum&pullRequest=6372&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [90.4% Coverage on New Code](https://sonarcloud.io/component_measures?id=medplum_medplum&pullRequest=6372&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=medplum_medplum&pullRequest=6372&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=medplum_medplum&pullRequest=6372)


author_association: NONE