diff --git a/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/.openspec.yaml b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/.openspec.yaml
new file mode 100644
index 000000000..149631464
--- /dev/null
+++ b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-08-17
diff --git a/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/design.md b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/design.md
new file mode 100644
index 000000000..e04daed8c
--- /dev/null
+++ b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/design.md
@@ -0,0 +1,119 @@
+## Context
+
+See `proposal.md` — Why, and the spec delta under `specs/location-composite-conversion/` for the behaviour being
+contracted.
+
+`DatatypeConverter.toLocationFromComposite` holds the IG's component mapping in a local `int[] c` and then never
+reads it — the loop indexes the composite with its own counter. The array and the method's Javadoc agree with the
+IG's `datatype-pl-to-location` ConceptMap, so the mapping is already correct and only the access is wrong.
+Everything else in the method (the `partOf` nesting, `mode`, the `operationalStatus` / `type` assignments) is
+sound.
+
+The reviewed Nevada bundle is the worked example throughout: `RXA-11` is
+`IZGATEWAYART^^^IZGATEWAY-AART^^^IZ GATEWAY AART TEST^^330 C ST SW UNIT 7^^UNKNOWN^VA^20201`, an `LA2`, and it
+converted to `IZGATEWAYART` labelled `bd`/Bed, `IZGATEWAY-AART` labelled `lvl`/Level, no Building at all, and
+`description` = `330 C ST SW UNIT 7`.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Make `Location.physicalType` a fact the sending system asserted, never one the converter inferred.
+- Preserve the IG mapping as the readable source of truth in the code, rather than encoding it
+ implicitly in loop order.
+- Read components 9 and beyond according to the datatype of the field being converted.
+
+**Non-Goals:**
+
+- Re-designing `Location` conversion beyond these two defects. The nesting behaviour, `mode`, address handling
+ and the `operationalStatus` / `type` assignments are unchanged.
+- Producing `ImmunizationEvaluation`, or moving `59781-5`, `30982-3`, `30956-7` or `30973-2` anywhere. See
+ proposal, Out of Scope.
+- Introducing the terminology package's injectable abstractions into this call site. It uses the static
+ `Systems` utility today; this change stays on that path so it does not collide with the in-flight
+ `vocabulary-management-integration` work.
+
+## Decisions
+
+### Index the composite by the mapping array, and let a component opt out of a code
+
+Replace the three parallel locals with class-level constants and index by the mapping array:
+
+```java
+private static final int[] LOC_COMPONENTS = { 2, 1, 0, 7, 6, 3 };
+private static final String[] LOC_CODES = { "bd", "ro", null, "lvl", "bu", "si" };
+private static final String[] LOC_DISPLAYS = { "Bed", "Room", null, "Level", "Building", "Site" };
+```
+
+The component order is Keith's original and is left alone: it runs Bed, Room, Point of Care, Floor,
+Building, Facility, which is most specific to least, so the `partOf` chain it builds nests correctly.
+The `si` display stays "Site", the code system's own display for that code, rather than being changed
+to "Facility" to match the source component — the value set's display is the conventional choice and
+changing it buys nothing.
+
+A `null` code means "produce the named `Location`, emit no `physicalType`". That is how Point of Care
+is handled: the IG leaves that cell unresolved — it names an extension rather than a code — and no
+concept in FHIR R4's `location-physical-type` describes a point of care. `Location.physicalType` is
+`0..1`, so a `Location` without one is conformant.
+
+*Alternatives considered.* Renumbering the code arrays so the existing `i` indexing becomes correct
+would work but discards the IG mapping as documentation — the next reader would have no way to see
+which component each code came from. Defining our own point-of-care extension follows the IG's stated
+intent, but the IG never finished defining it, so we would be inventing a canonical and repeating the
+mistake this same review flagged in our use of the core `originalText` extension. Keeping `wa`/Ward is
+rejected outright: `wa` is a real `location-physical-type` code, but neither the message nor the IG
+assigns it to Point of Care, so emitting it asserts a physical type the sender never sent — the same
+invented-data problem the review reported, merely relabelled.
+
+### Components 9 and beyond move out of the shared helper
+
+`toLocationFromComposite` is shared by `PL`, `LA1` and `LA2`, which agree only through component 8.
+The helper currently sets `description` from component 9 and `identifier` from component 10 for all
+three, which is right only for `PL`:
+
+- `LA1`-9 is an `AD` Address, and `LA2`-9 through -16 are the address components. So today an `LA1` or
+ `LA2` gets its address text stuffed into `description` as well as into `Location.address`, which
+ `AddressParser` already parses from component 9 onward. `LA1` has no component 10 at all.
+- For `PL`, `DatatypeConverter.toLocation`'s own `case "PL"` then overwrites `description` with
+ component 10 — the `EI` identifier — so PL-9 Location Description never reaches FHIR, and the
+ identifier text lands in `description` beside the parsed `identifier`.
+
+So the `description` and `identifier` assignments move out of the helper and into the `PL` case, and the
+redundant `setDescription` line in that case goes away. `LA1` and `LA2` keep only their address, which
+they already get. This is a second defect in the same method, found while contracting its behaviour, and
+it is fixed here because the spec for this capability has to state which component supplies
+`description` either way.
+
+## Risks / Trade-offs
+
+- **`Location` output changes for every existing consumer, not just the pilot.** `PL`, `LA1` and `LA2`
+ are reached by any message with a person-location field. → The proposal marks this BREAKING, and the
+ version it ships under says so. The change is a correction toward the IG,
+ so no consumer has a conformant reason to depend on the old values.
+- **`Normalizer` dedupes Locations on `physicalType` + `name` + `mode`, so the number of `Location`
+ resources in a bundle can change.** No test asserts on `Location` at all and there are no
+ expected-output fixtures, but a test that counts bundle entries would move. → Run the full suite as its
+ own task before writing new tests, so any movement is attributed to the fix rather than to new coverage.
+- **`Location.description` changes for `PL`, `LA1` and `LA2` alike.** `PL` gains PL-9 where it used to
+ carry PL-10's identifier text; `LA1` and `LA2` lose a `description` that duplicated their address. →
+ Part of the same breaking `Location` correction, so it ships under the same version.
+
+## Migration Plan
+
+No data migration and no configuration change. The library is a build-time dependency, so the change
+ships by version.
+
+The work is carried on `2.5.2-SNAPSHOT` for pilot testing and released as `2.6.0`, because the `Location`
+corrections are breaking and the project states it follows SemVer. `izgw-transform` pins
+`2.5.1-SNAPSHOT`, so it has to move to `2.5.2-SNAPSHOT` to pick this up.
+
+`RELEASE_NOTES.md` is not edited by hand in this repository — a GitHub Actions workflow writes it when a
+release is cut — so no task here touches it.
+
+Rollback is a dependency-version revert in the consuming service.
+
+## Open Questions
+
+- Whether the `location-physical-type` gap for a point of care is worth raising with the v2-to-FHIR IG
+ authors, given their own ConceptMap leaves the cell unresolved. Answering it later does not change
+ what we emit now, which is no code.
diff --git a/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/proposal.md b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/proposal.md
new file mode 100644
index 000000000..6dd0a2a5b
--- /dev/null
+++ b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/proposal.md
@@ -0,0 +1,79 @@
+## Why
+
+Independent review of a real Z42 (`RSP^K11`, evaluated history + forecast) response from the Nevada IIS test system,
+for the eHealth Exchange pilot, reported "extra data not returned in the Z42: physicalType: BD Bed, lvl Level".
+That is a real defect, it is not specific to Z42, and it is pre-existing — it affects every message carrying a
+person-location field.
+
+`DatatypeConverter.toLocationFromComposite` transcribes the v2-to-FHIR IG's `datatype-pl-to-location` ConceptMap
+into `int[] c = { 2, 1, 0, 7, 6, 3 }`, and the method's own Javadoc restates the same mapping — but the loop reads
+the composite with the loop counter instead of `c[i]`, so the array is never used. In the reviewed bundle that puts
+`bd`/Bed on `IZGATEWAYART` (LA2-1 Point of Care) and `lvl`/Level on `IZGATEWAY-AART` (LA2-4 Facility), while
+`IZ GATEWAY AART TEST` (LA2-7 Building) is never read at all. Both codes the reviewer saw were invented by the
+converter. PL-5 and PL-6 are also emitted twice — once as a mislabelled nested `Location`, once correctly as
+`operationalStatus` and `type`.
+
+The same method reads components 9 and 10 as Location Description and Comprehensive Location Identifier for all
+three datatypes, which holds only for `PL`. `LA1`-9 is an Address and `LA2`-9 onward are the address components, so
+an `LA1` or `LA2` duplicates its address text into `Location.description` — visible in the reviewed bundle as
+`"description": "330 C ST SW UNIT 7"`. For `PL`, `toLocation`'s own `case "PL"` then overwrites `description` with
+component 10, the identifier, so PL-9 Location Description never reaches FHIR at all.
+
+## What Changes
+
+- **BREAKING** `Location.physicalType` is corrected to the component mapping the IG specifies:
+ `bd` from PL-3, `ro` from PL-2, `lvl` from PL-8, `bu` from PL-7, `si` from PL-4. Existing consumers see
+ different `physicalType` codes and different `Location.name` values on the same input, because both are set
+ from the component now read correctly. PL-5 and PL-6 stop being emitted as nested `Location` resources and are
+ carried only as `operationalStatus` and `type`.
+- PL-1 Point of Care has no standard `physicalType` code — the IG specifies an extension there rather than a
+ code from `location-physical-type`. The current `wa`/Ward is an approximation with no basis in the IG, so no
+ code is emitted for that component.
+- **BREAKING** `Location.description` and `Location.identifier` are read per datatype: from PL-9 and PL-10 for a
+ `PL`, and from neither for an `LA1` or `LA2`, whose components 9 and beyond are the address they already
+ populate. A `PL` now carries PL-9 in `description` instead of PL-10's identifier text, and an `LA1`/`LA2` no
+ longer carries a `description` that duplicated its address.
+
+## Out of Scope
+
+`ImmunizationEvaluation` is **not** produced. The review asked for `59781-5^Dose Validity^LN` and
+`30982-3^Reason for invalid dose^LN` on an `ImmunizationEvaluation`, and an earlier draft of this change planned
+exactly that. Our FHIR lead ruled it out: the resource is maturity level 0 in R4, the HL7 v2-to-FHIR spec does not
+map it, and it needs substantial work in the Public Health Workgroup. It will not be supported by v2-to-FHIR until
+R6 progresses, and IZ Gateway targets R4 only. All `ImmunizationEvaluation` work — the resource, `doseStatus`,
+`doseStatusReason`, `targetDisease` from `30956-7`, and
+`ImmunizationRecommendation.recommendation.supportingImmunization` — is therefore dropped from this change rather
+than deferred to a later one.
+
+Two review items need no converter change:
+
+- `59781-5` and `30982-3` are already carried in the converted bundle as `Observation` resources, with
+ `Observation.code` holding the LOINC code, the value in `Observation.value[x]`, `subject` referencing the
+ `Patient` and `partOf` referencing the group's `Immunization`. They are absent from the reviewed bundle because
+ the Transformation Service's FHIR searchset filter removes resources it has not marked, not because the
+ converter drops them. Making them visible is an `izgw-transform` filter decision.
+- `30973-2^Dose Number in Series^LN` is present in the Nevada sample and already maps to
+ `Immunization.protocolApplied.doseNumber[x]` — the reviewed bundle shows `"doseNumberPositiveInt": 1` on both
+ history groups.
+
+## Capabilities
+
+### New Capabilities
+- `location-composite-conversion`: conversion of the HL7 v2 `PL`, `LA1` and `LA2` composites to `Location`,
+ covering which component supplies `name`, the `physicalType` code per component, the `partOf` nesting of
+ multiple populated components, which components are carried as `operationalStatus` and `type` instead, and
+ which element components 9 and beyond supply for each of the three datatypes. No existing spec covers
+ `Location`.
+
+## Impact
+
+- `converter/DatatypeConverter.toLocationFromComposite` and its `case "PL"` caller — the indexing fix and the
+ per-datatype reading of components 9 and 10. Reached by every `PL`, `LA1` and `LA2` conversion, so the blast
+ radius is every message with a person-location field, not only immunization messages.
+- New `Location` coverage. No test asserts on any part of `Location` today, so coverage is added rather than
+ adjusted.
+- `Normalizer` dedupes Locations on `physicalType` + `name` + `mode`, so the number of `Location` resources in a
+ bundle can change for the same input.
+- Consumers of the emitted bundle see changed `Location` content: different `physicalType` codes, a `Location`
+ per component that the message actually populated, and no `description` on an `LA1`/`LA2`.
+- No new dependency, no configuration change, and no change to any immunization resource.
diff --git a/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/specs/location-composite-conversion/spec.md b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/specs/location-composite-conversion/spec.md
new file mode 100644
index 000000000..c9ce929e1
--- /dev/null
+++ b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/specs/location-composite-conversion/spec.md
@@ -0,0 +1,147 @@
+## Purpose
+
+Defines how the HL7 V2 person-location composites (`PL`, `LA1` and `LA2`) become FHIR R4 `Location`
+resources — which component names each `Location`, which physical-location code each component
+carries, how several populated components nest into a containment chain, and which components are
+not physical locations at all — so that a consumer can trust that a `Location.physicalType` it
+receives was actually asserted by the sending system. It also fixes which components 9 and beyond
+supply, since `PL`, `LA1` and `LA2` agree only through component 8.
+
+## ADDED Requirements
+
+### Requirement: Physical location components carry the code the v2-to-FHIR IG assigns them
+
+The converter SHALL derive `Location.physicalType` from the component that the HL7 v2-to-FHIR
+Implementation Guide's `datatype-pl-to-location` ConceptMap assigns to that code, using the
+`http://terminology.hl7.org/CodeSystem/location-physical-type` code system:
+
+| Component | `physicalType` code |
+| --- | --- |
+| `PL`/`LA1`/`LA2`-2 Room | `ro` |
+| `PL`/`LA1`/`LA2`-3 Bed | `bd` |
+| `PL`/`LA1`/`LA2`-4 Facility | `si` |
+| `PL`/`LA1`/`LA2`-7 Building | `bu` |
+| `PL`/`LA1`/`LA2`-8 Floor | `lvl` |
+
+The converter SHALL NOT emit a `physicalType` code for a component the message left empty, and
+SHALL NOT emit a code the message does not support. A `Location` the converter produces from one of
+these components SHALL take its `name` from that same component.
+
+#### Scenario: Facility and building are labelled from their own components
+
+- **WHEN** an `RXA-11` carries
+ `IZGATEWAYART^^^IZGATEWAY-AART^^^IZ GATEWAY AART TEST^^330 C ST SW UNIT 7^^UNKNOWN^VA^20201`
+- **THEN** a `Location` named `IZ GATEWAY AART TEST` carries `physicalType` `bu` (Building)
+- **AND** a `Location` named `IZGATEWAY-AART` carries `physicalType` `si`
+
+#### Scenario: An empty component produces no Location
+
+- **WHEN** a person-location composite leaves Room, Bed and Floor empty
+- **THEN** the converted bundle contains no `Location` carrying `physicalType` `ro`, `bd` or `lvl`
+ derived from that composite
+
+#### Scenario: Bed and Room are labelled from their own components
+
+- **WHEN** a person-location composite populates component 2 with `RM101` and component 3 with `B2`
+- **THEN** a `Location` named `RM101` carries `physicalType` `ro` and a `Location` named `B2` carries
+ `physicalType` `bd`
+
+### Requirement: Point of Care carries no physical type code
+
+The v2-to-FHIR IG leaves the code for `PL`/`LA1`/`LA2`-1 Point of Care unresolved, naming an
+extension rather than a code, and no concept in FHIR R4's `location-physical-type` code system
+describes a point of care. `Location.physicalType` is `0..1`, so a `Location` without one is
+conformant. The converter SHALL produce a `Location` named from the Point of Care component with no
+`physicalType` element at all, rather than substituting an approximate code such as `wa`/Ward: that
+code is valid in itself, but nothing in the message or the IG assigns it to this component, so
+emitting it would assert a physical type the sending system never sent.
+
+#### Scenario: Point of Care is named but unlabelled
+
+- **WHEN** an `RXA-11` carries `IZGATEWAYART` in component 1
+- **THEN** the converted bundle contains a `Location` named `IZGATEWAYART`
+- **AND** that `Location` has no `physicalType` element, empty or otherwise
+
+#### Scenario: No physical type code is substituted for a point of care
+
+- **WHEN** a person-location composite populates only the Point of Care component
+- **THEN** no `Location` derived from that composite carries `physicalType` `wa`, `bd` or any other
+ code from `location-physical-type`
+
+### Requirement: Multiple populated components nest from most specific to least
+
+When a person-location composite populates more than one physical-location component, the converter
+SHALL produce one `Location` per populated component and SHALL relate them through `Location.partOf`,
+running from the most specific component to the least specific in the order Bed, Room, Point of Care,
+Floor, Building, Facility. Each `Location` the converter produces from these components SHALL have
+`mode` `instance`.
+
+#### Scenario: Point of care nests inside building inside facility
+
+- **WHEN** an `RXA-11` populates Point of Care `IZGATEWAYART`, Facility `IZGATEWAY-AART` and Building
+ `IZ GATEWAY AART TEST`
+- **THEN** the `Location` named `IZGATEWAYART` is `partOf` the `Location` named
+ `IZ GATEWAY AART TEST`, which is `partOf` the `Location` named `IZGATEWAY-AART`
+
+#### Scenario: A single populated component produces no containment chain
+
+- **WHEN** a person-location composite populates only the Facility component
+- **THEN** exactly one `Location` is produced from that composite and it has no `partOf`
+
+### Requirement: Location status and person location type are not physical locations
+
+The `PL`/`LA1`/`LA2`-5 Location Status and `PL`/`LA1`/`LA2`-6 Person Location Type components describe
+a location's state and kind, not a place within a containment hierarchy. The converter SHALL carry
+Location Status on `Location.operationalStatus` and Person Location Type on `Location.type`, each
+exactly once, and SHALL NOT produce a separate `Location` resource or a `physicalType` code from
+either component.
+
+#### Scenario: Location status is not duplicated as a nested Location
+
+- **WHEN** a person-location composite populates the Location Status component
+- **THEN** the value appears on `Location.operationalStatus`
+- **AND** no additional `Location` resource is produced from that component
+
+#### Scenario: Person location type is not duplicated as a nested Location
+
+- **WHEN** a person-location composite populates the Person Location Type component
+- **THEN** the value appears on `Location.type`
+- **AND** no additional `Location` resource is produced from that component
+
+### Requirement: Components 9 and beyond are read according to the composite's own datatype
+
+`PL`, `LA1` and `LA2` agree on components 1 through 8 and diverge from component 9 onward. The
+converter SHALL read those components according to the datatype of the field being converted, and
+SHALL NOT carry an address component as `Location.description` or as `Location.identifier`:
+
+| Datatype | Component | Element |
+| --- | --- | --- |
+| `PL` | PL-9 Location Description | `Location.description` |
+| `PL` | PL-10 Comprehensive Location Identifier | `Location.identifier` |
+| `LA1` | LA1-9 Address (`AD`) | `Location.address` |
+| `LA2` | LA2-9 Street Address through LA2-16 Other Geographic Designation | `Location.address` |
+
+`LA1` and `LA2` have no Location Description component and no Comprehensive Location Identifier
+component, so a `Location` converted from either SHALL have no `description` and no `identifier`
+derived from a component. The `Location` a `PL` converts to SHALL take `description` from PL-9 —
+not from PL-10, whose value is the identifier.
+
+#### Scenario: A PL carries its description and identifier
+
+- **WHEN** a `PL` field carries `EMERGENCY ROOM ENTRANCE` in PL-9 and `4707` in PL-10
+- **THEN** the resulting `Location` has `description` `EMERGENCY ROOM ENTRANCE`
+- **AND** the resulting `Location` has an `identifier` with value `4707`
+
+#### Scenario: An LA2 street address is an address, not a description
+
+- **WHEN** an `RXA-11` carries
+ `IZGATEWAYART^^^IZGATEWAY-AART^^^IZ GATEWAY AART TEST^^330 C ST SW UNIT 7^^UNKNOWN^VA^20201`
+- **THEN** the resulting `Location` has `address` with line `330 C ST SW UNIT 7`, city `UNKNOWN`,
+ state `VA` and postal code `20201`
+- **AND** no `Location` produced from that field has a `description`
+- **AND** no `Location` produced from that field has an `identifier`
+
+#### Scenario: Absent PL description and identifier are omitted
+
+- **WHEN** a `PL` field leaves PL-9 and PL-10 empty
+- **THEN** the resulting `Location` has no `description` and no `identifier`
diff --git a/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/tasks.md b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/tasks.md
new file mode 100644
index 000000000..952822369
--- /dev/null
+++ b/openspec/changes/archive/2026-08-17-fix-pilot-fhir-conformance-defects/tasks.md
@@ -0,0 +1,33 @@
+## 1. Baseline
+
+- [x] 1.1 Run `mvn clean test` on the unmodified branch and record the passing test count, so any later movement in `Location` counts is attributable to the fix rather than to new coverage — baseline: 24517 tests, 0 failures, 3 skipped, BUILD SUCCESS
+- [x] 1.2 Confirm no existing test asserts on `Location.physicalType`, `Location.description` or `Location.identifier`, or counts bundle entries in a way the fix will move; note any that do — no test in `src/test` references `Location` at all
+
+## 2. Location physicalType fix
+
+- [x] 2.1 In `DatatypeConverter`, promote the three parallel locals in `toLocationFromComposite` to class-level constants `LOC_COMPONENTS`, `LOC_CODES`, `LOC_DISPLAYS`, keeping the existing component order (Bed, Room, Point of Care, Floor, Building, Facility) and the existing `si`/"Site" display
+- [x] 2.2 Change the loop to read `ParserUtils.getComponent(comp, LOC_COMPONENTS[i])` instead of `getComponent(comp, i)`
+- [x] 2.3 Set the Point of Care entry's code and display to `null`, and skip `setPhysicalType` when the code is `null`, so Point of Care yields a named `Location` with no `physicalType` element
+- [x] 2.4 Update the method Javadoc so it documents the mapping as implemented and cites the IG's `datatype-pl-to-location` ConceptMap as its source
+- [x] 2.5 Verify PL-5 and PL-6 are no longer emitted as nested `Location` resources and are still carried once each on `operationalStatus` and `type`
+- [x] 2.6 Verify PL-7 Building and PL-8 Floor are now read, using the reviewed Nevada `RXA-11` as the worked example
+- [x] 2.7 Move the `setDescription` and `addIdentifier` calls out of `toLocationFromComposite` into `toLocation`'s `case "PL"`, so only a `PL` reads components 9 and 10
+- [x] 2.8 In `case "PL"`, set `description` from component index 8 (PL-9) and `identifier` from index 9 (PL-10), replacing the existing `setDescription(ParserUtils.toString(comp, 9))` that overwrote the description with the identifier
+- [x] 2.9 Verify an `LA1` and an `LA2` now produce no `description` and no `identifier` from components 9 and 10, and still produce the same `Location.address` as before
+
+## 3. Location tests
+
+- [x] 3.1 Add a test converting a person-location composite that populates Point of Care, Facility and Building, asserting the codes, the names, and the `partOf` chain order
+- [x] 3.2 Add a test asserting a Point of Care `Location` has no `physicalType` element, empty or otherwise, and carries no code from `location-physical-type`
+- [x] 3.3 Add a test asserting empty Room, Bed and Floor components produce no `Location` carrying `ro`, `bd` or `lvl`
+- [x] 3.4 Add a test asserting Location Status and Person Location Type reach `operationalStatus` and `type` exactly once and produce no additional `Location`
+- [x] 3.5 Add a test asserting a `PL` carries PL-9 in `description` and PL-10 in `identifier`, and that both are absent when those components are empty
+- [x] 3.6 Add a test asserting an `LA2` `RXA-11` puts its street address, city, state and postal code on `Location.address` and produces no `description` and no `identifier`
+- [x] 3.7 Add a test asserting an `LA1` produces its `Location.address` from LA1-9 and no `description`
+
+## 4. Verification and version
+
+- [x] 4.1 Run `mvn clean install` and compare the test count and results against the 1.1 baseline, accounting for every difference — 24529 tests, 0 failures, 3 skipped: baseline 24517 plus the 12 new Location tests
+- [x] 4.2 Convert the reviewed Nevada capture and confirm each `RXA-11` yields `IZGATEWAYART` with no `physicalType`, `IZ GATEWAY AART TEST` with `bu`, and `IZGATEWAY-AART` with `si`, that no `bd` or `lvl` remains, and that no `Location` carries `330 C ST SW UNIT 7` as a `description` — confirmed, and the address stays on the Point of Care Location
+- [x] 4.3 Settle the version the breaking `Location` corrections ship under — pom moved to `2.5.2-SNAPSHOT` for pilot testing, to be released as `2.6.0`. `RELEASE_NOTES.md` is written by the release workflow, not by hand, so it is not touched here
+- [x] 4.4 Move `izgw-transform`'s `v2tofhir` dependency from `2.5.1-SNAPSHOT` to `2.5.2-SNAPSHOT` so the service picks this up (separate repo) — `izgw-transform/pom.xml:111` now pins `2.5.2-SNAPSHOT`, verified end to end by converting the Nevada Z42 through the service
diff --git a/openspec/specs/location-composite-conversion/spec.md b/openspec/specs/location-composite-conversion/spec.md
new file mode 100644
index 000000000..487dfd29c
--- /dev/null
+++ b/openspec/specs/location-composite-conversion/spec.md
@@ -0,0 +1,147 @@
+## Purpose
+
+Defines how the HL7 V2 person-location composites (`PL`, `LA1` and `LA2`) become FHIR R4 `Location`
+resources — which component names each `Location`, which physical-location code each component
+carries, how several populated components nest into a containment chain, and which components are
+not physical locations at all — so that a consumer can trust that a `Location.physicalType` it
+receives was actually asserted by the sending system. It also fixes which components 9 and beyond
+supply, since `PL`, `LA1` and `LA2` agree only through component 8.
+
+## Requirements
+
+### Requirement: Physical location components carry the code the v2-to-FHIR IG assigns them
+
+The converter SHALL derive `Location.physicalType` from the component that the HL7 v2-to-FHIR
+Implementation Guide's `datatype-pl-to-location` ConceptMap assigns to that code, using the
+`http://terminology.hl7.org/CodeSystem/location-physical-type` code system:
+
+| Component | `physicalType` code |
+| --- | --- |
+| `PL`/`LA1`/`LA2`-2 Room | `ro` |
+| `PL`/`LA1`/`LA2`-3 Bed | `bd` |
+| `PL`/`LA1`/`LA2`-4 Facility | `si` |
+| `PL`/`LA1`/`LA2`-7 Building | `bu` |
+| `PL`/`LA1`/`LA2`-8 Floor | `lvl` |
+
+The converter SHALL NOT emit a `physicalType` code for a component the message left empty, and
+SHALL NOT emit a code the message does not support. A `Location` the converter produces from one of
+these components SHALL take its `name` from that same component.
+
+#### Scenario: Facility and building are labelled from their own components
+
+- **WHEN** an `RXA-11` carries
+ `IZGATEWAYART^^^IZGATEWAY-AART^^^IZ GATEWAY AART TEST^^330 C ST SW UNIT 7^^UNKNOWN^VA^20201`
+- **THEN** a `Location` named `IZ GATEWAY AART TEST` carries `physicalType` `bu` (Building)
+- **AND** a `Location` named `IZGATEWAY-AART` carries `physicalType` `si`
+
+#### Scenario: An empty component produces no Location
+
+- **WHEN** a person-location composite leaves Room, Bed and Floor empty
+- **THEN** the converted bundle contains no `Location` carrying `physicalType` `ro`, `bd` or `lvl`
+ derived from that composite
+
+#### Scenario: Bed and Room are labelled from their own components
+
+- **WHEN** a person-location composite populates component 2 with `RM101` and component 3 with `B2`
+- **THEN** a `Location` named `RM101` carries `physicalType` `ro` and a `Location` named `B2` carries
+ `physicalType` `bd`
+
+### Requirement: Point of Care carries no physical type code
+
+The v2-to-FHIR IG leaves the code for `PL`/`LA1`/`LA2`-1 Point of Care unresolved, naming an
+extension rather than a code, and no concept in FHIR R4's `location-physical-type` code system
+describes a point of care. `Location.physicalType` is `0..1`, so a `Location` without one is
+conformant. The converter SHALL produce a `Location` named from the Point of Care component with no
+`physicalType` element at all, rather than substituting an approximate code such as `wa`/Ward: that
+code is valid in itself, but nothing in the message or the IG assigns it to this component, so
+emitting it would assert a physical type the sending system never sent.
+
+#### Scenario: Point of Care is named but unlabelled
+
+- **WHEN** an `RXA-11` carries `IZGATEWAYART` in component 1
+- **THEN** the converted bundle contains a `Location` named `IZGATEWAYART`
+- **AND** that `Location` has no `physicalType` element, empty or otherwise
+
+#### Scenario: No physical type code is substituted for a point of care
+
+- **WHEN** a person-location composite populates only the Point of Care component
+- **THEN** no `Location` derived from that composite carries `physicalType` `wa`, `bd` or any other
+ code from `location-physical-type`
+
+### Requirement: Multiple populated components nest from most specific to least
+
+When a person-location composite populates more than one physical-location component, the converter
+SHALL produce one `Location` per populated component and SHALL relate them through `Location.partOf`,
+running from the most specific component to the least specific in the order Bed, Room, Point of Care,
+Floor, Building, Facility. Each `Location` the converter produces from these components SHALL have
+`mode` `instance`.
+
+#### Scenario: Point of care nests inside building inside facility
+
+- **WHEN** an `RXA-11` populates Point of Care `IZGATEWAYART`, Facility `IZGATEWAY-AART` and Building
+ `IZ GATEWAY AART TEST`
+- **THEN** the `Location` named `IZGATEWAYART` is `partOf` the `Location` named
+ `IZ GATEWAY AART TEST`, which is `partOf` the `Location` named `IZGATEWAY-AART`
+
+#### Scenario: A single populated component produces no containment chain
+
+- **WHEN** a person-location composite populates only the Facility component
+- **THEN** exactly one `Location` is produced from that composite and it has no `partOf`
+
+### Requirement: Location status and person location type are not physical locations
+
+The `PL`/`LA1`/`LA2`-5 Location Status and `PL`/`LA1`/`LA2`-6 Person Location Type components describe
+a location's state and kind, not a place within a containment hierarchy. The converter SHALL carry
+Location Status on `Location.operationalStatus` and Person Location Type on `Location.type`, each
+exactly once, and SHALL NOT produce a separate `Location` resource or a `physicalType` code from
+either component.
+
+#### Scenario: Location status is not duplicated as a nested Location
+
+- **WHEN** a person-location composite populates the Location Status component
+- **THEN** the value appears on `Location.operationalStatus`
+- **AND** no additional `Location` resource is produced from that component
+
+#### Scenario: Person location type is not duplicated as a nested Location
+
+- **WHEN** a person-location composite populates the Person Location Type component
+- **THEN** the value appears on `Location.type`
+- **AND** no additional `Location` resource is produced from that component
+
+### Requirement: Components 9 and beyond are read according to the composite's own datatype
+
+`PL`, `LA1` and `LA2` agree on components 1 through 8 and diverge from component 9 onward. The
+converter SHALL read those components according to the datatype of the field being converted, and
+SHALL NOT carry an address component as `Location.description` or as `Location.identifier`:
+
+| Datatype | Component | Element |
+| --- | --- | --- |
+| `PL` | PL-9 Location Description | `Location.description` |
+| `PL` | PL-10 Comprehensive Location Identifier | `Location.identifier` |
+| `LA1` | LA1-9 Address (`AD`) | `Location.address` |
+| `LA2` | LA2-9 Street Address through LA2-16 Other Geographic Designation | `Location.address` |
+
+`LA1` and `LA2` have no Location Description component and no Comprehensive Location Identifier
+component, so a `Location` converted from either SHALL have no `description` and no `identifier`
+derived from a component. The `Location` a `PL` converts to SHALL take `description` from PL-9 —
+not from PL-10, whose value is the identifier.
+
+#### Scenario: A PL carries its description and identifier
+
+- **WHEN** a `PL` field carries `EMERGENCY ROOM ENTRANCE` in PL-9 and `4707` in PL-10
+- **THEN** the resulting `Location` has `description` `EMERGENCY ROOM ENTRANCE`
+- **AND** the resulting `Location` has an `identifier` with value `4707`
+
+#### Scenario: An LA2 street address is an address, not a description
+
+- **WHEN** an `RXA-11` carries
+ `IZGATEWAYART^^^IZGATEWAY-AART^^^IZ GATEWAY AART TEST^^330 C ST SW UNIT 7^^UNKNOWN^VA^20201`
+- **THEN** the resulting `Location` has `address` with line `330 C ST SW UNIT 7`, city `UNKNOWN`,
+ state `VA` and postal code `20201`
+- **AND** no `Location` produced from that field has a `description`
+- **AND** no `Location` produced from that field has an `identifier`
+
+#### Scenario: Absent PL description and identifier are omitted
+
+- **WHEN** a `PL` field leaves PL-9 and PL-10 empty
+- **THEN** the resulting `Location` has no `description` and no `identifier`
diff --git a/pom.xml b/pom.xml
index db69e79b6..bd70017c1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
1.13.0
v2tofhir
- 2.5.1-SNAPSHOT
+ 2.5.2-SNAPSHOT
HL7 Version 2 to FHIR Conversion
This is the v2tofhir Converter core code for the IZ Gateway Transformation Service
http://github.com/IZGateway/v2tofhir
diff --git a/src/main/java/gov/cdc/izgw/v2tofhir/converter/DatatypeConverter.java b/src/main/java/gov/cdc/izgw/v2tofhir/converter/DatatypeConverter.java
index 32a42cc39..3e5b9ef23 100644
--- a/src/main/java/gov/cdc/izgw/v2tofhir/converter/DatatypeConverter.java
+++ b/src/main/java/gov/cdc/izgw/v2tofhir/converter/DatatypeConverter.java
@@ -1175,7 +1175,10 @@ public static Location toLocation(Type t) {
case "PL":
location.setMode(LocationMode.INSTANCE);
toLocationFromComposite(location, comp);
- location.setDescription(ParserUtils.toString(comp, 9));
+ // Only a PL has these. LA1-9 is an Address and LA2-9 through LA2-16 are the address
+ // components, both of which become Location.address below.
+ location.setDescription(ParserUtils.toString(comp, 8)); // PL-9 Location Description
+ location.addIdentifier(toIdentifier(ParserUtils.getComponent(comp, 9))); // PL-10
break;
case "LA1":
location.setMode(LocationMode.INSTANCE);
@@ -1228,29 +1231,49 @@ public static T markDeleted(T r
}
/**
- * Convert the bulk of a PL, LA1 or LA2 to a location. These all look almost the
- * same. PL/LA1/LA2.3 - Bed HD/IS 0304 PL/LA1/LA2.2 - Room HD/IS 0303
- * PL/LA1/LA2.1 - Point Of Care HD/IS 0302 PL/LA1/LA2.8 - Floor HD/IS 0308
- * PL/LA1/LA2.7 - Building HD/IS 0307 PL/LA1/LA2.4 - Facility HD/IS
- *
- * PL/LA1/LA2.5 - Location Status IS O - 0306 PL/LA1/LA2.6 - Person Location
- * Type IS O - 0305
- *
+ * The PL/LA1/LA2 components that name a physical location, most specific first, so that the
+ * Location resources built from them nest correctly through Location.partOf.
+ * @see #LOC_CODES
+ */
+ private static final int[] LOC_COMPONENTS = { 2, 1, 0, 7, 6, 3 };
+ /**
+ * The location-physical-type code for each component in {@link #LOC_COMPONENTS}, transcribed from
+ * the HL7 V2-to-FHIR IG ConceptMap datatype-pl-to-location. A null code means the component names
+ * a Location that carries no physicalType: FHIR R4's location-physical-type code system has no
+ * concept for a point of care, and the IG leaves that cell unresolved.
+ */
+ private static final String[] LOC_CODES = { "bd", "ro", null, "lvl", "bu", "si" };
+ /** The display for each code in {@link #LOC_CODES}, as the code system gives it */
+ private static final String[] LOC_DISPLAYS = { "Bed", "Room", null, "Level", "Building", "Site" };
+
+ /**
+ * Convert the bulk of a PL, LA1 or LA2 to a location. These all look almost the same through
+ * component 8, and the HL7 V2-to-FHIR IG ConceptMap datatype-pl-to-location gives the
+ * location-physical-type code for each one:
+ *
+ * PL/LA1/LA2-3 Bed = bd, PL/LA1/LA2-2 Room = ro, PL/LA1/LA2-1 Point of Care = no code,
+ * PL/LA1/LA2-8 Floor = lvl, PL/LA1/LA2-7 Building = bu, PL/LA1/LA2-4 Facility = si.
+ *
+ * Each populated component names its own Location, and they are chained through
+ * Location.partOf from the most specific component to the least.
+ *
+ * PL/LA1/LA2-5 Location Status (table 0306) and PL/LA1/LA2-6 Person Location Type (table 0305)
+ * describe the location rather than naming a place, so they become operationalStatus and type on
+ * the Location itself and never a Location of their own.
+ *
+ * Components 9 and up differ between the three datatypes, so the caller reads them.
+ *
* @param location The location
* @param comp The composite to convert
*/
private static void toLocationFromComposite(Location location, Composite comp) {
- String[] d = { "Bed", "Room", "Ward", "Level", "Building", "Site" };
- String[] n = { "bd", "ro", "wa", "lvl", "bu", "si" };
- int[] c = { 2, 1, 0, 7, 6, 3 };
Location curl = location;
- for (int i = 0; i < c.length; i++) {
- Type t1 = ParserUtils.getComponent(comp, i);
+ for (int i = 0; i < LOC_COMPONENTS.length; i++) {
+ Type t1 = ParserUtils.getComponent(comp, LOC_COMPONENTS[i]);
if (ParserUtils.isEmpty(t1)) {
continue;
}
- CodeableConcept cc = new CodeableConcept().addCoding(new Coding(Systems.LOCATION_TYPE, n[i], d[i]));
if (curl.hasName()) {
Location partOf = new Location();
@@ -1259,14 +1282,15 @@ private static void toLocationFromComposite(Location location, Composite comp) {
curl = partOf;
}
curl.setMode(LocationMode.INSTANCE);
- curl.setPhysicalType(cc);
+ if (LOC_CODES[i] != null) {
+ curl.setPhysicalType(
+ new CodeableConcept().addCoding(new Coding(Systems.LOCATION_TYPE, LOC_CODES[i], LOC_DISPLAYS[i])));
+ }
curl.setName(ParserUtils.toString(t1));
ParserUtils.toReference(curl, null, "partof"); // Update reference
}
location.setOperationalStatus(toCoding(ParserUtils.getComponent(comp, 4), "0306"));
location.addType(toCodeableConcept(ParserUtils.getComponent(comp, 5), "0305"));
- location.setDescription(ParserUtils.toString(comp, 8));
- location.addIdentifier(toIdentifier(ParserUtils.getComponent(comp, 9)));
}
/**
diff --git a/src/test/java/test/gov/cdc/izgateway/v2tofhir/LocationCompositeTests.java b/src/test/java/test/gov/cdc/izgateway/v2tofhir/LocationCompositeTests.java
new file mode 100644
index 000000000..c4708102e
--- /dev/null
+++ b/src/test/java/test/gov/cdc/izgateway/v2tofhir/LocationCompositeTests.java
@@ -0,0 +1,235 @@
+package test.gov.cdc.izgateway.v2tofhir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.hl7.fhir.r4.model.Location;
+import org.hl7.fhir.r4.model.Location.LocationMode;
+import org.junit.jupiter.api.Test;
+
+import ca.uhn.hl7v2.model.Segment;
+import ca.uhn.hl7v2.model.Type;
+import gov.cdc.izgw.v2tofhir.converter.DatatypeConverter;
+
+/**
+ * Tests for the conversion of the HL7 V2 person-location composites (PL, LA1 and LA2) to
+ * Location, covering which component names each Location, which location-physical-type code it
+ * carries, how several populated components nest through Location.partOf, and which element
+ * components 9 and beyond supply for each of the three datatypes.
+ *
+ * The physical-type codes come from the HL7 V2-to-FHIR IG ConceptMap datatype-pl-to-location:
+ * PL-3 Bed = bd, PL-2 Room = ro, PL-1 Point of Care = no code, PL-8 Floor = lvl,
+ * PL-7 Building = bu, PL-4 Facility = si.
+ *
+ * @author Audacious Inquiry
+ */
+class LocationCompositeTests extends TestBase {
+ private static final String PHYSICAL_TYPE = "http://terminology.hl7.org/CodeSystem/location-physical-type";
+ /** The RXA-11 from the eHealth Exchange pilot response the conformance review was written against */
+ private static final String REVIEWED_RXA_11 =
+ "IZGATEWAYART^^^IZGATEWAY-AART^^^IZ GATEWAY AART TEST^^330 C ST SW UNIT 7^^UNKNOWN^VA^20201";
+
+ /**
+ * The reviewed RXA-11: Point of Care, Facility and Building are populated, so each names its own
+ * Location and they nest from the most specific component to the least. The review reported
+ * bd/Bed on the Point of Care and lvl/Level on the Facility, neither of which the message sent.
+ */
+ @Test
+ void testReviewedRxa11IsLabelledFromItsOwnComponents() {
+ List chain = chainOf(toLa2(REVIEWED_RXA_11));
+ assertEquals(3, chain.size(), "One Location per populated component");
+
+ assertEquals(Arrays.asList("IZGATEWAYART", "IZ GATEWAY AART TEST", "IZGATEWAY-AART"), namesOf(chain),
+ "Point of Care, then Building, then Facility");
+ assertEquals(Arrays.asList(null, "bu", "si"), codesOf(chain));
+ assertFalse(codesOf(chain).contains("bd"), "Nothing sent a Bed");
+ assertFalse(codesOf(chain).contains("lvl"), "Nothing sent a Floor");
+ for (Location location : chain) {
+ assertEquals(LocationMode.INSTANCE, location.getMode());
+ }
+ }
+
+ /** The same three components in a PL, which is where most person locations arrive. */
+ @Test
+ void testPointOfCareFacilityAndBuilding() {
+ List chain = chainOf(toPl("IZGATEWAYART^^^IZGATEWAY-AART^^^IZ GATEWAY AART TEST"));
+ assertEquals(Arrays.asList("IZGATEWAYART", "IZ GATEWAY AART TEST", "IZGATEWAY-AART"), namesOf(chain));
+ assertEquals(Arrays.asList(null, "bu", "si"), codesOf(chain));
+ }
+
+ /** Point of Care has no code in location-physical-type, and none is invented for it. */
+ @Test
+ void testPointOfCareCarriesNoPhysicalType() {
+ Location location = toLocation(toPl("IZGATEWAYART"));
+ assertEquals("IZGATEWAYART", location.getName());
+ assertFalse(location.hasPhysicalType(), "No physicalType element, empty or otherwise");
+ assertFalse(location.hasPartOf(), "A single populated component builds no containment chain");
+ }
+
+ /** Bed, Room and Floor carry their own codes, most specific first. */
+ @Test
+ void testBedRoomAndFloor() {
+ List chain = chainOf(toPl("^RM101^B2^^^^^FL3"));
+ assertEquals(Arrays.asList("B2", "RM101", "FL3"), namesOf(chain));
+ assertEquals(Arrays.asList("bd", "ro", "lvl"), codesOf(chain));
+ }
+
+ /** An empty component names nothing, so it produces no Location and no code. */
+ @Test
+ void testEmptyComponentsProduceNoLocation() {
+ List chain = chainOf(toPl("IZGATEWAYART^^^IZGATEWAY-AART"));
+ assertEquals(2, chain.size());
+ List codes = codesOf(chain);
+ assertFalse(codes.contains("ro"), "No Room was sent");
+ assertFalse(codes.contains("bd"), "No Bed was sent");
+ assertFalse(codes.contains("lvl"), "No Floor was sent");
+ }
+
+ /**
+ * PL-5 Location Status and PL-6 Person Location Type describe the location rather than naming a
+ * place, so each is carried once on the Location and neither produces one of its own.
+ */
+ @Test
+ void testLocationStatusAndPersonLocationTypeAreNotLocations() {
+ Location location = toLocation(toPl("IZGATEWAYART^^^^A^E"));
+ assertEquals("A", location.getOperationalStatus().getCode(), "From PL-5");
+ assertEquals(1, location.getType().size(), "PL-6 once");
+ assertEquals("E", location.getTypeFirstRep().getCodingFirstRep().getCode(), "From PL-6");
+
+ List chain = chainOf(location);
+ assertEquals(1, chain.size(), "Only Point of Care names a Location");
+ assertFalse(codesOf(chain).contains("A"));
+ assertFalse(codesOf(chain).contains("E"));
+ }
+
+ /** Only a PL has a Location Description and a Comprehensive Location Identifier. */
+ @Test
+ void testPlDescriptionAndIdentifier() {
+ Location location = toLocation(toPl("IZGATEWAYART^^^^^^^^EMERGENCY ROOM ENTRANCE^4707"));
+ assertEquals("EMERGENCY ROOM ENTRANCE", location.getDescription(), "From PL-9, not PL-10");
+ assertEquals("4707", location.getIdentifierFirstRep().getValue(), "From PL-10");
+ }
+
+ /** Absent PL-9 and PL-10 produce no description and no identifier. */
+ @Test
+ void testPlWithoutDescriptionOrIdentifier() {
+ Location location = toLocation(toPl("IZGATEWAYART^^^IZGATEWAY-AART"));
+ assertFalse(location.hasDescription());
+ assertFalse(location.hasIdentifier());
+ }
+
+ /**
+ * LA2-9 through LA2-16 are the address, which is where they go. An LA2 has no Location
+ * Description component and no Comprehensive Location Identifier component, so it produces
+ * neither -- the review saw the street address of the reviewed RXA-11 as a description.
+ */
+ @Test
+ void testLa2CarriesAnAddressAndNoDescription() {
+ Location location = toLocation(toLa2(REVIEWED_RXA_11));
+ assertEquals("330 C ST SW UNIT 7", location.getAddress().getLine().get(0).getValue());
+ assertEquals("UNKNOWN", location.getAddress().getCity());
+ assertEquals("VA", location.getAddress().getState());
+ assertEquals("20201", location.getAddress().getPostalCode());
+ for (Location inChain : chainOf(location)) {
+ assertFalse(inChain.hasDescription(), "LA2-9 is a street address, not a description");
+ assertFalse(inChain.hasIdentifier(), "LA2-10 is another designation, not an identifier");
+ }
+ }
+
+ /** LA1-9 is an Address, so it becomes Location.address and never a description. */
+ @Test
+ void testLa1CarriesAnAddressAndNoDescription() {
+ Location location = toLocation(toLa1(
+ "IZGATEWAYART^^^IZGATEWAY-AART^^^^^123 MAIN ST&APT 2&SPRINGFIELD&IL&62701"));
+ assertEquals("SPRINGFIELD", location.getAddress().getCity());
+ for (Location inChain : chainOf(location)) {
+ assertFalse(inChain.hasDescription(), "LA1-9 is an address, not a description");
+ assertFalse(inChain.hasIdentifier(), "LA1 has no identifier component");
+ }
+ }
+
+ /** The physical type codes come from the standard code system. */
+ @Test
+ void testPhysicalTypeUsesTheStandardCodeSystem() {
+ for (Location location : chainOf(toPl("^RM101^B2^FAC^^^BLDG^FL3"))) {
+ if (location.hasPhysicalType()) {
+ assertEquals(PHYSICAL_TYPE, location.getPhysicalType().getCodingFirstRep().getSystem());
+ }
+ }
+ }
+
+ /** Every named Location in a chain is an instance, not a kind */
+ @Test
+ void testEveryLocationInAChainIsAnInstance() {
+ for (Location location : chainOf(toPl("POC^RM101^B2^FAC^^^BLDG^FL3"))) {
+ assertTrue(LocationMode.INSTANCE.equals(location.getMode()), "mode is instance");
+ }
+ }
+
+ private static Location toLocation(Type type) {
+ Location location = DatatypeConverter.toLocation(type);
+ assertNotNull(location, "The composite converts to a Location");
+ return location;
+ }
+
+ /** PL is PV1-3 Assigned Patient Location */
+ private static Type toPl(String pl) {
+ return fieldOf("PV1|1|I|" + pl, 3);
+ }
+
+ /** LA2 is RXA-11 Administered-at Location */
+ private static Type toLa2(String la2) {
+ return fieldOf("RXA|0|1|20220810|20220810|09^Td (adult), adsorbed^CVX|0.5|mL^MilliLiter^UCUM|||"
+ + "01^Historical Information - Source Unspecified^NIP001|" + la2, 11);
+ }
+
+ /** LA1 is RXE-8 Deliver-To Location */
+ private static Type toLa1(String la1) {
+ return fieldOf("RXE||||||||" + la1, 8);
+ }
+
+ private static Type fieldOf(String segment, int field) {
+ try {
+ Segment seg = parseSegment(segment);
+ Type type = seg.getField(field, 0);
+ assertNotNull(type);
+ return type;
+ } catch (Exception e) {
+ throw new AssertionError("Cannot parse " + segment, e);
+ }
+ }
+
+ /** The Location and everything it is partOf, in order */
+ private static List chainOf(Type type) {
+ return chainOf(toLocation(type));
+ }
+
+ private static List chainOf(Location location) {
+ List chain = new ArrayList<>();
+ Location current = location;
+ while (current != null) {
+ chain.add(current);
+ current = current.hasPartOf() ? (Location) current.getPartOf().getResource() : null;
+ }
+ return chain;
+ }
+
+ private static List namesOf(List chain) {
+ return chain.stream().map(Location::getName).toList();
+ }
+
+ /** The physicalType code of each Location in the chain, null where it carries none */
+ private static List codesOf(List chain) {
+ List codes = new ArrayList<>();
+ for (Location location : chain) {
+ codes.add(location.hasPhysicalType() ? location.getPhysicalType().getCodingFirstRep().getCode() : null);
+ }
+ return codes;
+ }
+}