Route a veterinary lab result
A veterinary lab result is an HL7 v2 ORU^R01 like any other, with one difference that matters on
the way to FHIR: the patient is an animal. PID-35 holds its species and PID-36 its breed, and a
channel that drops them hands the receiver a result it cannot tell from a person's. This page shows
how a channel keeps them end to end, with the
veterinary lab example as the project to copy, and says plainly what
we have tested and what we have not.
Keep species and breed from end to end
The example's channel, vet-results-to-fhir, does four things. Its files are on
the example's page.
-
Take the result over MLLP. An MLLP source stores each message, then acknowledges it. The channel's
format: hl7v2hands the transform the message parsed by@cosyte/hl7. -
Accept one exact shape, then read species and breed by name. The transform filters out every message that is not an
ORU^R01, and fails a result unless it has this shape:- exactly one MSH segment.
@cosyte/hl7reads a segment id in any case, so a second header in the same text,MSHormsh, starts a second message, and its results would be sent on as the first patient's; - exactly one PID segment. A second patient group, a person's or another animal's, would have its results sent on as the first patient's;
- no patient or visit segment (PID, PD1, NK1, PV1 or PV2) after the first OBR. In an
ORU^R01these open the message, so one after an order starts a second patient's group; - a control id in MSH-10, not empty and not only spaces, from which the Bundle's entry ids are made;
- PID-35 holding exactly one species, as
CODE^Text^SYSTEM: one repetition, the code and the coding system each one plain value (no subcomponent, no escape sequence, no space, and not the HL7 null""), and a text with no subcomponent. Nothing may follow the coding system; - PID-36 empty, or holding exactly one breed of the same shape.
The transform reads the fields' wire text through
@cosyte/hl7, under the message's own delimiters, so a message with other encoding characters in MSH-2 is held to the same shape. A result with no species fails, so a person's result is not sent on as an animal's. Anything else fails too, never trimmed to its first part:msg.patientreads only the first repetition and the first subcomponent, so a second species, a code in a subcomponent or an escaped delimiter would otherwise be dropped or sent on as a code no lab sent. We chose to fail such a result rather than guess: a failed message is kept, and replay sends it again once it is fixed.Once the shape holds,
msg.patient.speciesandmsg.patient.breedgive PID-35 and PID-36 as coded elements, each with itsidentifier,textandnameOfCodingSystem.@cosyte/hl7infers neither from anything else.The text (the second component) is not checked against the code: it is free display text, and the transform does not hold a list of the codes it knows.
CAT^Dog^99COSYTEis sent with the codeCATand the displayDog. If your receiver shows the display, check the pair against your lab's code list in the transform. - exactly one MSH segment.
-
Convert with
@cosyte/transform.toFhirturns the message into a FHIR R4 message Bundle. The species and breed go into the Patient'spatient-animalextension (http://hl7.org/fhir/StructureDefinition/patient-animal), each avalueCodeableConcept. The transform then reads the extension back and fails the message unless it carries the same species and breed codes the lab sent, so a later change in either library cannot drop them quietly. -
POST the Bundle. An HTTP destination sends it as
application/fhir+jsonto the FHIR server's$process-messageURL.
The extension's third part, genderStatus, is never written: PID-8 is administrative sex and does
not say whether an animal is neutered. PID-37 strain and PID-38 production class have no place in the
extension, so toFhir leaves them out of the Bundle and says so in its issues.
Check what toFhir reports
toFhir returns the Bundle and a list of issues: codes, locations and severities, never a value from
the message. The example's transform keeps the Bundle and does not act on the issues. On its
synthetic result they include:
TRANSFORM_RESOURCE_INVALIDatOBR: the synthetic OBR has no result status (OBR-25), which a FHIRDiagnosticReportrequires, sotoFhirwithholds theDiagnosticReportinstead of sending one that is not valid. TheObservationfor each result is still in the Bundle. A lab that values OBR-25 gets itsDiagnosticReport.TRANSFORM_CODE_SYSTEM_UNRESOLVEDfor the species and breed: their codes are from a local code system,99COSYTE, so each coding carries its code and display and nosystem.toFhirnever invents a system URI. The transform still requires the coding system in the message, so a step you add that maps a lab's local codes to a standard terminology knows which list a code is from.TRANSFORM_UNIT_NOT_UCUMfor units it cannot read as UCUM, kept as written.
Before a real feed, decide which issues fail a message for your receiver, and throw for those in the transform. A thrown error fails the message and keeps it for replay.
What we test
CI runs these on every change to Pathways, on synthetic messages only:
pathways teston the example: the lab result gives the Bundle in its expected file, byte for byte, and the admission gives nothing.- A real pass, as the example's page walks through it: a
running engine takes the result and the admission over MLLP and acknowledges both, POSTs one Bundle
to a stand-in receiver, and filters the admission. The Bundle the receiver got matches the expected
file, and its Patient's
patient-animalextension holds the species and breed codes that PID-35 and PID-36 of the message carried. Each entry of the Bundle passes@cosyte/fhir'svalidateResourcewith no error. - The transform fails a lab result with a second PID (a person's patient group after the animal's,
a person's PID right after the animal's, a second animal's PID), one followed in the same text by
a second header (
msh,MSHorMsh) and a person's OBR and OBX, one with a PV1 (upper or lower case), PD1, PV2 or NK1 after an OBX followed by another OBR and OBX, and one whose MSH-10 is empty, HL7 null or only spaces. It fails one whose species is absent, empty, HL7 null (""), text with no code, repeated, a code with no coding system, two codes or two coding systems in subcomponents, the HL7 null as its code or coding system, an escaped delimiter or a hex escape in its code, or an alternate code after its coding system, and one whose breed is HL7 null, repeated, has no code or no coding system, two codes in subcomponents, the HL7 null as its code or coding system, or an escaped delimiter in its code. It reads PID-35 the same way under other delimiters (#as the component separator). It sends one with a coded species and no breed. - The test messages are the ones
@cosyte/synthgenerates for the seeds the example's README names, so they can be made again. - The page's channel file passes the channel schema check, and its transform compiles against Pathways' types.
What we do not test
- No real lab. No laboratory system, reference lab interface or veterinary practice system has
sent this channel a message. The messages are made by
@cosyte/synth, and a real lab's messages will differ in fields, codes and quirks. Test with your lab's own interface specification, on synthetic or approved test messages, before you go live. - No NAHLN onboarding. We have not onboarded with the National Animal Health Laboratory Network or tested against its messaging requirements, and this example does not produce a message in the network's own format. Reporting to NAHLN is outside what this page covers.
- No standard species or breed codes. The species and breed are from
@cosyte/synth's short list under the local code system99COSYTE, not SNOMED CT or any other veterinary terminology. Pathways and@cosyte/transformcarry whatever codes the lab sends; mapping a lab's local codes to a standard terminology is for your transform. - No real FHIR server. The receiver is a stand-in that writes what it gets. No FHIR server has
processed the Bundle at
$process-message; the stand-in answers 200 to every request. - No TLS. The example runs plain TCP and HTTP on the loopback. A real lab connects over TLS, and
a real FHIR server is an
https://URL: see Secure endpoints.
Related
- Example: a veterinary lab result to FHIR, the project to copy.
- Send a lab result as v2 XML over HTTPS: the same kind of result in the HL7 v2 XML encoding, settled by the XML acknowledgement.
- Transform and filter messages: what a transform can return.
- Make sample messages with
@cosyte/synth. - Test a channel with its sample messages.