Search

PythonReactSEOPDF
GitHub Skill

lab-integration

Laboratory equipment integration patterns for veterinary clinics including IDEXX, HL7/FHIR message parsing, lab result normalization, and reference range handling. Use when building lab integration features.

Ai-Whisperers☆ 0 Repository stars0 Repository forks

Installation

Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.

View source ↗Guides

Skill instructions

Laboratory Equipment Integration Guide

Overview

This skill covers integration patterns for veterinary laboratory equipment, including analyzers from IDEXX, Abaxis, Heska, and generic HL7-compliant devices.


1. Common Lab Equipment Protocols

Protocol Comparison
Protocol Used By Format Connection
IDEXX VetConnect IDEXX analyzers Proprietary JSON REST API
HL7 v2.x Legacy analyzers Pipe-delimited Serial/TCP
ASTM E1381/E1394 Clinical analyzers Frame-based Serial
FHIR R4 Modern systems JSON/XML REST API
POCT1-A Point-of-care XML USB/Network

2. IDEXX VetConnect Integration

API Client
// lib/lab/idexx/client.ts
interface IDEXXConfig {
  baseUrl: string;
  apiKey: string;
  practiceId: string;
}

interface IDEXXPatient {
  patientId: string;
  patientName: string;
  species: 'Canine' | 'Feline' | 'Equine' | 'Other';
  breed?: string;
  sex?: 'Male' | 'Female' | 'Unknown';
  dateOfBirth?: string;
  weight?: number;
  weightUnit?: 'kg' | 'lb';
}

interface IDEXXOrderRequest {
  patient: IDEXXPatient;
  owner: {
    lastName: string;
    firstName: string;
  };
  tests: string[]; // Test codes
  clinician?: string;
  notes?: string;
}

interface IDEXXResult {
  orderId: string;
  status: 'Pending' | 'InProgress' | 'Completed' | 'Cancelled';
  results: Array<{
    testCode: string;
    testName: string;
    value: string;
    unit: string;
    referenceRange: {
      low: number;
      high: number;
    };
    flag: 'Normal' | 'Low' | 'High' | 'Critical';
    timestamp: string;
  }>;
}

export class IDEXXClient {
  private config: IDEXXConfig;
  private accessToken: string | null = null;

  constructor(config: IDEXXConfig) {
    this.config = config;
  }

  private async getAccessToken(): Promise<string> {
    if (this.accessToken) return this.accessToken;

    const response = await fetch(`${this.config.baseUrl}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: this.config.apiKey,
        scope: 'vetconnect',
      }),
    });

    const data = await response.json();
    this.accessToken = data.access_token;
    return this.accessToken;
  }

  async createOrder(request: IDEXXOrderRequest): Promise<string> {
    const token = await this.getAccessToken();

    const response = await fetch(`${this.config.baseUrl}/v1/orders`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
        'X-Practice-Id': this.config.practiceId,
      },
      body: JSON.stringify({
        patient: request.patient,
        owner: request.owner,
        orderedTests: request.tests.map(code => ({ testCode: code })),
        orderingClinician: request.clinician,
        clinicalNotes: request.notes,
      }),
    });

    const data = await response.json();
    return data.orderId;
  }

  async getResults(orderId: string): Promise<IDEXXResult> {
    const token = await this.getAccessToken();

    const response = await fetch(`${this.config.baseUrl}/v1/orders/${orderId}/results`, {
      headers: {
        'Authorization': `Bearer ${token}`,
        'X-Practice-Id': this.config.practiceId,
      },
    });

    return response.json();
  }

  async getAvailableTests(): Promise<Array<{ code: string; name: string; category: string }>> {
    const token = await this.getAccessToken();

    const response = await fetch(`${this.config.baseUrl}/v1/tests`, {
      headers: {
        'Authorization': `Bearer ${token}`,
        'X-Practice-Id': this.config.practiceId,
      },
    });

    return response.json();
  }
}
IDEXX Test Code Mapping
// lib/lab/idexx/test-codes.ts
export const IDEXX_TEST_CODES = {
  // Chemistry
  CHEM17: { name: 'Chemistry Panel 17', category: 'Chemistry' },
  CHEM10: { name: 'Chemistry Panel 10', category: 'Chemistry' },
  LIPA: { name: 'Lipase', category: 'Chemistry' },
  SPEC_CPL: { name: 'Spec cPL (Canine Pancreas-Specific Lipase)', category: 'Chemistry' },
  SPEC_FPL: { name: 'Spec fPL (Feline Pancreas-Specific Lipase)', category: 'Chemistry' },
  SDMA: { name: 'SDMA (Kidney Function)', category: 'Chemistry' },

  // Hematology
  CBC: { name: 'Complete Blood Count', category: 'Hematology' },
  DIFF: { name: 'Differential', category: 'Hematology' },
  RETIC: { name: 'Reticulocyte Count', category: 'Hematology' },

  // Endocrine
  T4: { name: 'Total T4', category: 'Endocrine' },
  FT4: { name: 'Free T4', category: 'Endocrine' },
  TSH: { name: 'TSH', category: 'Endocrine' },
  CORTISOL: { name: 'Cortisol', category: 'Endocrine' },

  // Infectious Disease
  SNAP_4DX: { name: '4Dx Plus (Heartworm, Lyme, Ehrlichia, Anaplasma)', category: 'Infectious' },
  SNAP_FELV_FIV: { name: 'FeLV/FIV Combo', category: 'Infectious' },
  SNAP_PARVO: { name: 'Parvo', category: 'Infectious' },
  SNAP_GIARDIA: { name: 'Giardia', category: 'Infectious' },

  // Urinalysis
  UA: { name: 'Urinalysis', category: 'Urinalysis' },
  UPC: { name: 'Urine Protein:Creatinine Ratio', category: 'Urinalysis' },
  URINE_CULTURE: { name: 'Urine Culture', category: 'Urinalysis' },

  // Coagulation
  PT_PTT: { name: 'PT/PTT', category: 'Coagulation' },
};

3. HL7 v2.x Message Parsing

HL7 Parser
// lib/lab/hl7/parser.ts
interface HL7Message {
  type: string;
  version: string;
  timestamp: Date;
  sendingApp: string;
  sendingFacility: string;
  segments: HL7Segment[];
}

interface HL7Segment {
  type: string;
  fields: string[];
}

interface HL7LabResult {
  patientId: string;
  patientName: string;
  species: string;
  orderId: string;
  testCode: string;
  testName: string;
  value: string;
  unit: string;
  referenceRange: string;
  abnormalFlag: string;
  resultStatus: string;
  observationTime: Date;
}

export class HL7Parser {
  private fieldSeparator = '|';
  private componentSeparator = '^';
  private repetitionSeparator = '~';
  private escapeCharacter = '\\';
  private subcomponentSeparator = '&';

  parse(message: string): HL7Message {
    const lines = message.split(/\r?\n/).filter(line => line.trim());
    const segments: HL7Segment[] = [];

    for (const line of lines) {
      const fields = line.split(this.fieldSeparator);
      segments.push({
        type: fields[0],
        fields: fields.slice(1),
      });
    }

    // Parse MSH segment for metadata
    const msh = segments.find(s => s.type === 'MSH');
    if (!msh) throw new Error('Invalid HL7: Missing MSH segment');

    // Update separators from MSH.1
    if (msh.fields[0]) {
      this.componentSeparator = msh.fields[0][0] || '^';
      this.repetitionSeparator = msh.fields[0][1] || '~';
      this.escapeCharacter = msh.fields[0][2] || '\\';
      this.subcomponentSeparator = msh.fields[0][3] || '&';
    }

    return {
      type: msh.fields[7] || 'Unknown', // MSH-9
      version: msh.fields[10] || '2.5', // MSH-12
      timestamp: this.parseDateTime(msh.fields[5]), // MSH-7
      sendingApp: msh.fields[1] || '', // MSH-3
      sendingFacility: msh.fields[2] || '', // MSH-4
      segments,
    };
  }

  parseLabResults(message: HL7Message): HL7LabResult[] {
    const results: HL7LabResult[] = [];

    // Find PID segment for patient info
    const pid = message.segments.find(s => s.type === 'PID');
    const patientId = pid?.fields[2] || ''; // PID-3
    const patientName = this.parsePatientName(pid?.fields[4] || ''); // PID-5

    // Find OBR segments (one per test order)
    const obrSegments = message.segments.filter(s => s.type === 'OBR');

    // Find OBX segments (one per result)
    const obxSegments = message.segments.filter(s => s.type === 'OBX');

    for (const obx of obxSegments) {
      const testId = this.parseComponent(obx.fields[2], 0); // OBX-3.1
      const testName = this.parseComponent(obx.fields[2], 1); // OBX-3.2

      results.push({
        patientId,
        patientName,
        species: '', // Would come from PID-35 in veterinary HL7
        orderId: obrSegments[0]?.fields[1] || '', // OBR-2
        testCode: testId,
        testName: testName,
        value: obx.fields[4] || '', // OBX-5
        unit: obx.fields[5] || '', // OBX-6
        referenceRange: obx.fields[6] || '', // OBX-7
        abnormalFlag: obx.fields[7] || '', // OBX-8
        resultStatus: obx.fields[10] || '', // OBX-11
        observationTime: this.parseDateTime(obx.fields[13] || ''), // OBX-14
      });
    }

    return results;
  }

  private parseComponent(field: string, index: number): string {
    const components = field.split(this.componentSeparator);
    return components[index] || '';
  }

  private parsePatientName(field: string): string {
    const components = field.split(this.componentSeparator);
    // Format: LastName^FirstName^MiddleName
    return [components[1], components[0]].filter(Boolean).join(' ');
  }

  private parseDateTime(value: string): Date {
    if (!value) return new Date();

    // HL7 format: YYYYMMDDHHMMSS
    const year = parseInt(value.slice(0, 4));
    const month = parseInt(value.slice(4, 6)) - 1;
    const day = parseInt(value.slice(6, 8));
    const hour = parseInt(value.slice(8, 10)) || 0;
    const minute = parseInt(value.slice(10, 12)) || 0;
    const second = parseInt(value.slice(12, 14)) || 0;

    return new Date(year, month, day, hour, minute, second);
  }
}
Sample HL7 Message
MSH|^~\&|IDEXX|VetLab|VETE|Clinic|20240115120000||ORU^R01|12345|P|2.5
PID|||12345^^^VETE||Rover^Dog||20200101|M|||||||||||||||Canine^DOG
OBR|1|12345|12345|CHEM17^Chemistry Panel 17|||20240115110000
OBX|1|NM|BUN^Blood Urea Nitrogen||15|mg/dL|7-27||||F|||20240115115000
OBX|2|NM|CREA^Creatinine||1.2|mg/dL|0.5-1.8||||F|||20240115115000
OBX|3|NM|GLU^Glucose||95|mg/dL|74-143||||F|||20240115115000
OBX|4|NM|ALT^Alanine Aminotransferase||45|U/L|10-125||||F|||20240115115000
OBX|5|NM|ALP^Alkaline Phosphatase||89|U/L|23-212||||F|||20240115115000

4. Result Normalization

Unified Result Format
// lib/lab/normalize.ts
interface NormalizedLabResult {
  id: string;
  orderId: string;
  petId: string;
  testCode: string;
  testName: string;
  category: LabTestCategory;
  value: number | string;
  valueType: 'numeric' | 'text' | 'coded';
  unit: string;
  referenceRange: {
    low?: number;
    high?: number;
    text?: string;
  };
  interpretation: 'normal' | 'low' | 'high' | 'critical_low' | 'critical_high' | 'abnormal';
  species: 'dog' | 'cat' | 'other';
  observedAt: Date;
  source: 'idexx' | 'hl7' | 'manual';
  rawData?: any;
}

type LabTestCategory =
  | 'chemistry'
  | 'hematology'
  | 'endocrine'
  | 'urinalysis'
  | 'coagulation'
  | 'infectious'
  | 'cytology'
  | 'other';

// Reference ranges by species
const REFERENCE_RANGES: Record<string, Record<string, { dog: [number, number]; cat: [number, number] }>> = {
  BUN: { unit: 'mg/dL', dog: [7, 27], cat: [16, 36] },
  CREA: { unit: 'mg/dL', dog: [0.5, 1.8], cat: [0.8, 2.4] },
  GLU: { unit: 'mg/dL', dog: [74, 143], cat: [74, 159] },
  ALT: { unit: 'U/L', dog: [10, 125], cat: [12, 130] },
  ALP: { unit: 'U/L', dog: [23, 212], cat: [14, 111] },
  TP: { unit: 'g/dL', dog: [5.2, 8.2], cat: [5.7, 8.9] },
  ALB: { unit: 'g/dL', dog: [2.3, 4.0], cat: [2.1, 3.3] },
  // ... more tests
};

export function normalizeResult(
  raw: any,
  source: 'idexx' | 'hl7' | 'manual',
  petId: string,
  species: 'dog' | 'cat' | 'other'
): NormalizedLabResult {
  const testCode = raw.testCode || raw.code;
  const numericValue = parseFloat(raw.value);
  const isNumeric = !isNaN(numericValue);

  // Get reference range for species
  const refRange = REFERENCE_RANGES[testCode];
  const speciesRange = refRange?.[species === 'other' ? 'dog' : species];

  // Determine interpretation
  let interpretation: NormalizedLabResult['interpretation'] = 'normal';
  if (isNumeric && speciesRange) {
    const [low, high] = speciesRange;
    const criticalLow = low * 0.5;
    const criticalHigh = high * 1.5;

    if (numericValue < criticalLow) interpretation = 'critical_low';
    else if (numericValue > criticalHigh) interpretation = 'critical_high';
    else if (numericValue < low) interpretation = 'low';
    else if (numericValue > high) interpretation = 'high';
  } else if (raw.flag === 'H' || raw.abnormalFlag === 'H') {
    interpretation = 'high';
  } else if (raw.flag === 'L' || raw.abnormalFlag === 'L') {
    interpretation = 'low';
  } else if (raw.flag === 'A' || raw.abnormalFlag === 'A') {
    interpretation = 'abnormal';
  }

  return {
    id: `${source}-${raw.orderId || ''}-${testCode}-${Date.now()}`,
    orderId: raw.orderId,
    petId,
    testCode,
    testName: raw.testName || raw.name || testCode,
    category: categorizeTest(testCode),
    value: isNumeric ? numericValue : raw.value,
    valueType: isNumeric ? 'numeric' : 'text',
    unit: raw.unit || refRange?.unit || '',
    referenceRange: {
      low: speciesRange?.[0],
      high: speciesRange?.[1],
      text: raw.referenceRange,
    },
    interpretation,
    species,
    observedAt: new Date(raw.observationTime || raw.timestamp || Date.now()),
    source,
    rawData: raw,
  };
}

function categorizeTest(testCode: string): LabTestCategory {
  const categories: Record<string, LabTestCategory> = {
    BUN: 'chemistry',
    CREA: 'chemistry',
    GLU: 'chemistry',
    ALT: 'chemistry',
    ALP: 'chemistry',
    // Hematology
    WBC: 'hematology',
    RBC: 'hematology',
    HGB: 'hematology',
    HCT: 'hematology',
    PLT: 'hematology',
    // Endocrine
    T4: 'endocrine',
    TSH: 'endocrine',
    CORTISOL: 'endocrine',
    // Urinalysis
    UA: 'urinalysis',
    UPC: 'urinalysis',
  };

  return categories[testCode] || 'other';
}

5. Lab Order Workflow

// lib/lab/workflow.ts
import { createClient } from '@/lib/supabase/server';
import { IDEXXClient } from './idexx/client';
import { normalizeResult } from './normalize';

interface CreateLabOrderRequest {
  petId: string;
  tests: string[];
  clinician: string;
  notes?: string;
  priority?: 'routine' | 'urgent';
}

export async function createLabOrder(request: CreateLabOrderRequest) {
  const supabase = await createClient();

  // Get pet and owner info
  const { data: pet } = await supabase
    .from('pets')
    .select(`
      id, name, species, breed, date_of_birth, weight,
      owner:profiles!pets_owner_id_fkey (full_name)
    `)
    .eq('id', request.petId)
    .single();

  if (!pet) throw new Error('Mascota no encontrada');

  // Create order in database
  const { data: order, error } = await supabase
    .from('lab_orders')
    .insert({
      pet_id: request.petId,
      ordered_by: request.clinician,
      status: 'pending',
      priority: request.priority || 'routine',
      notes: request.notes,
    })
    .select()
    .single();

  if (error) throw error;

  // Create order items
  await supabase.from('lab_order_items').insert(
    request.tests.map(testCode => ({
      lab_order_id: order.id,
      test_code: testCode,
      status: 'pending',
    }))
  );

  // Send to IDEXX if configured
  if (process.env.IDEXX_API_KEY) {
    const idexx = new IDEXXClient({
      baseUrl: process.env.IDEXX_BASE_URL!,
      apiKey: process.env.IDEXX_API_KEY,
      practiceId: process.env.IDEXX_PRACTICE_ID!,
    });

    const externalOrderId = await idexx.createOrder({
      patient: {
        patientId: pet.id,
        patientName: pet.name,
        species: mapSpecies(pet.species),
        breed: pet.breed,
        dateOfBirth: pet.date_of_birth,
        weight: pet.weight,
        weightUnit: 'kg',
      },
      owner: {
        lastName: pet.owner.full_name.split(' ').pop() || '',
        firstName: pet.owner.full_name.split(' ')[0] || '',
      },
      tests: request.tests,
      clinician: request.clinician,
      notes: request.notes,
    });

    // Update order with external ID
    await supabase
      .from('lab_orders')
      .update({
        external_order_id: externalOrderId,
        external_system: 'idexx',
      })
      .eq('id', order.id);
  }

  return order;
}

export async function processLabResults(
  orderId: string,
  results: any[],
  source: 'idexx' | 'hl7' | 'manual'
) {
  const supabase = await createClient();

  // Get order with pet info
  const { data: order } = await supabase
    .from('lab_orders')
    .select('pet_id, pets (species)')
    .eq('id', orderId)
    .single();

  if (!order) throw new Error('Orden no encontrada');

  const species = order.pets.species === 'dog' ? 'dog' : order.pets.species === 'cat' ? 'cat' : 'other';

  // Normalize and store results
  for (const rawResult of results) {
    const normalized = normalizeResult(rawResult, source, order.pet_id, species);

    await supabase.from('lab_results').insert({
      lab_order_id: orderId,
      test_code: normalized.testCode,
      test_name: normalized.testName,
      value: normalized.value.toString(),
      value_type: normalized.valueType,
      unit: normalized.unit,
      reference_low: normalized.referenceRange.low,
      reference_high: normalized.referenceRange.high,
      interpretation: normalized.interpretation,
      is_abnormal: normalized.interpretation !== 'normal',
      observed_at: normalized.observedAt.toISOString(),
      raw_data: normalized.rawData,
    });
  }

  // Update order status
  await supabase
    .from('lab_orders')
    .update({ status: 'completed', completed_at: new Date().toISOString() })
    .eq('id', orderId);

  // Update order items
  for (const result of results) {
    await supabase
      .from('lab_order_items')
      .update({ status: 'completed' })
      .eq('lab_order_id', orderId)
      .eq('test_code', result.testCode);
  }
}

function mapSpecies(species: string): 'Canine' | 'Feline' | 'Other' {
  switch (species.toLowerCase()) {
    case 'dog':
    case 'perro':
      return 'Canine';
    case 'cat':
    case 'gato':
      return 'Feline';
    default:
      return 'Other';
  }
}

6. Webhook Handler for External Results

// api/webhooks/lab-results/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { processLabResults } from '@/lib/lab/workflow';
import { HL7Parser } from '@/lib/lab/hl7/parser';

export async function POST(request: NextRequest) {
  const contentType = request.headers.get('content-type') || '';

  // Verify webhook signature
  const signature = request.headers.get('x-webhook-signature');
  if (!verifySignature(signature, await request.clone().text())) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  try {
    if (contentType.includes('application/json')) {
      // IDEXX JSON format
      const body = await request.json();
      await handleIDEXXWebhook(body);
    } else if (contentType.includes('text/plain') || contentType.includes('x-hl7')) {
      // HL7 format
      const body = await request.text();
      await handleHL7Message(body);
    } else {
      return NextResponse.json({ error: 'Unsupported content type' }, { status: 415 });
    }

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('Lab webhook error:', error);
    return NextResponse.json({ error: 'Processing failed' }, { status: 500 });
  }
}

async function handleIDEXXWebhook(body: any) {
  const { orderId, results, status } = body;

  if (status === 'Completed' && results?.length > 0) {
    // Find our order by external ID
    const { data: order } = await supabase
      .from('lab_orders')
      .select('id')
      .eq('external_order_id', orderId)
      .single();

    if (order) {
      await processLabResults(order.id, results, 'idexx');
    }
  }
}

async function handleHL7Message(message: string) {
  const parser = new HL7Parser();
  const parsed = parser.parse(message);

  if (parsed.type === 'ORU^R01') {
    // Result message
    const results = parser.parseLabResults(parsed);

    // Find order by patient ID or order number
    const orderId = results[0]?.orderId;
    if (orderId) {
      await processLabResults(orderId, results, 'hl7');
    }
  }
}

Reference: IDEXX VetConnect API, HL7 v2.5 specification, veterinary lab standards

Examples

Read the usage examples in the source repository.

View source ↗

Source history

Indexed on
2026-01-20
Index updated
2026-01-26
Branch
main

Index and sync dates describe this directory. View the source repository for code history.

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!