# EDIFACT in healthcare tutorial - Receive lab results and send letters

Receive laboratory results, route clinical messages by type and send letters to GPs - healthcare EDIFACT from Python services.

With Zato, you can receive, route and send healthcare EDIFACT from Python services, and this tutorial will show you how - from an incoming lab result to an outgoing letter to a GP.

```yaml
name: Clinical EDIFACT flow
actors:
  - id: lab
    label: Laboratory
  - id: channel
    label: EDIFACT Inbox channel
  - id: service
    label: Your service
  - id: gp
    label: GP system
steps:
  - from: lab
    to: channel
    label: A MEDLAB result arrives as the request body
    align: left
  - from: channel
    to: service
    label: Invoked with the wire text
  - from: service
    to: channel
    kind: return
    label: Parsed with typed access, each determination logged
  - from: channel
    to: lab
    kind: return
    label: 200 OK confirms receipt
    align: left
  - from: service
    to: gp
    label: A MEDVRI letter, built and serialized in Python, is sent
```

The example messages come from the Dutch healthcare dialect that ships with the platform, and every step works the same with [any dialect you define](https://zato.io/docs/dev/healthcare/edifact/dialects/index.html).

## Setting up Zato {#setting-up-zato}

1. Install Zato through [Docker](https://zato.io/docs/admin/guide/install/docker.html)
2. In your browser, go to [http://localhost:8183](http://localhost:8183) and log into the Dashboard

## Receive a lab result {#receive-a-lab-result}

### Write the service {#write-the-service}

In Zato, incoming messages are handled by services - Python classes that a channel invokes with each message it accepts.

With the Dashboard open, go to the IDE, create a file called `edifact_api.py`, paste the code below and click **Deploy**:

```python
# -*- coding: utf-8 -*-

# Zato
# Importing a dialect package registers its message classes -
# here, the Dutch healthcare dialect shipped as an example.
import zato.edifact.nl

from zato.edifact import parse_edifact
from zato.server.service import Service

# #####################################################################
# #####################################################################

class LabResults(Service):
    """ Receives laboratory results sent as EDIFACT interchanges.
    """

    def handle(self) -> 'None':

        # The wire text, as the channel delivered it ..
        raw = self.request.input

        # .. one call parses the envelope and the message in it ..
        interchange = parse_edifact(raw)
        msg = interchange.message

        # .. the UNB header names the sending laboratory ..
        sender = interchange.header.sender.identification
        message_type = msg.unh.identifier.message_type

        self.logger.info(f'Received {message_type} from {sender}')

        # .. and each specimen comes with its determinations.
        for material in msg.materials:
            for determination in material.determinations:
                value = f'{determination.result} {determination.unit}'
                self.logger.info(f'{determination.determination}: {value}')
```

After deployment, the service is available under the name `edifact-api.lab-results` - derived from the file and class names - and what it needs now is a channel to invoke it.

### Create the channel {#create-the-channel}

The interchange text arrives over the carrier the trading partners agreed on - a mailbox network, a file drop or an HTTP endpoint - and here a REST channel accepts it. Mailbox polling and file drops are covered in [transport patterns](https://zato.io/docs/dev/healthcare/edifact/transport/index.html).

Go to Connections ▹ Channels ▹ REST, click Create a new REST channel and fill in the form:

1. **Name:** EDIFACT Inbox
2. **URL path:** /edifact
3. **Service:** edifact-api.lab-results
4. **Security:** No security definition
5. Click **OK**

[Video: New REST channel](https://zatosource-production.b-cdn.net/docs/gfx/channels/edifact-rest-create.webm?v=1791298935)

Dashboard menu: Connections > Channels > REST

### Send a test message {#send-a-test-message}

Save this MEDLAB interchange - one blood specimen with three hematology determinations - as `lab-result.edi`:

```edifact
UNB+UNOA:1+500000101+500000201+260831:0930+2601'
UNH+2601+MEDLAB:1'
ZKH+Streeklab Rijnmond+Wytemaweg:80::Rotterdam:3015CN+?+31107033800'
PID+1975:03:18+V+Dijk:van der:Peters:de::M.++BSN999990019'
AFD+Klinische chemie'
ARA+Dr. E. Verhoeven'
DET+26:08:30+14:15'
IDE+C+26082254+bloed'
SEC+HEMATOLOGIE'
BEP+1+Hemoglobine+8.6++mmol/l+N+8.5+11.0'
BEP+1+Hematocriet+0.42++l/l+N+0.40+0.50'
BEP+1+Leukocyten+12.4++10*9/l+H+4.0+10.0'
UNT+12+2601'
UNZ+1+2601'
```

Post it to the channel:

```bash
curl http://localhost:11223/edifact --data-binary @lab-result.edi
```

The server log shows what your code read - the sender from UNB and every determination with its value and unit:

```text
INFO - Received MEDLAB from 500000101
INFO - Hemoglobine: 8.6 mmol/l
INFO - Hematocriet: 0.42 l/l
INFO - Leukocyten: 12.4 10*9/l
```

Note that the `?+` escape in the laboratory's phone number was unescaped for you, and the `MEDLAB:1` identity in UNH is what resolved the message to its typed class.

## Navigate messages with typed access {#navigate-messages-with-typed-access}

Every segment the message class declares is an attribute, and so is every element and component - demographics and laboratory details read like plain Python:

```python
msg = interchange.message

# Composites have typed components
family_name = msg.pid.patient_name.married_name   # 'Dijk'
born_year   = msg.pid.date_of_birth.year          # '1975'

# The sending laboratory and its department
lab        = msg.hospital.institution_name        # 'Streeklab Rijnmond'
department = msg.department.department            # 'Klinische chemie'
```

Repeating structures are lists - a lab result has one group per specimen, each with its own determinations, and every value comes with its reference range:

```python
for material in msg.materials:
    for determination in material.determinations:

        # The laboratory flagged this value as above range
        if determination.normality_indicator == 'H':
            name  = determination.determination
            value = f'{determination.result} {determination.unit}'
            limit = determination.upper_limit
            self.logger.info(f'Above range: {name} {value}, upper limit {limit}')
```

The test message has one such value, and the log points it out:

```text
INFO - Above range: Leukocyten 12.4 10*9/l, upper limit 10.0
```

The full access model is described in [EDIFACT field access](https://zato.io/docs/dev/healthcare/edifact/field-access/index.html).

## Route by message type {#route-by-message-type}

Lab results are not the only traffic - letters, radiology reports and specialist letters arrive as EDIFACT messages too, and the UNH identity tells them apart, so a routing service reads it and passes each message to its handler:

```python
class Router(Service):
    """ Passes each message of an interchange to its handler.
    """

    def handle(self) -> 'None':

        # Message types and the services that handle them ..
        handlers = {
            'MEDLAB': 'edifact-api.lab-results',
            'MEDVRI': 'edifact-api.letters',
        }

        # .. one interchange may bundle several messages ..
        interchange = parse_edifact(self.request.input)

        for msg in interchange.messages:

            # .. the UNH identity determines where each one goes ..
            message_type = msg.unh.identifier.message_type

            if service_name := handlers.get(message_type):
                _ = self.invoke(service_name, msg=msg)

            # .. and unknown types are still parsed into fully navigable messages.
            else:
                self.logger.info(f'Skipping {message_type}')
```

### Round trips are byte-exact {#round-trips-are-byte-exact}

The systems that receive healthcare EDIFACT are often decades old and strict about their input - an import job that has parsed the same layout since the nineties rejects an interchange whose separators changed or whose empty elements moved. For this reason, a parsed interchange serializes back to its exact wire bytes, and a message a handler passes on arrives as the laboratory produced it. When one field has to change, a corrected patient number for instance, assign to it and only that segment is re-serialized:

```python
interchange = parse_edifact(raw)

# The wire text, reproduced byte for byte
wire_text = interchange.serialize()

# One assignment, one changed segment - the rest stays as received
msg = interchange.message
msg.pid.sex = 'M'
modified = interchange.serialize()
```

## Send a letter to a GP {#send-a-letter-to-a-gp}

Messages go the other way too - a treatment update to the patient's GP is a MEDVRI message, built with the same typed classes that parsing uses.

The letter needs a way out first - an outgoing REST connection pointed at the mailbox provider. Go to Connections ▹ Outgoing ▹ REST, click Create a new outgoing REST connection and fill in the form:

1. **Name:** EDI Mailbox
2. **Host:** https://mailbox.example.com
3. **URL path:** /messages
4. **Security:** No security definition
5. Click **OK**

[Video: New outgoing REST connection](https://zatosource-production.b-cdn.net/docs/gfx/outgoing/edifact-mailbox-create.webm?v=1791298930)

Dashboard menu: Connections > Outgoing > REST

Now add this service to `edifact_api.py` and deploy it:

```python
# Zato
from zato.edifact.nl import MEDVRI
from zato.edifact.nl.segments import TXT

# #####################################################################
# #####################################################################

class SendLetter(Service):
    """ Builds a letter to a GP and sends it to the mailbox network.
    """

    def handle(self) -> 'None':

        # A new, empty letter ..
        letter = MEDVRI()

        # .. its UNH header holds the reference and the message identity ..
        letter.unh.reference_number = '8001'
        letter.unh.identifier.message_type = 'MEDVRI'
        letter.unh.identifier.version = '1'

        # .. the sending practitioner and institution ..
        letter.sender.person_name      = 'E. Vermeer'
        letter.sender.institution_name = 'GGZ De Linde'

        # .. the date the letter was written ..
        letter.det.date.year  = '26'
        letter.det.date.month = '08'
        letter.det.date.day   = '31'

        # .. the patient it concerns ..
        letter.pid.date_of_birth.year  = '1975'
        letter.pid.date_of_birth.month = '03'
        letter.pid.date_of_birth.day   = '18'
        letter.pid.sex = 'V'
        letter.pid.patient_name.married_name        = 'Dijk'
        letter.pid.patient_name.married_name_prefix = 'van der'

        # .. the letter's text, one TXT segment per line ..
        line_1 = TXT()
        line_1.text = 'Dear colleague'

        line_2 = TXT()
        line_2.text = 'Your patient has been in our care for six months now'

        letter.text = [line_1, line_2]

        # .. the UNT trailer closes the message ..
        letter.unt.segment_count    = '7'
        letter.unt.reference_number = '8001'

        # .. serialization produces the wire text ..
        wire_text = letter.serialize()

        # .. the connection created a moment ago is looked up by name ..
        conn = self.rest['EDI Mailbox']

        # .. and one POST delivers the letter to the mailbox.
        _ = conn.post(self.cid, wire_text)

        self.logger.info(f'Sent letter {letter.unh.reference_number}')
```

This is the wire text the mailbox receives:

```edifact
UNH+8001+MEDVRI:1'
GGA+E. Vermeer++GGZ De Linde'
DET+26:08:31'
PID+1975:03:18+V+Dijk:van der'
TXT+Dear colleague'
TXT+Your patient has been in our care for six months now'
UNT+7+8001'
```

Note that unlike the lab result from earlier, this wire text has no UNB-UNZ envelope - mailbox networks often deliver bare messages and `parse_edifact` accepts both forms.

## Convert between formats {#convert-between-formats}

The lab result received earlier does not have to stay EDIFACT - to post it to a REST API or store it in a reporting database, convert it to a dict or JSON:

```python
interchange = parse_edifact(raw)

data      = interchange.to_dict()
json_text = interchange.to_json(indent=2)

# Skip empty elements for compact output
compact = interchange.to_json(include_empty=False)
```

## And that's the whole loop {#and-thats-the-whole-loop}

A REST channel accepts the interchange, typed access reads it, and the same classes build what goes back out. The chapters below cover the details whenever you need more.

## What you built {#what-you-built}

- A **REST channel** that accepts EDIFACT interchanges and invokes your service with each one
- **Typed access** - patient demographics, specimens and determinations as named Python attributes
- A **router** that reads the UNH identity and passes each message type to its handler
- A **MEDVRI letter** built from typed classes, serialized and sent out over an outgoing connection
- **Byte-exact round trips** - forwarded messages arrive as the sender produced them
- **JSON conversion** - `to_dict` and `to_json` for REST APIs, queues and reporting stores

## What next {#what-next}

- [Connect your AI](https://zato.io/docs/ai/connect-your-copilot.html) to ask more questions about Zato and to build your interfaces
- Point the channel at your own dialect and transports - define the message classes and everything on this page works unchanged
- Receive HL7 v2 alongside EDIFACT with the [HL7 MLLP tutorial](https://zato.io/tutorials/healthcare/hl7v2.html)

## Where to go next {#where-to-go-next}

- [The EDIFACT engine](https://zato.io/docs/dev/healthcare/edifact/index.html) - Overview of parsing, building and serialization
- [Message parsing](https://zato.io/docs/dev/healthcare/edifact/parsing/index.html) - Envelopes, separators and round trips in depth
- [Field access](https://zato.io/docs/dev/healthcare/edifact/field-access/index.html) - Typed segments, composites and repeating groups
- [Dialects and profiles](https://zato.io/docs/dev/healthcare/edifact/dialects/index.html) - Define and register your own message types
- [Transport patterns](https://zato.io/docs/dev/healthcare/edifact/transport/index.html) - Mailboxes, file drops and REST carriers
- [EDIFACT in healthcare](https://zato.io/integrations/healthcare/edifact-healthcare-messaging/index.html) - Where EDIFACT remains in use in healthcare
