Receiving a document
Regulation (EU) 2025/327 asks an EHR system that stores, intermediates or gives access to health data to “be able to receive personal electronic health data in the European electronic health record exchange format, by means of the European interoperability software component” (Annex II, points 2.2 and 2.3). FerroFED intermediates, so it must receive. It stores nothing of its own, so a received document goes to one member: the one the deployment declares for the document’s category, as the Federation Tier sends every new object to one explicitly chosen node (§2.3, N23).
The interoperability component, crates/eehrxf, reads a received document,
checks it against its profiles and maps it into openEHR. The gateway takes
the document at POST {fhir-base}/Bundle and writes the composition to the
declared member, with an access record of each receipt
(The patient summary over FHIR).
The information sheet’s list of the categories received is planned
(#803).
What the component accepts
A received document is a FHIR R4 Bundle in JSON. The component reads it in
this order and refuses it at the first rule it breaks:
- An object that repeats a name is refused before anything reads the text, because two JSON readers may keep different values for it (RFC 8259, §4).
- The text is decoded against the R4 definitions of
Bundleand of every resource it carries. An unknown property, a value of the wrong type, anullor an empty object or array, and an invalid primitive are refused, with the element path where they were found, and so is a resource of a type R4 does not define, anywhere in the document. The decoded document must encode back to the JSON it was read from. Every later rule, the profile check and the mapping read that one decoded document. - The
Bundleis adocument, and meets the R4 rules for one: anidentifierwith a system and a value (bdl-9), atimestamp(bdl-10),fullUrls given once (bdl-7), none version specific (bdl-8), each REST-style one ending with its resource’s type and id, and aCompositionas its first entry (bdl-11). - The
Composition.subjectnames onePatiententry of the sameBundle, by itsfullUrl, or by a relativePatient/<id>resolved against the server base of the composition’s ownfullUrl. Every reference in the document is found by its R4 type, at any depth: in backbone elements, contained resources and extensions, and resolved by one resolver,receive::reference::resolve, which answers exactly one entry or a typed refusal. It reads a reference and afullUrlin one canonical spelling only: a relativeType/id, anhttporhttpsURL with a lower-case host,urn:uuid:with a lower-case UUID,urn:oid:, a_historyversion matched on the entry’smeta.versionId, and a#idinto the resource’s owncontained. Whitespace, a query string, percent-encoding, a trailing or doubled slash, a dot segment and a resource type in another case are refused, so no reader can take one of them for an entry the resolver would not. Atypeor anidentifierbeside the reference must agree with its target. Asubject,patient,beneficiaryorformust name that same entry by reference. Any other reference may name it, an entry of another type, a contained resource by#id, or a resource outside the document whose path names a type other thanPatient; one that could name another patient (a path or atypethat saysPatient, or a URN no entry carries) is refused, and so is atypethat disagrees with the reference’s target. A secondPatiententry and a containedPatientare refused, and so is an element the R4 element table does not describe. A document is registered under the identification data of the one person it is about (Art 13(3)), so a document that could be read as about two is refused whole.
The document’s text is kept byte for byte. No refusal quotes a value of the
document, so a patient identifier never reaches an error message or a log
line through one. Whether the caller may write for that patient, and whether
the member’s ehr_id is that patient’s, the federation half decides before
it sends anything (see below).
The check against the profiles
The document is then checked against the HL7 Europe profiles of its
category, read from the vendored package: for a patient summary,
bundle-eu-eps and composition-eu-eps of hl7.fhir.eu.eps 1.0.0-ballot.
At every occurrence of every element the profile’s snapshot lists, the check
holds:
- the cardinality, counted per occurrence of the parent element;
- the value a
fixed[x]orpattern[x]requires, for aCodeableConcept, aCodingor a string-valued primitive; - the slices of a sliced element, told apart by
value,pattern,existsandtypediscriminators. Each slice is held to its own cardinality, and aclosedslicing refuses an occurrence no slice admits. The five sections the EPS composition requires are slices of this kind. An entry of a resource type no slice of theBundleprofile names is refused, although the profile’s slicing is open, so no entry passes unread; - the invariants of one form: every entry of the listed types carries a
reference at one element, as the EPS
eps-bundle-subject-refandeps-bundle-patient-refrequire. Every other invariant is listed as not evaluated.
Every finding is reported with the profile, the element id and the place in
the document, such as Composition.section[2]. None is dropped.
The check says what it cannot evaluate, and lists it beside a passing result:
- a
profilediscriminator, such as the one that tells the EPSBundleentries apart by the profile of their resource; - a discriminator path through
resolve(), such as the one that tells a section’s entries apart by the type of the resource they reference; - the order of an ordered slicing;
- a
fixed[x]orpattern[x]form it does not read, by its key.
A slice such a discriminator tells apart is still held to its lower bound over the occurrences the other discriminators admit. Other invariants, terminology bindings and the profiles of the other entries are not checked yet (#808). The two example documents the EPS package publishes pass the check.
Mapped into openEHR, with the original kept
The component maps the document into one openEHR composition through the
FHIRconnect mapping files the deployment supplies, run in process by
FerroBRIDGE’s fhirconnect engine. The whole document is the input of
FHIRconnect’s $toopenehr, so the context that maps it is the one whose
profile the document’s resources claim, and a mapping may follow the
document’s references into its other entries. A document no context maps,
or one with two resources of the type a context maps, is refused with the
engine’s reason.
The composition keeps the document itself. The openEHR RM lets
FEEDER_AUDIT.original_content carry the original content inline, and puts
one at the composition to establish the equivalence between the whole
composition and the whole document (RM 1.1.0, Common Information Model,
“Original Content”). The component sets it to a DV_PARSABLE holding the
document’s text with the formalism application/fhir+json, beside the
engine’s own audit of the system the content passed through. What the
mapping does not carry into structured content is therefore still in the
record, and what the run declared lost comes back beside the composition as
an OperationOutcome.
Written to the declared member
The federation half takes the composition and the document’s patient. It
resolves the patient from every identifier of the document’s Patient in a
namespace fhir.receive_namespaces names, at the declared member alone, and
every one must name the same ehr_id there: identifiers that name two
patients, or a patient the member holds no EHR for, are refused before
anything is sent. A caller whose grant is confined to one patient writes
only into that patient’s EHR. The composition then goes to the member as an
ITS-REST composition_create, with no patient identifier in the request
path, query string or headers (§5.4.1, N33); the body keeps the received
document, which is the clinical content the client sent. A member that
refuses the composition or does not answer is an error that names it, never
a success.
Hazards
The clinical safety risk file lists the receive path as I-10 (a document filed under the wrong member or patient), whose controls are built, and I-11 (a document stored with content lost), which stays open until the gateway validates a received document in full (#808).