Correspondance FHIR R4
Generated from the source of truth:
apps/ehr-lab/src/infrastructure/fhir/mappers.ts(to<X>/from<X>functions + theSYSobject) andrepositories.ts(search parameters). This document faithfully describes the FHIR shape produced/read by the app — regenerate it if the mappers change.
Cross-cutting conventions: the FHIR store (Blaze) is the app's only datastore (directory, charts, messaging, documents, audit). References are written Type/${id} and read back by the last segment. Searches are bounded (_count=200) and sorted app-side (Blaze only sorts on _lastUpdated). All app-specific system/identifiers/extensions are grouped in the SYS object of the mappers. No authentication: the chosen identity signs the audit.
Patient & directory
patient — Patient (clinical identity)
FHIR resource: Patient (FHIR R4 standard, no profile; toPatient/fromPatient mappers)
Minimal clinical identity: last name/first name, sex, date of birth, INS identifier. The whole chart references the patient by its id. toPatient always emits identifier(INS)+name+gender+birthDate+active; register() rejects an INS that is already present. GROUP-WIDE GLOBAL CONVENTIONS: references written Type/${id} and read back via refId()=last segment; COUNT=200 everywhere, sorting APP-SIDE (localeCompare 'fr'), Blaze only sorts on _lastUpdated; systems in the SYS object of mappers.ts.
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
Patient.id |
— | Assigned by Blaze on creation; never written by toPatient. fromPatient requires r.id (r.id!). | |
familyName |
Patient.name[0].family |
✔ | toPatient: name:[{family, given:[givenName]}]. Read name[0].family, fallback "?". validatePatientDraft requires it (non-empty trim). | |
givenName |
Patient.name[0].given[0] |
✔ | A single first name: given is a 1-element array. Read name[0].given[0], fallback "". Required by validatePatientDraft. | |
sex |
Patient.gender |
FHIR administrative-gender codes: male | female | unknown (no explicit system written; raw value) | ✔ | Sex domain (male|female|unknown) mapped 1:1 to gender. Read: male/female preserved, any other value (including other/null) → unknown. |
birthDate |
Patient.birthDate |
✔ | ISO YYYY-MM-DD. validatePatientDraft enforces the format ^\d{4}-\d{2}-\d{2}$ and rejects a future date. Read fallback "". | |
ins |
Patient.identifier[?(system==SYS.ins)].value |
SYS.ins = urn:oid:1.2.250.1.213.1.4.8 (INS-NIR) | ✔ | 15-digit INS/NIR identifier (13 + check key), normalizeIns strips spaces/./-. toPatient: identifier:[{system:SYS.ins, value:ins}] (the only identifier). Read: identifier whose system==SYS.ins. search() discards any patient without ins. |
Markers (identifiers · categories · extensions · status/priority)
- INS identifier (uniqueness key) —
urn:oid:1.2.250.1.213.1.4.8 \| <NIR 15 digits>· SYS.ins constant. Unique identifier set; register() searches${SYS.ins}\|<ins>and throws DomainError if a chart already exists. - status / active —
active = true· Fixed boolean set at creation (no status field on Patient). - resourceType —
Patient
Search: FhirPatients.search(query): input cleaned (removes \s.-); if /^\d{4,}$/ → params identifier=${SYS.ins}|<digits>, otherwise name=. Adds _count=200. Results mapped, filtered on non-empty p.ins, sorted by familyName (fr). byId(id): http.read Patient/
${SYS.ins}|<ins> (reject duplicate) then http.create(toPatient).
Notes: PatientDraft = same fields without id; validatePatientDraft normalizes/validates before register. No business status (soft-delete not implemented for Patient).
practitioner — Healthcare professional
FHIR resource: Practitioner (FHIR R4 standard; toPractitioner/fromPractitioner mappers)
Directory caregiver (freely chosen identity, no auth). qualification carries the profession. toPractitioner takes Omit<Practitioner,'id'> & {key}; used at seeding — the repo (FhirDirectory) only reads practitioners.
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
Practitioner.id |
— | Assigned by Blaze; fromPractitioner requires r.id. | |
familyName |
Practitioner.name[0].family |
✔ | toPractitioner: name:[{family, given:[givenName]}]. Read name[0].family, fallback "?". | |
givenName |
Practitioner.name[0].given[0] |
✔ | Single first name (given[0]). Read fallback "". | |
title |
Practitioner.name[0].prefix[0] |
— | Carried title (« Dr », « Pr »). Written only when present; read back as prefix[0] || undefined. STORED rather than derived — the profession does not determine it. When absent, practitionerTitle() (domain, not FHIR) falls back to the profession's customary title. |
|
profession |
Practitioner.qualification[0].code.coding[?(system==SYS.profession)].code |
SYS.profession = urn:ehr-lab:profession ; MOCKED value-set: physician | nurse | resident | pharmacist | midwife | ✔ | toPractitioner: qualification:[{code:{coding:[{system:SYS.profession, code:profession}]}}]. Read: coding whose system==SYS.profession; fallback "physician" if missing/unknown. |
key (outside the domain type) |
Practitioner.identifier[?(system==SYS.practitioner)].value |
SYS.practitioner = urn:ehr-lab:practitioner | ✔ | Stable business key supplied on write (toPractitioner receives {key}), NOT exposed in the Practitioner interface and NOT read back by fromPractitioner. Used only as identifier value (and to target the directory search). |
Markers (identifiers · categories · extensions · status/priority)
- practitioner identifier —
urn:ehr-lab:practitioner \| <key>· SYS.practitioner constant; system used alone (identifier=${SYS.practitioner}\|) to list the directory. - qualification (profession) —
code.coding.system = urn:ehr-lab:profession· SYS.profession constant; code ∈ mocked ProfessionCode. - status / active —
active = true· Fixed at creation.
Search: FhirDirectory.practitioners(): http.search Practitioner { identifier:${SYS.practitioner}| (system alone, any value), _count:200 } → map fromPractitioner, sort by familyName (fr). No practitioner creation/modification via the repo (toPractitioner reserved for seeding).
Notes: practitionerDisplay(p, label?) formats "Firstname LASTNAME — profession label" on the domain side. ProfessionCode is a mocked value-set, replaceable by a real reference.
service — Care unit (CareService)
FHIR resource: Organization (type = department; toOrganization/fromOrganization mappers)
Facility service/unit (short code + name). Referenced as the serviceProvider of visits and as recipient (Organization/…) in messaging.
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
Organization.id |
— | Assigned by Blaze; fromOrganization requires r.id; also serves as fallback for code and name. | |
code |
Organization.identifier[?(system==SYS.service)].value |
SYS.service = urn:ehr-lab:service | ✔ | Short code (URG, MED, CAR…). toOrganization: identifier:[{system:SYS.service, value:code}]. Read: value of the SYS.service identifier, fallback to r.id if missing. |
name |
Organization.name |
✔ | toOrganization: name = s.name. Read: r.name, fallback "Service". |
Markers (identifiers · categories · extensions · status/priority)
- service identifier —
urn:ehr-lab:service \| <code>· SYS.service constant; system alone (identifier=${SYS.service}\|) to list services. - type (hospital department) —
http://terminology.hl7.org/CodeSystem/organization-type \| dept \| Hospital Department· SYS.orgType constant. type:[{coding:[{system:SYS.orgType, code:'dept', display:'Hospital Department'}]}] — fixed, not read back by fromOrganization. - status / active —
active = true· Fixed at creation.
Search: FhirDirectory.services(): http.search Organization { identifier:${SYS.service}|, _count:200 } → map fromOrganization, sort by name (fr).
Notes: No service creation/modification via the repo (toOrganization reserved for seeding).
caregiver↔service assignment (ServiceAssignment)
FHIR resource: PractitionerRole (link only, no coded role; toPractitionerRole/fromPractitionerRole mappers)
N–N link "this caregiver handles the patients of this service". Used for messaging routing (the threads/urgent items of the service's charts are visible to its team). The (professional, service) pair identifies the assignment — search before create (idempotent).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
practitionerId |
PractitionerRole.practitioner.reference |
✔ | Writes Practitioner/${practitionerId}. Read: refId(practitioner.reference) (last segment after '/'), fallback "". |
|
serviceId |
PractitionerRole.organization.reference |
✔ | Writes Organization/${serviceId}. Read: refId(organization.reference), fallback "". |
Markers (identifiers · categories · extensions · status/priority)
- status / active —
active = true· No code (no PractitionerRole.code): the only semantics is the professional↔service link. assignments() keeps only active=true, then filters roles missing practitionerId or serviceId.
Search: FhirDirectory.assignments(): search PractitionerRole { active:'true', _count:200 } → map fromPractitionerRole, filter (practitionerId && serviceId). Idempotence findRole(a): search { practitioner:Practitioner/${id}, organization:Organization/${id}, _count:'1' } → rows[0]?.id. assign(a): if findRole exists → no-op, otherwise http.create(toPractitionerRole). unassign(a): findRole then http.delete PractitionerRole/
Notes: No own identifier: uniqueness rests on the (practitioner, organization) pair via findRole. No business status; active always true at creation.
Visits (Encounter)
Venue (encounter.ts — Venue aggregate / episode of care)
FHIR resource: Encounter (FHIR R4 standard, no profile). Two modes carried by the same resource: ambulatory consultation (class AMB) and hospitalization (class IMP).
Care of a patient in a service by a professional, over a period. (Venue = the domain aggregate for a hospital visit / episode of care.) Written by toEncounter, read back by fromEncounter (mappers.ts:170-229). The transitions (openEncounter/transfer/discharge/closeConsultation) are PURE domain functions (encounter.ts); the infra only persists via create/update. Strictly clinical scope (neither billing nor logistics).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
Encounter.id |
— | On WRITE present only for an update (toEncounter emits id only if e.id exists); on creation it is absent (generated by Blaze). On READ required (fromEncounter does r.id!). | |
class ('ambulatory' | 'inpatient') |
Encounter.class.code (+ .system, .display) |
http://terminology.hl7.org/CodeSystem/v3-ActCode (SYS.encounterClass) | ✔ | class is a SIMPLE Coding (not a CodeableConcept). Write: inpatient→code 'IMP' display 'inpatient encounter', otherwise 'AMB' display 'ambulatory'. Read (fromEncounter): code 'IMP'→'inpatient', ANY OTHER code→'ambulatory'. Only IMP/AMB are mapped. |
status ('in-progress' | 'finished') |
Encounter.status |
✔ | Native FHIR EncounterStatus code (no system). Write: the business value is written as-is (e.status). Read: 'finished'→'finished', ANY OTHER value→'in-progress'. Only these two values are used by the app. | |
patientId |
Encounter.subject.reference |
Patient/{id} | ✔ | Literal reference Patient/${e.patientId}. Read via refId(subject.reference). |
serviceId (responsible service) |
Encounter.serviceProvider.reference |
Organization/{id} | ✔ | Reference Organization/${e.serviceId}. Transfer (domain transfer()) modifies ONLY this field. Read via refId(serviceProvider.reference). |
practitionerId (professional who initiated the visit) |
Encounter.participant[0].individual.reference |
Practitioner/{id} | ✔ | A single participant, no coded type/role: Practitioner/${e.practitionerId}. Read: participant[0].individual.reference. |
reason (clinical reason, free text) |
Encounter.reasonCode[0].text |
— | reasonCode is an array; the whole block is emitted only if e.reason OR e.reasonCode is present. text = e.reason. Read: reasonCode[0].text. | |
reasonCode (structured reason, Coded: code/display/system) |
Encounter.reasonCode[0].coding[0] (.code, .display, .system) |
urn:posos:cim10 (SYS.cim10) expected, but NOT enforced | — | Written in the same reasonCode[0] as the text, with coding=[{ system: e.reasonCode.system, code, display }] — the system comes from the Coded object (ICD-10 via the terminology), it is not imposed by the mapper. Read: takes coding[0] WITHOUT a system filter; display falls back to code if missing. |
start |
Encounter.period.start |
✔ | Always emitted (ISO). Domain: iso() guarantees a valid date at opening. Read: period.start ?? ''. | |
end |
Encounter.period.end |
— | Emitted only if e.end present. Set by discharge() (hospital discharge) and closeConsultation() on the transition to 'finished'. Read: period.end (optional). | |
dischargeDisposition (discharge outcome) |
Encounter.hospitalization.dischargeDisposition.coding[0].code |
urn:ehr-lab:discharge-disposition (SYS.dischargeDisposition) | — | hospitalization block emitted only if e.dischargeDisposition present: coding=[{ system: SYS.dischargeDisposition, code }] (no display on write). Read: hospitalization.dischargeDisposition.coding[0].code. MOCKED value-set (MockTerminology.dischargeDispositions): home, other-hcf, rehab, long, aadvice, exp. |
Markers (identifiers · categories · extensions · status/priority)
- resourceType (fixed) —
Encounter· Standard FHIR R4 resource, no profile/meta.profile set by the mapper. - CLASS value-set (Encounter.class) —
http://terminology.hl7.org/CodeSystem/v3-ActCode \| IMP (inpatient encounter) · AMB (ambulatory)· SYS.encounterClass. Two codes only; class is a simple Coding. Used as the 'class' search param on the repo side (see query). - FHIR status —
in-progress \| finished· Subset of EncounterStatus, written/read without system. Encounter has NO priority in this model. - OUTCOME value-set (dischargeDisposition) —
urn:ehr-lab:discharge-disposition \| home · other-hcf · rehab · long · aadvice · exp· SYS.dischargeDisposition. Value-set entirely MOCKED in MockTerminology (infrastructure/terminology/mock.ts): home=discharge home, other-hcf=transfer to another facility, rehab=rehabilitation care (SSR), long=long-term care (EHPAD nursing home), aadvice=against medical advice, exp=death (deceased). - structured-reason system (reasonCode.coding) —
urn:posos:cim10 (SYS.cim10)· ICD-10 expected but the system is taken from the supplied Coded object, not imposed by toEncounter; the read filters on no system (coding[0]). - identifier / category / extension / priority —
NONE· The Encounter carries NEITHER identifier, NOR category, NOR extension, NOR priority in the mappers — unlike other resources (Patient, MedicationRequest, Communication…).
Search: byId: GET Encounter/{id} (http.read). forPatient(patientId): search Encounter {subject:'Patient/{id}', _count:200}, app-side sort start desc. Helper ofService(serviceId): {'service-provider':'Organization/{id}', _include:'Encounter:subject', _count:200, ...}. inpatientsOf(serviceId): ofService + {class:'IMP', status:'in-progress'}, sort start desc. consultationsOf(serviceId, sinceIso): ofService + {class:'AMB', date:'ge{YYYY-MM-DD}'}, sort in-progress first then start desc. create/update: http.create/update(toEncounter). Search params used: subject, service-provider, class, status, date, _include=Encounter:subject.
Notes: Sorting always APP-SIDE (Blaze only sorts on _lastUpdated), _count bounded at 200. _include:'Encounter:subject' brings Patients into the same bundle for service views (ofService rebuilds a Map id→Patient and drops Encounters with no resolved patient). The server search param is named 'service-provider' (with a hyphen) whereas the FHIR field is serviceProvider. No entered-in-error / soft-delete filter on the Encounter (unlike Condition/Allergy/Document).
Problems/Past history & Allergies
history — Past-history item / Current problem (MedicalHistoryItem)
FHIR resource: Condition (R4). A SINGLE resource serves two business uses, distinguished by toHistoryCondition(h, confirmed): confirmed=false → PAST HISTORY (background terrain, category=problem-list-item, WITHOUT verificationStatus) ; confirmed=true → CURRENT PROBLEM / established diagnosis (category=encounter-diagnosis + verificationStatus=confirmed).
Background chart data (patient background), coded via SNOMED/ICD-10 terminology when available, free-text label otherwise. Mappers: toHistoryCondition/fromHistoryCondition (apps/ehr-lab/src/infrastructure/fhir/mappers.ts). Repo: FhirHistory (repositories.ts). The write never depends on a visit (no encounter field).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
Condition.id |
✔ | Assigned by the store (r.id!). Omitted on creation. | |
patientId |
subject.reference |
✔ | Writes Patient/${patientId}; read back via refId(). CAUTION: Condition uses subject (unlike AllergyIntolerance which uses patient). |
|
label |
code.text |
✔ | Always written (= h.label). On read: code.text ?? code.coding[0].display ?? "?". | |
coding.code |
code.coding[0].code |
— | code.coding block written only if h.coding present. On read, coding kept only if code.coding[0].code is defined (otherwise coding=undefined). | |
coding.display |
code.coding[0].display |
— | On read: display ?? code (falls back to code if display missing). | |
coding.system |
code.coding[0].system |
— | Not fixed by the mapper: comes from the terminology (h.coding.system). May be SYS.cim10 = "urn:posos:cim10" or a SNOMED system, depending on the entry. No default value. | |
recordedAt |
recordedDate |
✔ | ISO string. On read: recordedDate ?? "". | |
recordedBy |
recorder.reference |
✔ | Writes Practitioner/${recordedBy}; read back via refId(). |
|
coverageAck.at |
extension[?(url==SYS.coverageAck)].valueAnnotation.time |
SYS.coverageAck = urn:ehr-lab:coverage-ack | — | Coverage acknowledgement ("seen, no treatment expected") on a CURRENT PROBLEM. Written by FhirHistory.setCoverageAck, never by toHistoryCondition. Read back only if time is present. |
coverageAck.by |
extension[?(url==SYS.coverageAck)].valueAnnotation.authorReference |
— | — | Writes Practitioner/${by}; read via refId(). Comes from Annotation, the standard R4 type that already carries author + time — nothing invented for it. |
Markers (identifiers · categories · extensions · status/priority)
- clinicalStatus (clinical status, always written) —
http://terminology.hl7.org/CodeSystem/condition-clinical \| active· System written as a literal in the mapper (NOT via SYS). Alwaysactive, for both variants (past history and problem). - verificationStatus (CURRENT PROBLEM only) —
http://terminology.hl7.org/CodeSystem/condition-ver-status \| confirmed· Written ONLY if confirmed=true (createProblem). Absent for a past-history item. System as a literal. This status is what surfaces the Condition as an active problem and excludes it from past history. - verificationStatus — soft-delete —
http://terminology.hl7.org/CodeSystem/condition-ver-status \| entered-in-error· FhirHistory.remove() does not delete: it re-reads the Condition and rewrites verificationStatus=entered-in-error (Blaze forbids physical deletion because it is referenced by the audit). Filtered on read via isEnteredInError(). - category — PAST HISTORY (confirmed=false) —
SYS.conditionCategory (http://terminology.hl7.org/CodeSystem/condition-category) \| problem-list-item· display="Problem List Item". category=[{coding:[{...}]}]. The only case that actually uses the SYS.conditionCategory constant. - category — CURRENT PROBLEM (confirmed=true) —
http://terminology.hl7.org/CodeSystem/condition-category \| encounter-diagnosis· display="Encounter Diagnosis". System written as a LITERAL (same URL as SYS.conditionCategory but not referenced via the constant). - extension — coverage acknowledgement —
SYS.coverageAck (urn:ehr-lab:coverage-ack) \| valueAnnotation· Set/cleared by FhirHistory.setCoverageAck (read Condition, rebuild extensions minus ours, PUT). The Annotation carries authorReference + time + a fixed French text. Clearing writesextension: []rather than dropping the field, so the removal is explicit. Read by fromHistoryCondition into MedicalHistoryItem.coverageAck.
Search: FhirHistory.forPatient (PAST HISTORY): GET Condition?subject=Patient/{id}&category=problem-list-item&_count=200, then app-side filter !isEnteredInError(verificationStatus), sort recordedAt desc. FhirHistory.problemsOf (CURRENT PROBLEMS): GET Condition?subject=Patient/{id}&clinical-status=active&verification-status=confirmed&_count=200, same filter + sort. create → toHistoryCondition(item) [confirmed=false] ; createProblem → toHistoryCondition(item, true) ; remove → read Condition/{id} then update verificationStatus=entered-in-error. No byId and no search param on the code.
Notes: setCoverageAck is the only writer of the coverage extension and it preserves every other extension on the resource. The mapper does NOT set an onset field (no onsetDateTime/onsetString written or read), contrary to what the lead suggested — do not invent it. No encounter, severity, note, or bodySite field. The read takes code.coding[0] directly (first coding, no system filter, unlike Observation which goes through codingOf). The past-history vs problem distinction rests entirely on category + presence of verificationStatus=confirmed, never on a domain flag (MedicalHistoryItem carries no confirmed field).
problem — Problem on one of the four lists (Problem)
FHIR resource: Condition (R4). Mappers: toProblemCondition/fromProblemCondition (apps/ehr-lab/src/infrastructure/fhir/problem-mappers.ts) — a module of their own, distinct from the MedicalHistoryItem mappers above, which they do NOT replace: the two read the same resources through different models (see Backward compatibility).
The four-list model (domain/problem.ts): active, stay, multidisciplinary, history. Repo: FhirHistory.allProblems / addProblem / updateProblem / attachmentsOf / setAttachedObservations (repositories.ts). SYS for these three URNs lives in infrastructure/fhir/systems.ts, a leaf module with no imports, so the domain tests can read it without the @/ alias.
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
Condition.id |
✔ | Assigned by the store. Omitted on creation. | |
patientId |
subject.reference |
✔ | Patient/${patientId}; read via refId(). |
|
label |
code.text |
✔ | Always written. On read: code.text ?? coding[0].display ?? "?". |
|
coding |
code.coding[0] |
— | Written only if present. An uncoded problem is accepted and flagged in the UI, never rejected. | |
list |
category[0].coding[0].code |
SYS.problemList = urn:ehr-lab:problem-list | ✔ | OUR category, written FIRST — it is what governs the read. |
list (standard mirror) |
category[1].coding[0].code |
SYS.conditionCategory | problem-list-item · encounter-diagnosis | ✔ | Nearest standard category, for third-party readers: stay → encounter-diagnosis, the other three → problem-list-item. Never written instead of ours — the standard codes cannot carry multidisciplinary nor separate active from history. |
status |
clinicalStatus + verificationStatus |
condition-clinical · condition-ver-status | ✔ | provisional → clinical active + verification provisional ; active → active + confirmed ; resolved → clinical resolved, verification unchanged (confirmed) ; refuted → verification refuted + clinical inactive. |
parentId |
extension[?(url==SYS.parentProblem)].valueReference |
SYS.parentProblem = urn:ehr-lab:parent-problem | — | The umbrella. Extension for want of a standard element: Condition has no parent link. |
supersedes.id |
extension[?(url==SYS.supersedes)].valueReference.reference |
SYS.supersedes = urn:ehr-lab:supersedes | — | Written by precise(): the hypothesis this diagnosis replaced. |
supersedes.label |
extension[?(url==SYS.supersedes)].valueReference.display |
— | — | The display carries the label so the replaced hypothesis stays readable without a second round-trip. |
encounterId |
encounter.reference |
— | Set on a stay problem: the visit it is the focus of, which is what allows reviewing it at discharge. |
|
recordedAt / recordedBy |
recordedDate / recorder.reference |
✔ | Same convention as the history item. | |
coverageAck |
extension[?(url==SYS.coverageAck)].valueAnnotation |
SYS.coverageAck | — | Same extension as the history item — Annotation (authorReference + time). Written by setCoverageAck. |
organSystems |
extension[?(url==SYS.organSystem)].valueCode (REPEATED) |
SYS.organSystem = urn:ehr-lab:organ-system | — | Organ-system categories from the SNOMED subsumption classifier (PMC7062335), computed AT ENTRY and persisted. Repeated because multiple membership is the normal case (19.4 % in the paper). DERIVED data: a third-party reader can ignore it, and pnpm --filter ehr-lab classify:problems recomputes it. Read back only for codes this version of the domain knows — an unknown code is dropped rather than invented. |
| attached lab results | evidence[0].detail[*].reference |
— | — | Observation/${id}, MANY-TO-MANY: an observation may be carried by several problems. evidenceDetail([]) writes nothing rather than an empty block. |
Markers
evidence.detailfor lab results, notObservation.reason—Observationhas NOreasonelement in R4, so the link is carried problem-side, where the specification puts it.evidencemeans "what supports": exact for a creatinine under renal failure, slightly stretched for a monitoring INR. The stretch is preferred over inventing an extension — a third-party FHIR reader understandsevidence.Treatments are NOT written here — the treatment ↔ problem link already exists on the prescription (
MedicationRequest.reasonReference/reasonCode, from the indication of the selected regimen) and is derived at read time. Duplicating it problem-side would create two truths.updateProblemre-reads then merges —{...r, ...rebuilt, id}: evidence, notes and foreign extensions survive an edit of the list, status, label, code or umbrella.refuted= the assertion is withdrawn. A ruled-out or replaced hypothesis. The READ tests verification before clinical status: the resource carriesclinicalStatus: inactive, and reading that first would map it back toresolved, i.e. to a disease the patient had. Such a Condition is filtered out of every list bybuildProblemBoard, and its trace is thesupersedesreference on the replacement.scripts/repair-refuted-hypotheses.tsrepairs the ones written before this status existed (signature: referenced by another Condition'ssupersedesand filed as resolved past history); it writes nothing without--write.
Backward compatibility (read): a Condition without our category is read under the OLD convention — encounter-diagnosis or verificationStatus=confirmed → active, everything else → history. unconfirmed and differential are read as provisional: they say the same thing, and ignoring them would let another system's hypothesis pass for an established diagnosis. An existing chart must not empty itself because the model changed; the seed exercises this path for real.
Search: allProblems → GET Condition?subject=Patient/{id}&_count=200, app-side isEnteredInError filter, then fromProblemCondition. Note it does NOT filter on category: the four lists live in one query, and legacy Conditions have to come back too.
allergy — Allergy / Intolerance (Allergy)
FHIR resource: AllergyIntolerance (R4). toAllergyIntolerance/fromAllergyIntolerance mappers (mappers.ts). FhirAllergies repo (repositories.ts).
Background chart data (anamnesis), coded via the Posos terminology (active ingredient / excipient / allergen class) or a free-text label. Independent of visits.
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
AllergyIntolerance.id |
✔ | Assigned by the store (r.id!). | |
patientId |
patient.reference |
✔ | Writes Patient/${patientId}; read back via refId(). CAUTION: patient field (NOT subject) — hence the patient search param in the repo. |
|
label |
code.text |
✔ | Always written (= a.label). On read: code.text ?? code.coding[0].display ?? "?". | |
coding.code |
code.coding[0].code |
— | code.coding block written only if a.coding present. On read, coding kept only if code.coding[0].code defined. | |
coding.display |
code.coding[0].display |
— | On read: display ?? code. | |
coding.system |
code.coding[0].system |
— | Not fixed by the mapper: comes from the Posos terminology (a.coding.system). No default value. | |
recordedAt |
recordedDate |
✔ | ISO string. On read: recordedDate ?? "". | |
recordedBy |
recorder.reference |
✔ | Writes Practitioner/${recordedBy}; read back via refId(). |
Markers (identifiers · categories · extensions · status/priority)
- clinicalStatus (always written) —
http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical \| active· Local constant ALLERGY_CLINICAL in mappers.ts (not in SYS). Alwaysactiveat creation. Not read back by fromAllergyIntolerance. - verificationStatus (always written) —
http://terminology.hl7.org/CodeSystem/allergyintolerance-verification \| confirmed· Local constant ALLERGY_VERIF (not in SYS). Alwaysconfirmedat creation. - verificationStatus — soft-delete —
http://terminology.hl7.org/CodeSystem/allergyintolerance-verification \| entered-in-error· FhirAllergies.remove() re-reads then rewrites verificationStatus=entered-in-error (referenced by the audit → no physical deletion). Filtered on read via isEnteredInError().
Search: FhirAllergies.forPatient: GET AllergyIntolerance?patient=Patient/{id}&_count=200, then app-side filter !isEnteredInError(verificationStatus), map fromAllergyIntolerance, sort recordedAt desc. create → toAllergyIntolerance(item). remove → read AllergyIntolerance/{id} then update verificationStatus=entered-in-error. No byId, no search param on criticality/category.
Notes: IMPORTANT — fields from the lead ABSENT from the mappers, NOT to be invented: criticality, category (food/medication/environment/biologic), type (allergy/intolerance), reaction/manifestation, onset. None are written or read. The only clinical granularity is code + label; clinicalStatus/verificationStatus are fixed (active/confirmed). The repo's search param is patient (aligned with the patient.reference field, different from Condition which uses subject).
Medications
Prescription (domain/medication.ts — hospital prescription line, with optional BIM dosage)
FHIR resource: MedicationRequest (R4) — toMedicationRequest / fromMedicationRequest mappers
The INTENT to prescribe a medication (identified by a code from the Posos terminology), attached to a visit and a prescriber. intent always 'order'. When the prescriber entered a structured dosage (guided prescription flow) or picked a recommended dosage from the Posos MedicalDB (BIM), it is carried as a structured dosageInstruction[0] (text, route, doseAndRate, timing.repeat — including intake moments when and infusion duration) plus reasonCode for the chosen indication. When the guided flow computed an administration plan, its summary rides on the dispense-plan extension and the individual planned intakes are materialized as Tasks (see PlannedAdministration below). May be a discharge prescription (category discharge). Discontinuation materialized by status 'stopped' + stop-date extension (and cancels the remaining planned-intake Tasks).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
id |
— | Written ONLY if provided (update branch: ...(p.id ? { id } : {})). On read: r.id! (non-null ensured). | |
patientId |
subject.reference |
Patient/{id} | ✔ | Writes Patient/${p.patientId}. Read via refId(subject.reference). |
encounterId |
encounter.reference |
Encounter/{id} | ✔ | Writes Encounter/${p.encounterId}. Read via refId(encounter.reference). |
drug.code |
medicationCodeableConcept.coding[0].code |
✔ | System carried by coding[0].system (see drug.system). Read via codingOf(r,'medicationCodeableConcept') WITHOUT system filter → takes the 1st coding found. Default '?' if missing. | |
drug.display |
medicationCodeableConcept.coding[0].display AND medicationCodeableConcept.text |
✔ | Written both in coding[0].display and in .text. Read: coding.display, otherwise fallback to medicationCodeableConcept.text, otherwise '?'. | |
drug.system |
medicationCodeableConcept.coding[0].system |
variable (pass-through): e.g. urn:posos:cis, urn:posos:medicabase | ✔ | Medication terminology system passed through as-is from DrugCoding; NOT a SYS constant (examples urn:posos:cis / urn:posos:medicabase cited in the domain docstrings). Read: med.system, otherwise ''. |
status |
status |
✔ | Write: 'active' if status==='active', otherwise 'stopped' (no other value emitted). Read: 'active' if r.status==='active', otherwise 'stopped'. | |
prescriberId |
requester.reference |
Practitioner/{id} | ✔ | Writes Practitioner/${p.prescriberId}. Read via refId(requester.reference). |
authoredAt |
authoredOn |
✔ | ISO creation timestamp (domain: new Date().toISOString()). Read: r.authoredOn, otherwise ''. | |
stoppedAt |
extension[url=urn:ehr-lab:stopped-at].valueDateTime |
urn:ehr-lab:stopped-at (SYS.stoppedAt) | — | Extension emitted ONLY if p.stoppedAt defined. Read: extension whose url===SYS.stoppedAt → valueDateTime. |
isDischarge |
category[].coding[](system=…medicationrequest-category, code=discharge) |
http://terminology.hl7.org/CodeSystem/medicationrequest-category (SYS.mrCategory) | discharge | — | category emitted ONLY if p.isDischarge. Fixed coding {system:SYS.mrCategory, code:'discharge', display:'Discharge'}. Read: isDischarge=true if a category coding has system===SYS.mrCategory AND code==='discharge' (otherwise field omitted). |
dosage.text |
dosageInstruction[0].text |
— | Whole dosageInstruction emitted ONLY if p.dosage present. Semi-structured sentence built from BIM parts (dose · frequency · duration · route). Read: a dosageInstruction[0] without text is ignored. |
|
dosage.dose |
dosageInstruction[0].doseAndRate[0].doseQuantity | .doseRange |
UCUM (ucum → http://unitsofmeasure.org) |
— | Quantity when the BIM gives a single value, Range (low/high) otherwise; unit + UCUM code passed through (incl. mg/kg). |
dosage.timing |
dosageInstruction[0].timing.repeat |
— | frequency/frequencyMax/period/periodMax/periodUnit (UCUM time code straight from the BIM: h, d…), boundsDuration (Quantity) or boundsRange. | |
dosage.timing.whens |
dosageInstruction[0].timing.repeat.when[] |
HL7 event-timing codes (MORN, NOON, EVE, HS) | — | Intake moments from the structured entry. Read back as-is. |
dosage.timing.adminDuration |
dosageInstruction[0].timing.repeat.duration + .durationUnit |
UCUM (min) | — | Duration of ONE administration (infusion). Emitted when the structured entry sets it. |
dispensePlan |
extension[url=urn:ehr-lab:dispense-plan].valueString |
urn:ehr-lab:dispense-plan (SYS.dispensePlan) | — | Compact JSON summary {intakeText, times?, occurrenceCount?} of the retained administration plan (intake composition in specialties + daily times). The occurrences themselves are Tasks. Read tolerantly: unparseable JSON → field absent. |
dosage.route |
dosageInstruction[0].route |
urn:posos:roa (SYS.roa) | — | route.text = designation; coding emitted only when the BIM provides an EDQM ROA code. |
dosage.instructions |
dosageInstruction[0].additionalInstruction[].text |
— | BIM guidelines (conduites à tenir), text-only. | |
dosage.nature |
dosageInstruction[0].extension[url=urn:ehr-lab:dosage-nature].valueCoding |
urn:ehr-lab:dosage-nature (SYS.dosageNature) | — | BIM nature (STANDARD, ATTACK, MAXIMAL…) as code + French label as display — context FHIR has no slot for. |
dosage.sourceLabel |
dosageInstruction[0].extension[url=urn:ehr-lab:dosage-source].valueString |
urn:ehr-lab:dosage-source (SYS.dosageSource) | — | Source of the recommendation (e.g. « Résumé des Caractéristiques du Produit »). |
dosage.indication |
reasonCode[0] |
cim10→urn:posos:cim10, snomed→http://snomed.info/sct, meddra→urn:posos:meddra | — | reasonCode.text = indication label (condition + modifiers); coding emitted when the BIM condition carries one (short BIM terminology keys mapped to the app's canonical systems). |
Markers (identifiers · categories · extensions · status/priority)
- resourceType —
MedicationRequest· Fixed. - intent (FHIR) —
order· Fixed, always 'order' (no other intent emitted). - status —
active \| stopped· Fixed to these two values (direct mapping of the domain status). - category (discharge prescription) —
SYS.mrCategory = http://terminology.hl7.org/CodeSystem/medicationrequest-category ; code=discharge, display=Discharge· Conditional (isDischarge). Distinguishes the discharge-prescription line from hospital treatment. - stop-date extension —
SYS.stoppedAt = urn:ehr-lab:stopped-at (valueDateTime)· Conditional (stoppedAt present). - dosage-nature extension —
SYS.dosageNature = urn:ehr-lab:dosage-nature (valueCoding)· Conditional (dosage picked). BIM nature of the regimen, needed for a lossless round-trip. - dosage-source extension —
SYS.dosageSource = urn:ehr-lab:dosage-source (valueString)· Conditional (source known). - dispense-plan extension —
SYS.dispensePlan = urn:ehr-lab:dispense-plan (valueString, compact JSON)· Conditional (guided flow computed a plan).
Search: byId: http.read('MedicationRequest', id). forPatient: GET MedicationRequest?subject=Patient/{patientId}&_count=200, then sort authoredAt descending APP-SIDE (byDateDesc). create → http.create(toMedicationRequest) ; update → http.update(toMedicationRequest, with id).
Notes: No business identifier on the resource. Discontinuing a line = update status→stopped + stoppedAt extension (no deletion). The dosage is OPTIONAL: a line prescribed without picking a BIM recommendation carries no dosageInstruction/reasonCode, exactly as before. The client-sent dosage payload is defensively re-sanitized server-side (sanitizeDosage) before reaching the mapper.
HomeMedication (domain/medication.ts — usual / home treatment, collected at admission)
FHIR resource: MedicationStatement (R4) — toMedicationStatement / fromMedicationStatement mappers
A STATEMENT (the patient takes…), distinct from the hospital prescription: collected at admission (manual entry or Scan&Go transcription). Always status 'active' on write; deletion = soft-delete status 'entered-in-error' (drops out of lists because the search is status=active).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
id |
— | Not emitted on creation (no id branch in toMedicationStatement). Read: r.id!. | |
patientId |
subject.reference |
Patient/{id} | ✔ | Writes Patient/${m.patientId}. Read via refId(subject.reference). |
drug.code |
medicationCodeableConcept.coding[0].code |
✔ | Read via codingOf(r,'medicationCodeableConcept') without system filter; default '?'. | |
drug.display |
medicationCodeableConcept.coding[0].display AND medicationCodeableConcept.text |
✔ | Written in coding[0].display and .text. Read: coding.display, otherwise fallback to .text, otherwise '?'. | |
drug.system |
medicationCodeableConcept.coding[0].system |
variable (pass-through): e.g. urn:posos:cis, urn:posos:medicabase | ✔ | Passed through as-is from DrugCoding; not a SYS constant. Read: med.system, otherwise ''. |
recordedAt |
dateAsserted |
✔ | ISO timestamp of collection (draftHomeMedication: new Date().toISOString()). Read: r.dateAsserted, otherwise ''. | |
recordedBy |
informationSource.reference |
Practitioner/{id} | ✔ | Writes Practitioner/${m.recordedBy}. Read via refId(informationSource.reference). |
Markers (identifiers · categories · extensions · status/priority)
- resourceType —
MedicationStatement· Fixed. - status —
active (write) ; entered-in-error (soft-delete)· toMedicationStatement forces 'active'. remove() does update status→'entered-in-error' ; since forPatient filters status=active, the entry disappears from lists.
Search: forPatient: GET MedicationStatement?subject=Patient/{patientId}&status=active&_count=200, sort recordedAt descending APP-SIDE. create → http.create(toMedicationStatement). remove(id): read then update status→entered-in-error.
Notes: No encounter (unlike the prescription): it's a patient statement, not attached to a visit. No business identifier.
Administration (domain/medication.ts — administration of a dose at a point in time)
FHIR resource: MedicationAdministration (R4) — toMedicationAdministration / fromMedicationAdministration mappers
The FACT that a dose was given at a point in time, ALWAYS attached to a prescription line (request → MedicationRequest). status always 'completed'.
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
id |
— | Not emitted on creation. Read: r.id!. | |
patientId |
subject.reference |
Patient/{id} | ✔ | Writes Patient/${a.patientId}. Read via refId(subject.reference). |
encounterId |
context.reference |
Encounter/{id} | ✔ | CAUTION: 'context' field (not 'encounter' like MedicationRequest). Writes Encounter/${a.encounterId}. Read via refId(context.reference). |
prescriptionId |
request.reference |
MedicationRequest/{id} | ✔ | Link to the prescription line. Writes MedicationRequest/${a.prescriptionId}. Read via refId(request.reference). |
drug.code |
medicationCodeableConcept.coding[0].code |
✔ | Copied from the prescription (administer() takes prescription.drug). Read via codingOf without filter; default '?'. | |
drug.display |
medicationCodeableConcept.coding[0].display (+ .text on write) |
✔ | Written in coding[0].display AND .text. BUT on read, fromMedicationAdministration does NOT fall back to .text: med.display otherwise '?' directly (different from Prescription/HomeMedication). | |
drug.system |
medicationCodeableConcept.coding[0].system |
variable (pass-through): e.g. urn:posos:cis, urn:posos:medicabase | ✔ | Passed through as-is. Read: med.system, otherwise ''. |
at |
effectiveDateTime |
✔ | Instant of administration (ISO, truncated to the minute on the domain side). Read: r.effectiveDateTime, otherwise ''. | |
performerId |
performer[0].actor.reference |
Practitioner/{id} | ✔ | Writes performer:[{actor:{reference:Practitioner/${a.performerId}}}]. Read via refId(performer[0].actor.reference). |
Markers (identifiers · categories · extensions · status/priority)
- resourceType —
MedicationAdministration· Fixed. - status —
completed· Fixed, always 'completed' (no partial/cancelled administration modeled).
Search: forPatient: GET MedicationAdministration?subject=Patient/{patientId}&_count=200, sort 'at' descending APP-SIDE. create → http.create(toMedicationAdministration). No byId/update/remove exposed in the repository.
Notes: No business identifier. The 'request' link to MedicationRequest is structural (an administration always exists for a line). FHIR field 'context' for the visit (a specificity of MedicationAdministration in R4).
PlannedAdministration (domain/medication.ts — planned intake of the administration plan)
FHIR resource: Task (R4) — toPlannedAdministrationTask / fromPlannedAdministrationTask mappers
One PLANNED intake of the administration plan, materialized at prescription time (guided flow) — same Task pattern as the workflow validations, so the platform can observe the plan by polling. Lifecycle: requested (planned) → completed (administered — writes the MedicationAdministration at the same time) or cancelled (line stopped). Materialization is bounded (≤ 60 Tasks per line; no treatment duration → 48 h horizon).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
id |
— | Not emitted on creation. Read: r.id!. | |
patientId |
for.reference |
Patient/{id} | ✔ | Read via refId(for.reference). |
prescriptionId |
focus.reference |
MedicationRequest/{id} | ✔ | The line the intake belongs to — the cancellation key when the line is stopped. |
at |
executionPeriod.start |
✔ | Scheduled instant of the intake (ISO). | |
doseText |
description |
✔ | Intake composition in specialties (« 5,3 mL — AMOXICILLINE ARROW 500 mg / 5 mL… »). | |
status |
status |
requested→planned, completed→done, cancelled→cancelled | ✔ | Any other Task status reads as 'planned'. |
Markers (identifiers · categories · extensions · status/priority)
- resourceType —
Task· Fixed. - intent (FHIR) —
order· Fixed. - code —
SYS.plannedAdministration = urn:ehr-lab:planned-administration ; code=administer· Distinguishes these Tasks from the validation Tasks (urn:ehr-lab:validation).
Search: forPatient: GET Task?patient=Patient/{id}&code=urn:ehr-lab:planned-administration|administer&_count=200, sort 'at' ASCENDING app-side (a MAR reads forward). byId: http.read + code check. complete(id): update status→completed. cancelForPrescription: GET Task?focus=MedicationRequest/{id}&code=…&status=requested, then update each →cancelled.
Notes: Administering a planned intake goes through the same domain guard as a manual administration (administer()), then completes the Task — the pancarte and the platform see the same transition. Tasks are never deleted.
Biology & derived values
lab — Lab result (LabResult / Analyte)
FHIR resource: Observation (R4 standard). toObservation/fromObservation mappers in mappers.ts (l.385-416). No FHIR profile declared; use: measurement of a LOINC analyte with a numeric value + UCUM unit at a given instant.
A biological measurement of an analyte (LOINC code from the mocked value-set MockTerminology.labAnalytes): numeric value, UCUM unit, sampling/result timestamp, performer. Manual entry (testbed for workflows triggered on the arrival of results). DERIVED VALUES (e.g. eGFR) have NO separate mapper: if recorded they would be an identical Observation (same toObservation, category laboratory, one LOINC code). No eGFR analyte exists in the mocked value-set (see markers). Created by ehr.recordLab (application/ehr.ts l.665).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
Observation.id |
— | Read-only (fromObservation: r.id!). Assigned by the Blaze store at creation; never set on write. | |
patientId |
subject.reference |
✔ | Writes Patient/${l.patientId}. Read back via refId(subject.reference). REQUIRED (always written). |
|
encounterId |
encounter.reference |
— | Writes Encounter/${l.encounterId} ONLY if encounterId present (conditional spread). Optional on the domain side (encounterId?: string). recordLab fills it with the active visit if one exists. |
|
analyte.code |
code.coding[0].code |
http://loinc.org (SYS.loinc) | ✔ | LOINC code of the analyte. On write code.coding[0] = { system: SYS.loinc, code: l.analyte.code }. On read: codingOf(r, "code", SYS.loinc) ?? codingOf(r, "code") — the LOINC-system coding is preferred, otherwise fallback to the 1st coding. Default "?" if missing. |
analyte.label |
code.coding[0].display + code.text |
✔ | Written both in the LOINC coding display AND in code.text (redundancy). On read: code?.display ?? code.text ?? "?". | |
analyte.unit |
valueQuantity.unit |
http://unitsofmeasure.org (SYS.ucum) | ✔ | No FHIR field dedicated to the analyte unit: fromObservation reconstructs analyte.unit from valueQuantity.unit. On the domain side LabResult.unit = analyte.unit (recordLabResult enforces unit = analyte.unit). |
value |
valueQuantity.value |
✔ | Numeric value. On read value?.value ?? Number.NaN (NaN if missing). The domain requires Number.isFinite(value) on entry. | |
unit |
valueQuantity.unit (and valueQuantity.code) |
http://unitsofmeasure.org (SYS.ucum) | ✔ | On write valueQuantity = { value, unit: l.unit, system: SYS.ucum, code: l.unit } — the unit serves BOTH as label (unit) AND as UCUM code (code), the same string. On read only valueQuantity.unit is read (value.code ignored). |
at |
effectiveDateTime |
✔ | Instant of sampling/result (ISO). Read as-is, default "". Domain: rejects a date in the future (5-min tolerance) and ISO normalized via toISOString(). | |
performerId |
performer[0].reference |
✔ | Writes performer: [{ reference: Practitioner/${l.performerId} }] (DIRECT reference, no actor wrapper like MedicationAdministration). Read via refId(performer[0].reference). Filled by ctx.practitionerId. |
Markers (identifiers · categories · extensions · status/priority)
- status (FHIR-mandatory) —
status = "final"· Hard-coded constant on write (toObservation), never read back by fromObservation. All created Observations are final. - category (laboratory category — key marker) —
category[0].coding[0] = { system: SYS.obsCategory = "http://terminology.hl7.org/CodeSystem/observation-category", code: "laboratory", display: "Laboratory" }· Hard-coded. Used as the repo's search filter (category=laboratory). Not read back in fromObservation. This is the marker that attaches a derived value (eGFR) to biology if it were recorded. - analyte-code system —
SYS.loinc = "http://loinc.org"· System of the code coding. codingOf prefers this system on read. - quantity system —
SYS.ucum = "http://unitsofmeasure.org"· valueQuantity.system. UCUM code = the unit string itself. - identifier / extension —
NONE· The Observation carries neither a business identifier nor an extension. No biology-specific SYS.* identifier/extension constant. - priority —
N/A· Observation has no priority; not applicable to biology (the priority/urgency field exists only for Communication). - derived values (eGFR) —
NO dedicated code/category in the code· The mocked value-set (terminology/mock.ts ANALYTES) contains: 2160-0 serum creatinine µmol/L, 2823-3 potassium mmol/L, 2951-2 sodium mmol/L, 718-7 hemoglobin g/dL, 6690-2 leukocytes 10*3/µL, 1988-5 CRP mg/L, 4548-4 HbA1c %, 2345-7 blood glucose mmol/L, 1751-7 albumin g/L, 6301-6 INR 1. NO eGFR analyte. The mention 'detect-from-egfr' (ValidationCard.tsx l.20) is a WORKFLOW NAME that proposes a Condition — not a derived Observation. An eGFR, if entered, would reuse toObservation identically.
Search: FhirLabs.forPatient (repositories.ts l.353): GET Observation?subject=Patient/{id}&category=laboratory&_count=200, then APP-SIDE sort by at descending (byDateDesc — Blaze only sorts on _lastUpdated). category=laboratory passed as CODE ALONE (no system). FhirLabs.create: http.create(toObservation(result)). No byId, no additional filter, no soft-delete (unlike Condition/Allergy/Document).
Notes: Source of truth: mappers.ts (toObservation/fromObservation l.385-416) + repositories.ts (FhirLabs l.350-365) + domain/lab.ts (LabResult, Analyte, recordLabResult) + terminology/mock.ts (ANALYTES value-set). Point of attention: valueQuantity.code == valueQuantity.unit (the UCUM unit string doubles as the code, a testbed simplification). No reference range (referenceRange), no interpretation/abnormal flag are mapped — fields absent from the mappers, not invented here.
vitals — Body measurement (VitalMeasure: height / weight)
FHIR resource: Observation (R4 standard) — toVitalObservation / fromVitalObservation mappers
A dated body measurement — height or weight — in the canonical unit (cm / kg). Category vital-signs (vs laboratory for LabResult), fixed LOINC codes: 8302-2 Body height, 29463-7 Body weight. Every new entry is APPENDED (history preserved); the chart banner and the BIM dosage matching use the LATEST measurement of each kind + a derived BMI (summarizeVitals, kg/m², computed — never persisted). Created by ehr.recordVitals (audited), displayed/entered via the chart banner (VitalsActions).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
kind |
code.coding[0] |
http://loinc.org — 8302-2 (height) | 29463-7 (weight) | ✔ | Read: any vital-signs Observation whose LOINC code is not one of these two is IGNORED (fromVitalObservation → undefined, filtered by the repo) — other vital signs may coexist. |
value |
valueQuantity.value |
UCUM cm | kg (unit doubles as code) |
✔ | Canonical unit enforced by the domain (VITAL_UNITS); plausibility bounds 20–260 cm / 0.3–400 kg. |
patientId |
subject.reference |
Patient/{id} | ✔ | |
at |
effectiveDateTime |
✔ | Never in the future (5-min tolerance). | |
recordedBy |
performer[0].reference |
Practitioner/{id} | ✔ |
Markers: status final (hard-coded) · category vital-signs (search filter of FhirVitals) · no identifier/extension.
Search: FhirVitals.forPatient: GET Observation?subject=Patient/{id}&category=vital-signs&_count=200, unknown codes filtered, app-side sort by at desc.
Documents & Scan&Go
document (PatientDocument) — Chart document (PDF: report, letter, scanned prescription…)
FHIR resource: DocumentReference (R4) + Binary (raw bytes). Mappers: toDocumentReference / fromDocumentReference (mappers.ts:560-612). Binary via FhirHttp.createBinary/readBinary (http.ts:86-102).
The document metadata lives in a DocumentReference; the bytes in a separate FHIR Binary, referenced by content.attachment.url = "Binary/
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
DocumentReference.id |
— | Read-only (r.id!); assigned by Blaze at creation. | |
patientId |
subject.reference |
✔ | Writes "Patient/ |
|
title |
description (+ content[0].attachment.title) |
✔ | Written in description AND attachment.title. Read: description ?? attachment.contentType ?? "Document". Required (validateUpload: non-empty title). | |
category |
category[0].coding[0].code |
urn:ehr-lab:document-category (SYS.docCategory) | — | Written ONLY if present. Read via codingOf(r,'category',SYS.docCategory).code (exact system match, no fallback). Mocked value-set: report / letter / prescription / imaging / other (terminology/mock.ts DOCUMENT_CATEGORIES). |
contentType |
content[0].attachment.contentType |
✔ | Also used as the Content-Type header of POST /Binary and the Accept of GET /Binary. Read: attachment.contentType ?? "application/pdf". Domain enforces "application/pdf" (validateUpload). | |
binaryUrl |
content[0].attachment.url |
✔ | Value "Binary/ |
|
size |
content[0].attachment.size |
— | Bytes (bytes.byteLength on upload). Optional; read as-is. | |
createdAt |
date (+ content[0].attachment.creation) |
✔ | Written in date AND attachment.creation (ISO, new Date().toISOString() on upload). Read from r.date ?? "". | |
authorId |
author[0].reference |
✔ | "Practitioner/ |
|
transcribed |
extension[url=urn:ehr-lab:transcribed].valueBoolean |
urn:ehr-lab:transcribed (SYS.transcribed) | — | "transcribed" extension. NOT written by toDocumentReference: added by FhirDocuments.markTranscribed (update, idempotent: ignores if the url already exists). Read: true if an extension has url===SYS.transcribed AND valueBoolean===true, otherwise false. |
Markers (identifiers · categories · extensions · status/priority)
- status (write) —
current (fixed)· Always "current" at creation. Also used as a search filter (status=current) and as a soft-delete mechanism (set to "entered-in-error" by remove). - category system —
urn:ehr-lab:document-category \| {report\|letter\|prescription\|imaging\|other}· SYS.docCategory. Codes = mocked value-set DOCUMENT_CATEGORIES. category block entirely omitted if no category. - transcription extension —
urn:ehr-lab:transcribed \| valueBoolean=true· SYS.transcribed. Marks "prescription already transcribed (Scan & Go)" → neutralizes the "Transcribe" CTA. - Binary (byte storage) —
resourceType Binary, Content-Type = document's contentType· createBinary: POST /Binary, body = raw bytes. readBinary: GET /Binary/{id} with Accept = contentType (404/410 → undefined). The DocumentReference points to it via attachment.url = "Binary/". - soft-delete —
status = entered-in-error· remove() does not physically delete (the DocumentReference is referenced by the creation AuditEvent → Blaze forbids the delete). Updating status → drops out of lists (search status=current). The Binary stays inert.
Search: FhirDocuments.forPatient: DocumentReference?subject=Patient/
Notes: Domain guardrails (validateUpload, document.ts): title required, contentType === application/pdf, size > 0, size ≤ 15 MB. binaryUrl expected in the "Binary/
Scan&Go source (ScanSourceDoc) — source PDF of the scanned prescription, archived to the chart
FHIR resource: Input: DocumentReference read from the PARTNER FHIR STORE (Posos), pointed to by an extension of the scan's RequestGroup (scango/posos.ts pullSource:168-191). Output: archived as a local DocumentReference + Binary via the same toDocumentReference (application/ehr.ts archiveScanDocument:613-635).
ScanSourceDoc {contentType, title, bytes} has no dedicated to/from mapper: it is PULLED from the partner FHIR store (not from the postMessage) then re-injected into the local document store via documents.upload with category "prescription" and marked transcribed. The session join is done on the partner patient identifier.
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
title |
(constant) → DocumentReference.description + attachment.title |
✔ | FIXED value on the pull side: "Ordonnance scannée (Scan & Go)". Serves as the idempotency key on archival (existing.title === source.title). | |
contentType |
warehouse DocumentReference.content[0].attachment.contentType → local attachment.contentType |
✔ | Read from the partner attachment; att.contentType ?? "application/pdf". | |
bytes |
warehouse content[0].attachment.data (base64) OR attachment.url → local Binary |
✔ | Either decoded from attachment.data (base64), or fetched from attachment.url (temporary public URL ~24h, no auth). undefined if bytes empty. Become the local Binary. | |
category (on archival) |
DocumentReference.category[0].coding[0].code |
urn:ehr-lab:document-category (SYS.docCategory) | — | Set to "prescription" (label "Ordonnance scannée") by archiveScanDocument. |
transcribed (on archival) |
extension[url=urn:ehr-lab:transcribed].valueBoolean=true |
urn:ehr-lab:transcribed (SYS.transcribed) | — | markTranscribed(doc.id) called right after the upload: the archived PDF IS the source of the transcription in progress → already transcribed. |
patientId / sessionId |
subject.reference (local) ; warehouse side: Patient.identifier |
id://partner (PARTNER_SYS) | ✔ | sessionId = patientId (correlation key returned to Posos). Warehouse join via patient identifier "id://partner| |
authorId (on archival) |
DocumentReference.author[0].reference |
✔ | "Practitioner/<ctx.practitionerId>". |
Markers (identifiers · categories · extensions · status/priority)
- session ↔ warehouse join —
id://partner \| <sessionId>· PARTNER_SYS. Searches the latest RequestGroup by partner patient identifier. - RequestGroup → source DocumentReference link —
extension url contains "prescriptionDocumentReference" \| valueReference.reference· The scan's RequestGroup references the source DocumentReference; docId cleaned of the "urn:uuid:" and "DocumentReference/" prefixes. - archival idempotency —
category === 'prescription' && title === source.title· archiveScanDocument does not re-create if a prescription document with the same title already exists for the patient (returns {archived:false, reason:'déjà archivé'}). - local result —
DocumentReference status=current, category=prescription, extension transcribed=true + Binary· Once archived, it is a standard PatientDocument (same fields as the document entity above).
Search: scango/posos.ts latestRequestGroup: RequestGroup?subject:Patient.identifier=id://partner|
Notes: pull(sessionId) (proposed lines) and pullSource(sessionId) (source PDF) query the SAME latest RequestGroup. The Bundle→lines mapping (mapScannedPrescription) is on the domain side (scango.ts) and does not belong to mappers.ts. Warehouse access uses an OAuth2/IAP token (warehouseGet).
Messaging (threads) & audit
Thread — discussion root (Thread / root ThreadEntry)
FHIR resource: Communication (root = WITHOUT partOf). Written by toThreadRoot; header re-read by threadRootFields; read as the 1st entry by fromCommunicationEntry. Recognized as root by isThreadRoot (no partOf).
The originating message of a thread (notification / chart note / workflow alert). recipient present = user notification; recipient absent = chart note. The root carries the header (title, patient, urgency, sender, audience) AND its content is the thread's 1st entry.
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id (Thread.id / ThreadEntry.id) |
id |
— | FHIR server id; the Thread's id = the id of the root Communication. | |
title (Thread.title) |
topic.text |
— | Written only if input.title provided. Read by threadRootFields.title. Also serves as content fallback for the 1st entry (fromCommunicationEntry: body || topic || ""). | |
patientId (Thread.patientId) |
subject.reference |
— | Writes Patient/{id} only if patientId provided. Read via refId(subject.reference). |
|
urgency (Thread.urgency / ThreadEntry.urgency) |
priority |
✔ | Writes the raw Urgency value ("routine" | "urgent"). Read by urgencyOf: priority ∈ {urgent,stat,asap} → "urgent", otherwise "routine". The urgency of the assembled thread = MAX(root, entries) (buildThread, outside the mappers). | |
createdBy / senderId (Thread.createdBy, ThreadEntry.authorId) |
sender.reference |
✔ | Writes Practitioner/{senderId}. Read by threadRootFields.createdBy and fromCommunicationEntry.authorId via refId(); || undefined → absent = clinical automaton. |
|
content (root's ThreadEntry.content) |
payload[0].contentString |
✔ | Content of the originating message. Read by payloadOf() which joins all payload[].contentString with "\n". @mentions are rendered INLINE in this text (no dedicated FHIR field); the recipient audience is carried by recipient. | |
audience.practitioners (Thread.audience) |
recipient[].reference |
— | Writes Practitioner/{id} for each professional; recipient block omitted if audience empty. Read by audienceFromRecipients: Practitioner/ refs → practitioners. addAudience merges (dedup) and rewrites recipient. |
|
audience.services (Thread.audience) |
recipient[].reference |
— | Writes Organization/{id} for each service. Read by audienceFromRecipients: Organization/ refs → services. |
|
createdAt / sentAt (Thread.createdAt, ThreadEntry.sentAt) |
sent |
✔ | Writes input.at. Read with fallback: sent ?? meta.lastUpdated ?? "". | |
summary (Thread.summary) |
extension(url=SYS.noteSummary).valueString |
SYS.noteSummary (urn:ehr-lab:note-summary) | — | One-sentence automated summary, written by the synthesis workflow — never typed. Read by noteSummaryOf; patched by withNoteAnnotations (which preserves the other extensions). |
focus (Thread.focus) |
extension(url=SYS.noteFocus).valueCodeableConcept |
SNOMED CT (http://snomed.info/sct) | — | Focus of a focused nursing note (« transmission ciblée »). Derived, never typed: written only by withNoteAnnotations (the synthesis workflow's patch), never by toThreadRoot — there is no focus field in the compose form. focusConcept expands the short snomed system key to the canonical URI; noteFocusOf reads it back (display falls back to CodeableConcept.text, then to the code). |
archivedAt (Thread.archivedAt) |
extension(url=SYS.threadArchivedAt).valueDateTime |
SYS.threadArchivedAt (urn:ehr-lab:thread-archived-at) | — | Archiving is TEAM-WIDE, like acknowledgement — it lives on the root, not in the per-professional Basic. Written by withThreadArchive (which drops both extensions when unarchiving) and read by threadArchiveOf. buildThread derives archived = archivedAt && lastAt <= archivedAt, so later activity makes the thread resurface without an explicit unarchive. |
archivedBy (Thread.archivedBy) |
extension(url=SYS.threadArchivedBy).valueString |
SYS.threadArchivedBy (urn:ehr-lab:thread-archived-by) | — | Practitioner id behind the last archive gesture. Written alongside archivedAt, removed with it. |
Markers (identifiers · categories · extensions · status/priority)
- resourceType —
Communication· Fixed. - status —
completed· Constant on write (toThreadRoot). Not read back. - category[0] (note kind) —
SYS.noteKind (urn:ehr-lab:note-kind) \| observation \| transmission· Nature of the chart entry: medical observation (physician) vs focused nursing note. Read by noteKindOf, which defaults toobservationwhen absent — pre-existing threads and platform-written alerts need no migration. It is deliberately FIRST in the array: the platform'sfhir-communication-receivedbinding mapscategory.0.coding.0.codeonto the trigger'scategoryoutput, so a workflow can route on the note kind with no binding change. - category (root nature) —
SYS.communicationCategory (urn:ehr-lab:communication-category) \| thread-root· coding without display. Marks a thread root. isThreadRoot does NOT rely on it (it tests the absence of partOf), but the category distinguishes root vs ack (threadEvent). Read by system, so its position in the array does not matter. - priority (values) —
routine \| urgent· urgencyOf also accepts urgent/stat/asap → urgent on read (tolerance to standard FHIR data).
Search: threadsFor({practitionerId,serviceIds,patientIds}): 2 to 3 merged Communication searches, all with part-of:missing=true & _revinclude=Communication:part-of & _count=200 — (a) recipient=Practitioner/{id},Organization/{serviceIds...} (list = OR) ; (b) sender=Practitioner/{id} ; (c) if patientIds: subject=Patient/{ids...}. threadsForPatient(patientId): Communication?subject=Patient/{id}&part-of:missing=true&_revinclude=Communication:part-of&_count=200. byId(threadId): read Communication/{threadId} (checks isThreadRoot) then Communication?part-of=Communication/{threadId}&_count=200. Assembly: assembleThreads dedup by id, attaches children via parentThreadId, sort by lastAt desc.
Notes: Root = Communication without partOf (isThreadRoot). buildThread computes app-side: acknowledgedBy/At (1st entry kind=ack), lastAt (max of sentAt), urgency (max), unread/archived (via ThreadState), validationRunId (validationRunIdOf). start() = create(toThreadRoot); annotate() = read + update(withNoteAnnotations) — a patch of the summary/focus extensions only. Blaze only sorts on _lastUpdated → app-side sort.
ThreadEntry — thread entry: reply (message) or acknowledgement (ack)
FHIR resource: Communication (child = WITH partOf → root). Written by toThreadEntry; read by fromCommunicationEntry; attached to the root by parentThreadId.
A follow-up entry in a thread. kind="message" → text reply; kind="ack" → acknowledgement (empty content, marked by category threadEvent). The acknowledgement is GLOBAL (the 1st ack counts for everyone, cf. acknowledgement()).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id (ThreadEntry.id) |
id |
— | FHIR server id. | |
threadId (parent) |
partOf[0].reference |
✔ | Writes Communication/{threadId}. Read by parentThreadId via refId(partOf[0].reference). The presence of partOf = it is NOT a root. |
|
kind (ThreadEntry.kind) |
category[0].coding[0] (ack) | presence of payload (message) |
SYS.threadEvent (urn:ehr-lab:thread-event) | ack | ✔ | If kind="ack": writes category=[{coding:[{system:SYS.threadEvent,code:"ack"}]}] and NO payload. If kind="message": writes payload, NO category. Read: isAck = codingOf(category, SYS.threadEvent)?.code==="ack". |
authorId / senderId (ThreadEntry.authorId) |
sender.reference |
✔ | Writes Practitioner/{senderId}. Read via refId() || undefined. |
|
content (ThreadEntry.content) |
payload[0].contentString |
— | Written only for kind="message". Read by payloadOf; for an ack, content forced to "" (isAck). | |
sentAt (ThreadEntry.sentAt) |
sent |
✔ | Writes input.at; read sent ?? meta.lastUpdated ?? "". | |
urgency (ThreadEntry.urgency) |
priority |
— | toThreadEntry does NOT write priority → read by urgencyOf(undefined) = "routine" by default. (The domain intends an urgent reply to bump the thread up, but the entry write does not set priority.) |
Markers (identifiers · categories · extensions · status/priority)
- resourceType —
Communication· Fixed. - status —
completed· Constant on write. - category ack —
SYS.threadEvent (urn:ehr-lab:thread-event) \| ack· Present only on an acknowledgement; absent on a message. - mentions —
(no FHIR field)· resolveMentions/buildMentionables are on the domain side; @mentions stay in contentString, no separate FHIR mapping for entries.
Search: Retrieved with the root via _revinclude=Communication:part-of (threadsFor / threadsForPatient) or explicitly via Communication?part-of=Communication/{threadId}&_count=200 (byId). addEntry() = create(toThreadEntry). No standalone entry search outside a thread's context.
Notes: fromCommunicationEntry is common to root + entries. buildThread sorts entries by sentAt asc and puts the root first. acknowledgement() returns {by,at} of the 1st entry kind=ack.
Validation root — request for a human decision (suspended workflow)
FHIR resource: Communication (special root, WITHOUT sender). Written by toValidationRoot; runId read by validationRunIdOf; materialized (idempotent) by ensureValidationRoot.
Root of a VALIDATION thread: materializes into the chart a suspended platform run so it can be discussed/decided. No sender (= clinical automaton). The identifier carries the runId (reconciliation key + idempotency). Created only on the 1st interaction (before that, synthetic thread id validation:{runId}).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id (Thread.id) |
id |
— | Server id; returned by ensureValidationRoot. | |
runId (Thread.validationRunId) |
identifier[0].value |
SYS.validationRun (urn:ehr-lab:validation-run) | ✔ | Writes identifier=[{system:SYS.validationRun,value:runId}]. Read by validationRunIdOf: identifier.find(system===SYS.validationRun).value. Reconciliation key with the platform and idempotency key. |
patientId (Thread.patientId) |
subject.reference |
— | Patient/{id} if provided. Read by threadRootFields. |
|
title (Thread.title) |
topic.text AND payload[0].contentString |
✔ | The title is written BOTH in topic.text and in payload[0].contentString (content = title). | |
createdAt (Thread.createdAt) |
sent |
✔ | Writes input.at; read sent ?? meta.lastUpdated. |
Markers (identifiers · categories · extensions · status/priority)
- resourceType —
Communication· Fixed. - status —
completed· Constant. - priority —
routine· Fixed (a validation is always written as routine; urgency can rise via the thread entries). - category (root) —
SYS.communicationCategory (urn:ehr-lab:communication-category) \| thread-root· Same category as any thread root. - sender —
(absent)· No sender → createdBy undefined = clinical automaton.
Search: ensureValidationRoot({runId,...}): Communication?identifier={SYS.validationRun}|{runId}&_count=1 ; if found returns its id, otherwise create(toValidationRoot). Then searched/assembled like an ordinary root (threadsFor/byId).
Notes: Synthetic thread while not materialized: domain id validation:{runId} (VALIDATION_THREAD_PREFIX, isSyntheticValidationId, runIdFromSyntheticId). validation? (actionable) is computed app-side by reconciliation with the platform, outside the mappers.
ThreadState — read state of a thread per professional
FHIR resource: Basic (one resource per (professional, thread) pair; upsert). Written by toThreadState; read by fromThreadState.
Unread = last thread event later than readAt. A single Basic per (professional, thread), author = the professional. Archiving is NOT here: it is a team-wide fact carried by the thread root (see Thread), so a discussion closed by one clinician stops notifying the others.
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
id |
— | Server id; used for the in-place update (upsert preserves extension). | |
practitionerId (author of the state) |
author.reference |
✔ | Writes Practitioner/{practitionerId}. Used as search filter (author=). |
|
threadId |
extension(url=SYS.threadReadThread).valueString |
SYS.threadReadThread (urn:ehr-lab:thread-read-thread) | ✔ | valueString extension carrying the id of the root Communication. Read by fromThreadState. |
readAt (ThreadState.readAt) |
extension(url=SYS.threadReadAt).valueDateTime |
SYS.threadReadAt (urn:ehr-lab:thread-read-at) | — | Extension written only if readAt provided. markRead sets readAt=at. Drives unread. |
Markers (identifiers · categories · extensions · status/priority)
- resourceType —
Basic· Fixed. - code (state type) —
SYS.threadRead (urn:ehr-lab:thread-read) \| state· code.coding=[{system:SYS.threadRead,code:"state"}]. Used as search filter (code=SYS.threadRead|state).
Search: FhirThreadState.rows(pro): Basic?code={SYS.threadRead}|state&author=Practitioner/{practitionerId}&_count=200. stateOf → map threadId→{readAt}. markRead: searches the existing one by threadId (fromThreadState) then update (rewrites extension) or create.
Notes: A single Basic per (professional, thread). Unread is computed app-side by comparing readAt to the last thread event, and is forced false on an archived thread. Basics written before archiving moved to the root may still carry a stale threadArchivedAt extension; it is ignored on read.
AuditEntry — audit trail (WHO / WHERE / WHAT / ON WHOM / WHEN)
FHIR resource: AuditEvent. Written by toAuditEvent; read by fromAuditEvent. Systematic logging by the application layer.
Every traced action: professional (agent), service in context (source.site), action + target resource (entity), patient concerned (entity Patient), timestamp (recorded).
Fields
| Business field | FHIR path | System / value-set / code | Required | Notes |
|---|---|---|---|---|
id |
id |
— | Server id. | |
action (AuditEntry.action) |
action (+ subtype[0].code) |
AUDIT_ACTION create→"C", update→"U", read→"R" ; subtype system SYS.restInteraction (http://hl7.org/fhir/restful-interaction), code = raw action (create/update/read) | ✔ | FHIR action code, single letter (C/U/R). Read by AUDIT_ACTION_BACK[action] (C→create,U→update,R→read) ?? "create". subtype[0]=[{system:SYS.restInteraction,code:a.action}] carries the raw action. |
activity (AuditEntry.activity) |
entity[].name |
✔ | Short human-readable description, carried by the name of the DESCRIBED entity (2nd entity pushed). Read: entities.find(e=>e.name).name ?? "Action". | |
practitionerId (AuditEntry.practitionerId) |
agent[0].who.reference |
✔ | Writes Practitioner/{id} with requestor:true. Read via refId(agent[0].who.reference). |
|
serviceId (AuditEntry.serviceId) |
source.site |
— | Writes the raw serviceId string into source.site (NOT a reference). Read source.site. | |
patientId (AuditEntry.patientId) |
entity[].what.reference (Patient/…) |
— | If present, pushed as the 1st entity: {what:{reference:Patient/{id}}}. Read: entities.find(what.reference startsWith Patient/) then refId(). |
|
target (AuditEntry.target) |
entity[].what.reference (described entity) |
— | Raw reference of the resource concerned (e.g. MedicationRequest/42), written in the what of the 2nd entity (the one carrying name=activity). Read: described.what.reference ?? 1st non-patient entity. |
|
at (AuditEntry.at) |
recorded |
✔ | Timestamp; read recorded ?? "". |
Markers (identifiers · categories · extensions · status/priority)
- resourceType —
AuditEvent· Fixed. - type —
SYS.auditType (http://terminology.hl7.org/CodeSystem/audit-event-type) \| rest — display "RESTful Operation"· Constant on write. - subtype —
SYS.restInteraction (http://hl7.org/fhir/restful-interaction) \| {create\|update\|read}· Code = raw domain action. - outcome —
"0"· Fixed (success). Not read back. - source.observer —
display "ehr-lab"· Constant; identifies the observer (the app). site = serviceId. - agent.requestor —
true· The professional is the requestor of the action. - entity (structure) —
[ {what:Patient/{id}} (if patient), {what:{target?}, name:activity} ]· 1st entity = patient (optional); last entity = described resource (name=activity, what=target if provided).
Search: FhirAuditTrail.query: AuditEvent?_count={limit||100} (+ entity=Patient/{patientId} if patientId) (+ agent=Practitioner/{practitionerId} if practitionerId). Sort byDateDesc("at") app-side then slice(limit||100). record() = create(toAuditEvent).
Notes: The entity mapping carries 2 roles: patient (search by entity=Patient/…) and described target resource. serviceId is NOT a FHIR reference but a free value in source.site.