🏥 Sprint 2: Integración HCCA (HL7 v2.x)
Este documento describe la integración con la Historia Clínica Compartida de Andorra (HCCA) mediante el protocolo HL7 v2.x.
📋 Índice
🎯 Visión General
Objetivo
Recibir automáticamente las peticiones de estudios radiológicos que los médicos introducen en el sistema HCCA, evitando la entrada manual y reduciendo errores.
Flujo Principal
┌──────────────────┐ ORM^O01 ┌──────────────────┐
│ Médico │ │ │
│ Peticionario │ │ CIMAD │
│ (HCCA) │─────────────────►│ RIS │
│ │ │ │
│ │◄─────────────────│ │
└──────────────────┘ ACK └──────────────────┘
│ │
│ │
│ Cuando informe firmado │
│ ORU^R01 │
│◄─────────────────────────────────────│
Beneficios
- ✅ Eliminación de entrada manual de peticiones
- ✅ Reducción de errores de transcripción
- ✅ Trazabilidad completa del ciclo de vida
- ✅ Cumplimiento normativo de interoperabilidad
🏗️ Arquitectura
Componentes
┌─────────────────────────────────────────────────────────────────┐
│ CIMAD RIS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ MLLP Server │ │ HL7 Parser │ │ Message │ │
│ │ (TCP:2575) │───►│ v2.x │───►│ Router │ │
│ └─────────────────┘ └─────────────────┘ └──────┬──────┘ │
│ │ │
│ ┌─────────────────────────────────────────────┤ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ORM Handler │ │ ADT Handler │ │ QRY Handler │ │
│ │ (Órdenes) │ │ (Pacientes) │ │ (Consultas) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └───────────────────┴────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Cadences │ │
│ │ (Database) │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Stack Tecnológico
| Componente | Tecnología | Notas |
|---|---|---|
| MLLP Server | Node.js (net module) | Puerto 2575 |
| HL7 Parser | node-hl7-complete o custom |
Parseo/serialización |
| Message Queue | Cloudflare Queues | Opcional, para resiliencia |
| Database | Cadences DATA_TABLE | hcca_orders, hl7_messages |
📨 Mensajes HL7
Mensajes Soportados
| Tipo | Trigger | Dirección | Descripción |
|---|---|---|---|
| ORM | O01 | HCCA → CIMAD | Nueva orden/petición |
| ORM | O02 | HCCA → CIMAD | Modificación de orden |
| ORM | O03 | HCCA → CIMAD | Cancelación de orden |
| ORU | R01 | CIMAD → HCCA | Envío de resultados |
| ACK | * | Bidireccional | Confirmación de recepción |
| ADT | A01 | HCCA → CIMAD | Admisión (opcional) |
| ADT | A04 | HCCA → CIMAD | Registro (opcional) |
Estructura ORM^O01 (Nueva Orden)
MSH|^~\&|HCCA|GOVERN|CIMAD|CIMAD|20260118120000||ORM^O01|MSG001|P|2.5
PID|1||12345678A^^^HCCA||GARCIA PEREZ^MARIA||19850315|F|||AV MERITXELL 50^^ANDORRA LA VELLA^^AD500^AD||+376123456
PV1|1|O|||||||||||||||||V001
ORC|NW|ORD001|||||^^^20260120120000||20260118120000|||DR MARTINEZ^JUAN||+376654321
OBR|1|ORD001||TC-TORAX^TC TORAX AMB CONTRAST^LOCAL|||20260120120000||||||||DR MARTINEZ^JUAN|||||||||ROUTINE
Segmentos Clave
MSH (Message Header)
MSH|^~\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|TIMESTAMP||MSG_TYPE|CONTROL_ID|PROC_ID|VERSION
| Campo | Posición | Descripción | Ejemplo |
|---|---|---|---|
| Sending Application | MSH-3 | Sistema origen | HCCA |
| Sending Facility | MSH-4 | Institución origen | GOVERN |
| Receiving Application | MSH-5 | Sistema destino | CIMAD |
| Message Type | MSH-9 | Tipo de mensaje | ORM^O01 |
| Message Control ID | MSH-10 | ID único del mensaje | MSG001 |
| Version | MSH-12 | Versión HL7 | 2.5 |
PID (Patient Identification)
PID|1||PATIENT_ID^^^ASSIGNING_AUTH||LASTNAME^FIRSTNAME||DOB|SEX|||ADDRESS||PHONE
| Campo | Posición | Descripción | Ejemplo |
|---|---|---|---|
| Patient ID | PID-3 | Identificador del paciente | 12345678A |
| Patient Name | PID-5 | Nombre completo | GARCIA PEREZ^MARIA |
| Date of Birth | PID-7 | Fecha nacimiento | 19850315 |
| Sex | PID-8 | Sexo | F |
| Address | PID-11 | Dirección | AV MERITXELL 50^^ANDORRA LA VELLA |
| Phone | PID-13 | Teléfono | +376123456 |
ORC (Common Order)
ORC|ORDER_CONTROL|PLACER_ORDER_NUM|||||SCHEDULED_DATETIME||ORDER_DATETIME|||ORDERING_PROVIDER
| Campo | Posición | Descripción | Ejemplo |
|---|---|---|---|
| Order Control | ORC-1 | Acción | NW (New), CA (Cancel), XO (Change) |
| Placer Order Number | ORC-2 | Nº orden del peticionario | ORD001 |
| Ordering Provider | ORC-12 | Médico solicitante | DR MARTINEZ^JUAN |
OBR (Observation Request)
OBR|SET_ID|PLACER_ORDER_NUM||PROCEDURE_CODE|||SCHEDULED_DATETIME||||||||ORDERING_PROVIDER|||||||||PRIORITY
| Campo | Posición | Descripción | Ejemplo |
|---|---|---|---|
| Placer Order Number | OBR-2 | Nº orden | ORD001 |
| Universal Service ID | OBR-4 | Código del procedimiento | TC-TORAX^TC TORAX AMB CONTRAST |
| Scheduled DateTime | OBR-7 | Fecha/hora programada | 20260120120000 |
| Priority | OBR-27 | Prioridad | ROUTINE, URGENT, STAT |
Estructura ORU^R01 (Resultados)
MSH|^~\&|CIMAD|CIMAD|HCCA|GOVERN|20260121150000||ORU^R01|MSG002|P|2.5
PID|1||12345678A^^^HCCA||GARCIA PEREZ^MARIA||19850315|F
ORC|RE|ORD001||||||||||DR MARTINEZ^JUAN
OBR|1|ORD001|ACC001|TC-TORAX^TC TORAX AMB CONTRAST|||20260120120000|20260120121500|||||||DR MARTINEZ^JUAN|||20260121143000||CT|F||
OBX|1|TX|REPORT^Informe||TC de tórax con contraste intravenoso.~Técnica: Adquisición helicoidal con contraste iodado.~Hallazgos: Parénquima pulmonar sin alteraciones...||||||F
Estructura ACK (Acknowledgment)
MSH|^~\&|CIMAD|CIMAD|HCCA|GOVERN|20260118120001||ACK^O01|ACK001|P|2.5
MSA|AA|MSG001||Message accepted
| Código ACK | Significado |
|---|---|
AA |
Application Accept - Mensaje aceptado |
AE |
Application Error - Error de aplicación |
AR |
Application Reject - Mensaje rechazado |
💻 Implementación
1. MLLP Server
El protocolo MLLP (Minimal Lower Layer Protocol) es el estándar para transportar mensajes HL7 sobre TCP.
// src/lib/hl7/mllp-server.ts
import * as net from 'net';
import { parseHL7Message, createACK } from './hl7-parser';
import { routeMessage } from './message-router';
const MLLP_START = '\x0b'; // VT (Vertical Tab)
const MLLP_END = '\x1c\x0d'; // FS + CR
interface MLLPServerConfig {
port: number;
host?: string;
onMessage: (message: HL7Message) => Promise<HL7Message>;
onError?: (error: Error) => void;
}
export class MLLPServer {
private server: net.Server;
private config: MLLPServerConfig;
constructor(config: MLLPServerConfig) {
this.config = config;
this.server = net.createServer(this.handleConnection.bind(this));
}
async start(): Promise<void> {
return new Promise((resolve, reject) => {
this.server.listen(this.config.port, this.config.host || '0.0.0.0', () => {
console.log(`MLLP Server listening on port ${this.config.port}`);
resolve();
});
this.server.on('error', reject);
});
}
private handleConnection(socket: net.Socket): void {
let buffer = '';
socket.on('data', async (data) => {
buffer += data.toString();
// Buscar mensaje completo (entre MLLP_START y MLLP_END)
const startIdx = buffer.indexOf(MLLP_START);
const endIdx = buffer.indexOf(MLLP_END);
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
const rawMessage = buffer.substring(startIdx + 1, endIdx);
buffer = buffer.substring(endIdx + 2);
try {
// Parsear mensaje HL7
const message = parseHL7Message(rawMessage);
// Procesar y obtener ACK
const ack = await this.config.onMessage(message);
// Enviar ACK
const ackString = MLLP_START + serializeHL7Message(ack) + MLLP_END;
socket.write(ackString);
} catch (error) {
console.error('Error processing HL7 message:', error);
// Enviar ACK de error
const errorAck = createACK(rawMessage, 'AE', error.message);
socket.write(MLLP_START + errorAck + MLLP_END);
this.config.onError?.(error);
}
}
});
socket.on('error', (error) => {
console.error('Socket error:', error);
this.config.onError?.(error);
});
}
stop(): void {
this.server.close();
}
}
2. Parser HL7
// src/lib/hl7/hl7-parser.ts
export interface HL7Message {
raw: string;
segments: HL7Segment[];
messageType: string;
messageControlId: string;
version: string;
}
export interface HL7Segment {
name: string;
fields: string[];
}
export function parseHL7Message(raw: string): HL7Message {
const lines = raw.split('\r').filter(line => line.length > 0);
const segments: HL7Segment[] = [];
for (const line of lines) {
const fields = line.split('|');
const name = fields[0];
segments.push({ name, fields });
}
// Extraer info del MSH
const msh = segments.find(s => s.name === 'MSH');
if (!msh) throw new Error('Missing MSH segment');
return {
raw,
segments,
messageType: msh.fields[8] || '', // MSH-9
messageControlId: msh.fields[9] || '', // MSH-10
version: msh.fields[11] || '2.5', // MSH-12
};
}
export function getSegment(message: HL7Message, name: string): HL7Segment | undefined {
return message.segments.find(s => s.name === name);
}
export function getField(segment: HL7Segment, index: number): string {
return segment.fields[index] || '';
}
export function getComponent(field: string, index: number, separator = '^'): string {
const components = field.split(separator);
return components[index] || '';
}
// Extraer datos del paciente del PID
export function extractPatientData(message: HL7Message): PatientData {
const pid = getSegment(message, 'PID');
if (!pid) throw new Error('Missing PID segment');
const patientId = getComponent(getField(pid, 3), 0);
const lastName = getComponent(getField(pid, 5), 0);
const firstName = getComponent(getField(pid, 5), 1);
const dob = getField(pid, 7);
const sex = getField(pid, 8);
const address = getField(pid, 11);
const phone = getField(pid, 13);
return {
patientId,
lastName,
firstName,
dateOfBirth: parseHL7Date(dob),
sex: sex === 'M' ? 'M' : sex === 'F' ? 'F' : 'O',
address: parseHL7Address(address),
phone,
};
}
// Extraer datos de la orden del ORC/OBR
export function extractOrderData(message: HL7Message): OrderData {
const orc = getSegment(message, 'ORC');
const obr = getSegment(message, 'OBR');
if (!orc || !obr) throw new Error('Missing ORC or OBR segment');
const orderControl = getField(orc, 1);
const placerOrderNumber = getField(orc, 2);
const orderingProvider = getField(orc, 12);
const procedureCode = getComponent(getField(obr, 4), 0);
const procedureName = getComponent(getField(obr, 4), 1);
const scheduledDateTime = getField(obr, 7);
const priority = getField(obr, 27) || 'ROUTINE';
return {
orderControl,
placerOrderNumber,
orderingProvider: parseHL7Name(orderingProvider),
procedureCode,
procedureName,
scheduledDateTime: parseHL7DateTime(scheduledDateTime),
priority: priority as 'ROUTINE' | 'URGENT' | 'STAT',
};
}
// Crear ACK
export function createACK(
originalMessage: HL7Message,
ackCode: 'AA' | 'AE' | 'AR',
errorMessage?: string
): string {
const now = formatHL7DateTime(new Date());
const ackControlId = `ACK${Date.now()}`;
const msh = getSegment(originalMessage, 'MSH');
const sendingApp = getField(msh!, 4); // Swap sender/receiver
const sendingFac = getField(msh!, 5);
const receivingApp = getField(msh!, 2);
const receivingFac = getField(msh!, 3);
let ack = `MSH|^~\\&|${sendingApp}|${sendingFac}|${receivingApp}|${receivingFac}|${now}||ACK^${originalMessage.messageType.split('^')[1]}|${ackControlId}|P|2.5\r`;
ack += `MSA|${ackCode}|${originalMessage.messageControlId}`;
if (errorMessage) {
ack += `||${errorMessage}`;
}
return ack;
}
// Helpers de fecha
function parseHL7Date(hl7Date: string): string {
if (!hl7Date || hl7Date.length < 8) return '';
const year = hl7Date.substring(0, 4);
const month = hl7Date.substring(4, 6);
const day = hl7Date.substring(6, 8);
return `${year}-${month}-${day}`;
}
function parseHL7DateTime(hl7DateTime: string): string {
if (!hl7DateTime || hl7DateTime.length < 12) return parseHL7Date(hl7DateTime);
const date = parseHL7Date(hl7DateTime);
const hour = hl7DateTime.substring(8, 10);
const minute = hl7DateTime.substring(10, 12);
return `${date}T${hour}:${minute}:00`;
}
function formatHL7DateTime(date: Date): string {
return date.toISOString().replace(/[-:T]/g, '').substring(0, 14);
}
3. Message Router
// src/lib/hl7/message-router.ts
import { HL7Message, createACK, extractPatientData, extractOrderData } from './hl7-parser';
import { cadencesClient } from '../cadences';
export async function routeMessage(message: HL7Message): Promise<HL7Message> {
const [messageType, triggerEvent] = message.messageType.split('^');
switch (messageType) {
case 'ORM':
return handleORM(message, triggerEvent);
case 'ADT':
return handleADT(message, triggerEvent);
default:
console.warn(`Unsupported message type: ${messageType}`);
return createACK(message, 'AR', `Unsupported message type: ${messageType}`);
}
}
async function handleORM(message: HL7Message, trigger: string): Promise<HL7Message> {
switch (trigger) {
case 'O01': // Nueva orden
return handleNewOrder(message);
case 'O02': // Modificación
return handleModifyOrder(message);
case 'O03': // Cancelación
return handleCancelOrder(message);
default:
return createACK(message, 'AE', `Unsupported ORM trigger: ${trigger}`);
}
}
async function handleNewOrder(message: HL7Message): Promise<HL7Message> {
try {
// 1. Guardar mensaje raw para auditoría
await saveHL7Message(message, 'inbound');
// 2. Extraer datos
const patientData = extractPatientData(message);
const orderData = extractOrderData(message);
// 3. Buscar o crear paciente
const patient = await findOrCreatePatient(patientData);
// 4. Crear orden HCCA
const order = await cadencesClient.create('hcca_orders', {
hccaOrderId: orderData.placerOrderNumber,
hccaMessageId: message.messageControlId,
// Datos del peticionario
requestingPhysician: orderData.orderingProvider,
requestDate: new Date().toISOString(),
// Datos del paciente (de HCCA)
patientHccaId: patientData.patientId,
patientName: `${patientData.lastName}, ${patientData.firstName}`,
patientDob: patientData.dateOfBirth,
patientSex: patientData.sex,
// Datos de la petición
requestedProcedure: orderData.procedureName,
requestedProcedureCode: orderData.procedureCode,
priority: orderData.priority.toLowerCase(),
scheduledDateTime: orderData.scheduledDateTime,
// Estado inicial
status: 'pending',
linkedPatientId: patient?.id,
});
console.log(`Created HCCA order: ${order.id}`);
// 5. ACK de éxito
return createACK(message, 'AA');
} catch (error) {
console.error('Error handling new order:', error);
return createACK(message, 'AE', error.message);
}
}
async function findOrCreatePatient(patientData: PatientData): Promise<any> {
// Buscar por ID de HCCA
const existing = await cadencesClient.query('patients', {
filters: [
{ field: 'nationalId', operator: 'eq', value: patientData.patientId }
]
});
if (existing.data.length > 0) {
return existing.data[0];
}
// Si no existe, retornar null (se vinculará manualmente)
// O crear automáticamente según configuración
return null;
}
async function saveHL7Message(message: HL7Message, direction: 'inbound' | 'outbound'): Promise<void> {
await cadencesClient.create('hl7_messages', {
messageControlId: message.messageControlId,
messageType: message.messageType,
direction,
rawMessage: message.raw,
status: 'received',
receivedAt: new Date().toISOString(),
});
}
4. Generador ORU^R01 (Envío de Resultados)
// src/lib/hl7/oru-generator.ts
import { formatHL7DateTime } from './hl7-parser';
interface ReportData {
patient: {
id: string;
hccaId: string;
lastName: string;
firstName: string;
dob: string;
sex: string;
};
order: {
hccaOrderId: string;
accessionNumber: string;
procedureCode: string;
procedureName: string;
scheduledDateTime: string;
completedDateTime: string;
orderingProvider: string;
};
report: {
technique: string;
findings: string;
impression: string;
signedBy: string;
signedAt: string;
};
}
export function generateORU(data: ReportData): string {
const now = formatHL7DateTime(new Date());
const msgControlId = `ORU${Date.now()}`;
let message = '';
// MSH
message += `MSH|^~\\&|CIMAD|CIMAD|HCCA|GOVERN|${now}||ORU^R01|${msgControlId}|P|2.5\r`;
// PID
message += `PID|1||${data.patient.hccaId}^^^HCCA||${data.patient.lastName}^${data.patient.firstName}||${data.patient.dob.replace(/-/g, '')}|${data.patient.sex}\r`;
// ORC
message += `ORC|RE|${data.order.hccaOrderId}||||||||||${data.order.orderingProvider}\r`;
// OBR
const scheduledDT = data.order.scheduledDateTime.replace(/[-:T]/g, '').substring(0, 14);
const completedDT = data.order.completedDateTime.replace(/[-:T]/g, '').substring(0, 14);
const signedDT = data.report.signedAt.replace(/[-:T]/g, '').substring(0, 14);
message += `OBR|1|${data.order.hccaOrderId}|${data.order.accessionNumber}|${data.order.procedureCode}^${data.order.procedureName}|||${scheduledDT}|${completedDT}|||||||${data.order.orderingProvider}|||${signedDT}||${getModality(data.order.procedureCode)}|F||\r`;
// OBX - Informe (puede ser múltiples líneas)
let obxSetId = 1;
// Técnica
if (data.report.technique) {
message += createOBX(obxSetId++, 'TECHNIQUE', 'Tècnica', data.report.technique);
}
// Hallazgos
if (data.report.findings) {
message += createOBX(obxSetId++, 'FINDINGS', 'Troballes', data.report.findings);
}
// Impresión/Conclusión
if (data.report.impression) {
message += createOBX(obxSetId++, 'IMPRESSION', 'Impressió', data.report.impression);
}
return message;
}
function createOBX(setId: number, code: string, name: string, value: string): string {
// Escapar caracteres especiales y convertir saltos de línea a ~
const escapedValue = value
.replace(/\|/g, '\\|')
.replace(/\^/g, '\\^')
.replace(/\n/g, '~');
return `OBX|${setId}|TX|${code}^${name}||${escapedValue}||||||F\r`;
}
function getModality(procedureCode: string): string {
// Mapear código de procedimiento a modalidad DICOM
const code = procedureCode.toUpperCase();
if (code.includes('TC') || code.includes('CT')) return 'CT';
if (code.includes('RM') || code.includes('MR')) return 'MR';
if (code.includes('ECO') || code.includes('US')) return 'US';
if (code.includes('RX') || code.includes('XR')) return 'DX';
if (code.includes('MAMMO') || code.includes('MG')) return 'MG';
return 'OT';
}
🔄 Flujos de Trabajo
Flujo 1: Recepción de Petición
┌─────────────┐
│ HCCA │
│ ORM^O01 │
└──────┬──────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 1. Recibir mensaje MLLP │
│ 2. Parsear HL7 │
│ 3. Validar estructura │
│ 4. Guardar mensaje raw (auditoría) │
│ 5. Extraer datos paciente (PID) │
│ 6. Extraer datos orden (ORC/OBR) │
│ 7. Buscar paciente existente por DNI/HCCA ID │
│ 8. Crear registro en hcca_orders │
│ 9. Enviar ACK^O01 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ UI: Peticiones HCCA Pendientes │
│ - Lista de peticiones sin programar │
│ - Opción: Vincular paciente existente │
│ - Opción: Crear paciente nuevo │
│ - Opción: Programar cita │
└─────────────────────────────────────────────────────────┘
Flujo 2: Envío de Resultados
┌─────────────────────────────────────────────────────────┐
│ Trigger: Informe firmado por radiólogo │
└──────┬──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 1. Detectar que estudio tiene orden HCCA vinculada │
│ 2. Obtener datos del paciente │
│ 3. Obtener datos de la orden original │
│ 4. Obtener contenido del informe │
│ 5. Generar mensaje ORU^R01 │
│ 6. Enviar a HCCA via MLLP │
│ 7. Esperar ACK │
│ 8. Actualizar estado de la orden │
│ 9. Guardar mensaje enviado (auditoría) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────┐
│ HCCA │
│ Recibe ORU │
└─────────────┘
⚙️ Configuración
Variables de Entorno
# Servidor MLLP (recepción)
HCCA_MLLP_HOST=0.0.0.0
HCCA_MLLP_PORT=2575
# Identificadores
HCCA_SENDING_FACILITY=CIMAD
HCCA_SENDING_APPLICATION=CIMAD_RIS
HCCA_RECEIVING_FACILITY=GOVERN
HCCA_RECEIVING_APPLICATION=HCCA
# Cliente MLLP (envío a HCCA)
HCCA_OUTBOUND_HOST=hcca.govern.ad
HCCA_OUTBOUND_PORT=2575
HCCA_OUTBOUND_TIMEOUT=30000
# Opciones
HCCA_AUTO_CREATE_PATIENT=false
HCCA_AUTO_SCHEDULE=false
Configuración en Cadences
// Configuración almacenada en DATA_TABLE config
{
key: 'hcca_integration',
value: JSON.stringify({
enabled: true,
mllpPort: 2575,
sendingFacility: 'CIMAD',
autoCreatePatient: false,
autoSchedule: false,
defaultPriority: 'routine',
procedureMappings: {
'TC-TORAX': 'SRV-001', // Mapeo código HCCA → nuestro serviceId
'RM-CRANEO': 'SRV-002',
// ...
}
})
}
🧪 Testing
Test Unitarios
// __tests__/hl7-parser.test.ts
import { parseHL7Message, extractPatientData, extractOrderData } from '../src/lib/hl7/hl7-parser';
const sampleORM = `MSH|^~\\&|HCCA|GOVERN|CIMAD|CIMAD|20260118120000||ORM^O01|MSG001|P|2.5\rPID|1||12345678A^^^HCCA||GARCIA PEREZ^MARIA||19850315|F|||AV MERITXELL 50^^ANDORRA LA VELLA^^AD500^AD||+376123456\rORC|NW|ORD001|||||^^^20260120120000||20260118120000|||DR MARTINEZ^JUAN\rOBR|1|ORD001||TC-TORAX^TC TORAX AMB CONTRAST|||20260120120000||||||||DR MARTINEZ^JUAN|||||||||ROUTINE`;
describe('HL7 Parser', () => {
test('parses MSH correctly', () => {
const msg = parseHL7Message(sampleORM);
expect(msg.messageType).toBe('ORM^O01');
expect(msg.messageControlId).toBe('MSG001');
expect(msg.version).toBe('2.5');
});
test('extracts patient data', () => {
const msg = parseHL7Message(sampleORM);
const patient = extractPatientData(msg);
expect(patient.patientId).toBe('12345678A');
expect(patient.lastName).toBe('GARCIA PEREZ');
expect(patient.firstName).toBe('MARIA');
expect(patient.sex).toBe('F');
});
test('extracts order data', () => {
const msg = parseHL7Message(sampleORM);
const order = extractOrderData(msg);
expect(order.placerOrderNumber).toBe('ORD001');
expect(order.procedureCode).toBe('TC-TORAX');
expect(order.priority).toBe('ROUTINE');
});
});
Test de Integración
# Enviar mensaje de prueba con netcat
echo -e "\x0bMSH|^~\&|TEST|TEST|CIMAD|CIMAD|20260118120000||ORM^O01|TEST001|P|2.5\rPID|1||TEST123|||TEST^PATIENT\rORC|NW|ORD001\rOBR|1|ORD001||TEST-PROC\x1c\r" | nc localhost 2575
Simulador HCCA
Para testing sin conexión real a HCCA:
// scripts/hcca-simulator.ts
// Simula el envío de mensajes ORM desde HCCA
import * as net from 'net';
const MLLP_START = '\x0b';
const MLLP_END = '\x1c\x0d';
function sendTestOrder() {
const client = new net.Socket();
client.connect(2575, 'localhost', () => {
const message = generateTestORM();
client.write(MLLP_START + message + MLLP_END);
});
client.on('data', (data) => {
console.log('Received ACK:', data.toString());
client.destroy();
});
}
function generateTestORM(): string {
const now = new Date().toISOString().replace(/[-:T.Z]/g, '').substring(0, 14);
const orderId = `ORD${Date.now()}`;
return `MSH|^~\\&|HCCA|GOVERN|CIMAD|CIMAD|${now}||ORM^O01|${orderId}|P|2.5\r` +
`PID|1||TEST-${Date.now()}^^^HCCA||PACIENTE^PRUEBA||19900101|M|||CALLE TEST 1^^ANDORRA||+376600000\r` +
`ORC|NW|${orderId}|||||^^^${now}||${now}|||DR TEST^MEDICO\r` +
`OBR|1|${orderId}||TC-TORAX^TC TORAX|||${now}||||||||DR TEST^MEDICO|||||||||ROUTINE`;
}
sendTestOrder();
📋 Checklist de Implementación
Fase 1: Infraestructura
- Configurar servidor MLLP
- Implementar parser HL7 básico
- Crear DATA_TABLEs
hcca_ordersyhl7_messages - Configurar variables de entorno
Fase 2: Recepción
- Procesar ORM^O01 (nueva orden)
- Procesar ORM^O02 (modificación)
- Procesar ORM^O03 (cancelación)
- Generar ACK correctos
- UI de peticiones pendientes
Fase 3: Envío
- Generador ORU^R01
- Cliente MLLP para envío
- Trigger al firmar informe
- Reintentos en caso de fallo
Fase 4: Testing
- Tests unitarios del parser
- Tests de integración
- Simulador HCCA
- Pruebas con HCCA real (entorno de test)
Sprint 2 - Enero/Febrero 2026