Comprendre et références · Référence

Correspondance FHIR R4

Generated from the source of truth: apps/ehr-lab/src/infrastructure/fhir/mappers.ts (to<X>/from<X> functions + the SYS object) and repositories.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 / activeactive = true · Fixed boolean set at creation (no status field on Patient).
  • resourceTypePatient

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/. register(draft): prior search identifier=${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 identifierurn: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 / activeactive = 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 identifierurn: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 / activeactive = 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 / activeactive = 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/ if found.

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 statusin-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 / priorityNONE · 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). Always active, 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-deletehttp://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 acknowledgementSYS.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 writes extension: [] 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 ; activeactive + 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.detail for lab results, not Observation.reasonObservation has NO reason element in R4, so the link is carried problem-side, where the specification puts it. evidence means "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 understands evidence.

  • 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.

  • updateProblem re-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 carries clinicalStatus: inactive, and reading that first would map it back to resolved, i.e. to a disease the patient had. Such a Condition is filtered out of every list by buildProblemBoard, and its trace is the supersedes reference on the replacement. scripts/repair-refuted-hypotheses.ts repairs the ones written before this status existed (signature: referenced by another Condition's supersedes and 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=confirmedactive, 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). Always active at creation. Not read back by fromAllergyIntolerance.
  • verificationStatus (always written)http://terminology.hl7.org/CodeSystem/allergyintolerance-verification \| confirmed · Local constant ALLERGY_VERIF (not in SYS). Always confirmed at creation.
  • verificationStatus — soft-deletehttp://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 (ucumhttp://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)

  • resourceTypeMedicationRequest · Fixed.
  • intent (FHIR)order · Fixed, always 'order' (no other intent emitted).
  • statusactive \| 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 extensionSYS.stoppedAt = urn:ehr-lab:stopped-at (valueDateTime) · Conditional (stoppedAt present).
  • dosage-nature extensionSYS.dosageNature = urn:ehr-lab:dosage-nature (valueCoding) · Conditional (dosage picked). BIM nature of the regimen, needed for a lossless round-trip.
  • dosage-source extensionSYS.dosageSource = urn:ehr-lab:dosage-source (valueString) · Conditional (source known).
  • dispense-plan extensionSYS.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)

  • resourceTypeMedicationStatement · Fixed.
  • statusactive (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)

  • resourceTypeMedicationAdministration · Fixed.
  • statuscompleted · 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)

  • resourceTypeTask · Fixed.
  • intent (FHIR)order · Fixed.
  • codeSYS.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 systemSYS.loinc = "http://loinc.org" · System of the code coding. codingOf prefers this system on read.
  • quantity systemSYS.ucum = "http://unitsofmeasure.org" · valueQuantity.system. UCUM code = the unit string itself.
  • identifier / extensionNONE · The Observation carries neither a business identifier nor an extension. No biology-specific SYS.* identifier/extension constant.
  • priorityN/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/". The upload first creates the Binary then the DocumentReference (repositories.ts FhirDocuments.upload:576-598).

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/"; read back via refId(subject.reference). Required on write.
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/" (relative reference). Read: attachment.url ?? "". content() extracts its id via split('/').pop() for readBinary.
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/"; read back via refId(author[0].reference). On Scan&Go archival = ctx.practitionerId.
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 systemurn: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 extensionurn: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-deletestatus = 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/&status=current&_count=200, sort createdAt descending APP-SIDE (byDateDesc). content(id): read DocumentReference/ then readBinary(). remove(id) / markTranscribed(id): read DocumentReference/ then update.

Notes: Domain guardrails (validateUpload, document.ts): title required, contentType === application/pdf, size > 0, size ≤ 15 MB. binaryUrl expected in the "Binary/" format.

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 joinid://partner \| <sessionId> · PARTNER_SYS. Searches the latest RequestGroup by partner patient identifier.
  • RequestGroup → source DocumentReference linkextension url contains "prescriptionDocumentReference" \| valueReference.reference · The scan's RequestGroup references the source DocumentReference; docId cleaned of the "urn:uuid:" and "DocumentReference/" prefixes.
  • archival idempotencycategory === '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 resultDocumentReference 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|&_count=1&_sort=-_lastUpdated (partner warehouse); then GET DocumentReference/ (partner warehouse). Archival: documents.forPatient(patientId) for idempotency, then documents.upload + markTranscribed (local store).

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)

  • resourceTypeCommunication · Fixed.
  • statuscompleted · 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 to observation when absent — pre-existing threads and platform-written alerts need no migration. It is deliberately FIRST in the array: the platform's fhir-communication-received binding maps category.0.coding.0.code onto the trigger's category output, 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)

  • resourceTypeCommunication · Fixed.
  • statuscompleted · Constant on write.
  • category ackSYS.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)

  • resourceTypeCommunication · Fixed.
  • statuscompleted · Constant.
  • priorityroutine · 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)

  • resourceTypeBasic · 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)

  • resourceTypeAuditEvent · Fixed.
  • typeSYS.auditType (http://terminology.hl7.org/CodeSystem/audit-event-type) \| rest — display "RESTful Operation" · Constant on write.
  • subtypeSYS.restInteraction (http://hl7.org/fhir/restful-interaction) \| {create\|update\|read} · Code = raw domain action.
  • outcome"0" · Fixed (success). Not read back.
  • source.observerdisplay "ehr-lab" · Constant; identifies the observer (the app). site = serviceId.
  • agent.requestortrue · 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.