fhir-data-utils-ts 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.2 - 2026-09-30
6
+
7
+ - Add strict FHIR `instant` to Unix epoch-second derivation while preserving
8
+ the boundary between clinical time, `Encounter.id`, provider business
9
+ identifiers and human display codes.
10
+
11
+ ## 0.6.1 - 2026-09-30
12
+
13
+ - Add canonical flat claims, a claims-first builder and native R5 projection
14
+ for `EncounterHistory`, including explicit Encounter, HealthcareService,
15
+ subject, status, class, period and Location boundaries.
16
+ - Add the R5 Encounter lifecycle and projection without weakening the existing
17
+ R4 adapter; `on-hold`, `discharged` and `completed` are validated only at the
18
+ R5 boundary.
19
+
5
20
  ## 0.6.0 - 2026-09-29
6
21
 
7
22
  - Add canonical two-level Communication Bundle validation tools recovered from
package/README.md CHANGED
@@ -92,6 +92,12 @@ metadata and protected endpoint references only. DICOM instances and pixel
92
92
  payloads remain outside this neutral package and are never embedded as flat
93
93
  claims.
94
94
 
95
+ FHIR instants, resource ids and provider business identifiers remain separate.
96
+ Use `fhirInstantToEpochSeconds(...)` only when a downstream convention needs a
97
+ derived whole-second value; retain the original timezone-bearing instant and
98
+ an independent `Encounter.id`. See
99
+ [`docs/ENCOUNTER_IDENTIFIERS_AND_TIME.md`](docs/ENCOUNTER_IDENTIFIERS_AND_TIME.md).
100
+
95
101
  No consumer should copy claim paths or rebuild these structures with literals.
96
102
  Use `fhir-data-utils-ts/claims`, `/converters`, `/fhir-bundle`,
97
103
  `/fhir-search-parameters`, `/clinical-terminology`, `/group-r5` or
@@ -1,3 +1,4 @@
1
+ import { EncounterStatusR5 } from './claims/index.js';
1
2
  import { type FlatClaimResourceEntry } from './flat-claim-resource-graph.js';
2
3
  export type ClinicalCoding = Readonly<{
3
4
  system: string;
@@ -14,6 +15,23 @@ export type EncounterJourneyNode = Readonly<{
14
15
  locationReference: string;
15
16
  type?: ClinicalCoding;
16
17
  }>;
18
+ /** Builds one FHIR R5 EncounterHistory milestone from canonical flat claims. */
19
+ export declare function buildEncounterHistoryR5FlatEntry(input: Readonly<{
20
+ entryId: string;
21
+ identifier: string;
22
+ encounterReference: string;
23
+ status: EncounterStatusR5;
24
+ class: ClinicalCoding;
25
+ serviceReference?: string;
26
+ subjectReference?: string;
27
+ subjectStatus?: ClinicalCoding;
28
+ actualPeriodStart?: string;
29
+ actualPeriodEnd?: string;
30
+ plannedStartDate?: string;
31
+ plannedEndDate?: string;
32
+ locationReference?: string;
33
+ locationForm?: ClinicalCoding;
34
+ }>): FlatClaimResourceEntry;
17
35
  /**
18
36
  * Builds one actual-care Encounter graph from an originating Appointment.
19
37
  * Child service/location encounters inherit subject and provider and reference
@@ -1,5 +1,33 @@
1
- import { CommunicationRequestClaim, EncounterClaim, ServiceRequestClaim, TaskClaim, } from './claims/index.js';
1
+ import { CommunicationRequestClaim, EncounterClaim, EncounterHistoryClaim, ServiceRequestClaim, TaskClaim, } from './claims/index.js';
2
2
  import { buildFlatClaimResourceEntry } from './flat-claim-resource-graph.js';
3
+ /** Builds one FHIR R5 EncounterHistory milestone from canonical flat claims. */
4
+ export function buildEncounterHistoryR5FlatEntry(input) {
5
+ const claims = {
6
+ [EncounterHistoryClaim.Identifier]: required(input.identifier, 'encounter_history_identifier_required'),
7
+ [EncounterHistoryClaim.Encounter]: reference(input.encounterReference, 'encounter_history_encounter_invalid'),
8
+ [EncounterHistoryClaim.Status]: input.status,
9
+ [EncounterHistoryClaim.Class]: coding(input.class, 'encounter_history_class_invalid'),
10
+ };
11
+ if (input.serviceReference)
12
+ claims[EncounterHistoryClaim.ServiceType] = reference(input.serviceReference, 'encounter_history_service_invalid');
13
+ if (input.subjectReference)
14
+ claims[EncounterHistoryClaim.Subject] = reference(input.subjectReference, 'encounter_history_subject_invalid');
15
+ if (input.subjectStatus)
16
+ claims[EncounterHistoryClaim.SubjectStatus] = coding(input.subjectStatus, 'encounter_history_subject_status_invalid');
17
+ if (input.actualPeriodStart)
18
+ claims[EncounterHistoryClaim.ActualPeriodStart] = dateTime(input.actualPeriodStart, 'encounter_history_actual_start_invalid');
19
+ if (input.actualPeriodEnd)
20
+ claims[EncounterHistoryClaim.ActualPeriodEnd] = dateTime(input.actualPeriodEnd, 'encounter_history_actual_end_invalid');
21
+ if (input.plannedStartDate)
22
+ claims[EncounterHistoryClaim.PlannedStartDate] = dateTime(input.plannedStartDate, 'encounter_history_planned_start_invalid');
23
+ if (input.plannedEndDate)
24
+ claims[EncounterHistoryClaim.PlannedEndDate] = dateTime(input.plannedEndDate, 'encounter_history_planned_end_invalid');
25
+ if (input.locationReference)
26
+ claims[EncounterHistoryClaim.Location] = reference(input.locationReference, 'encounter_history_location_invalid');
27
+ if (input.locationForm)
28
+ claims[EncounterHistoryClaim.LocationForm] = coding(input.locationForm, 'encounter_history_location_form_invalid');
29
+ return buildFlatClaimResourceEntry({ entryId: input.entryId, resourceType: 'EncounterHistory', claims });
30
+ }
3
31
  /**
4
32
  * Builds one actual-care Encounter graph from an originating Appointment.
5
33
  * Child service/location encounters inherit subject and provider and reference
@@ -8,9 +8,12 @@ export declare const EncounterClaim: {
8
8
  readonly Subject: "Encounter.subject";
9
9
  readonly Patient: "Encounter.patient";
10
10
  readonly Participant: "Encounter.participant";
11
+ readonly ServiceType: "Encounter.service-type";
11
12
  readonly ServiceProvider: "Encounter.service-provider";
12
13
  readonly PeriodStart: "Encounter.period-start";
13
14
  readonly PeriodEnd: "Encounter.period-end";
15
+ readonly ActualPeriodStart: "Encounter.actual-period-start";
16
+ readonly ActualPeriodEnd: "Encounter.actual-period-end";
14
17
  readonly ReasonCode: "Encounter.reason-code";
15
18
  readonly Diagnosis: "Encounter.diagnosis";
16
19
  readonly Location: "Encounter.location";
@@ -28,5 +31,17 @@ export declare enum EncounterStatus {
28
31
  EnteredInError = "entered-in-error",
29
32
  Unknown = "unknown"
30
33
  }
34
+ /** Canonical FHIR R5 Encounter lifecycle codes. */
35
+ export declare enum EncounterStatusR5 {
36
+ Planned = "planned",
37
+ InProgress = "in-progress",
38
+ OnHold = "on-hold",
39
+ Discharged = "discharged",
40
+ Completed = "completed",
41
+ Cancelled = "cancelled",
42
+ Discontinued = "discontinued",
43
+ EnteredInError = "entered-in-error",
44
+ Unknown = "unknown"
45
+ }
31
46
  export type EncounterClaimKey = typeof EncounterClaim[keyof typeof EncounterClaim];
32
47
  export declare const EncounterClaimSpecs: ClaimSpec[];
@@ -9,9 +9,12 @@ export const EncounterClaim = {
9
9
  Subject: 'Encounter.subject',
10
10
  Patient: 'Encounter.patient',
11
11
  Participant: 'Encounter.participant',
12
+ ServiceType: 'Encounter.service-type',
12
13
  ServiceProvider: 'Encounter.service-provider',
13
14
  PeriodStart: 'Encounter.period-start',
14
15
  PeriodEnd: 'Encounter.period-end',
16
+ ActualPeriodStart: 'Encounter.actual-period-start',
17
+ ActualPeriodEnd: 'Encounter.actual-period-end',
15
18
  ReasonCode: 'Encounter.reason-code',
16
19
  Diagnosis: 'Encounter.diagnosis',
17
20
  Location: 'Encounter.location',
@@ -30,6 +33,19 @@ export var EncounterStatus;
30
33
  EncounterStatus["EnteredInError"] = "entered-in-error";
31
34
  EncounterStatus["Unknown"] = "unknown";
32
35
  })(EncounterStatus || (EncounterStatus = {}));
36
+ /** Canonical FHIR R5 Encounter lifecycle codes. */
37
+ export var EncounterStatusR5;
38
+ (function (EncounterStatusR5) {
39
+ EncounterStatusR5["Planned"] = "planned";
40
+ EncounterStatusR5["InProgress"] = "in-progress";
41
+ EncounterStatusR5["OnHold"] = "on-hold";
42
+ EncounterStatusR5["Discharged"] = "discharged";
43
+ EncounterStatusR5["Completed"] = "completed";
44
+ EncounterStatusR5["Cancelled"] = "cancelled";
45
+ EncounterStatusR5["Discontinued"] = "discontinued";
46
+ EncounterStatusR5["EnteredInError"] = "entered-in-error";
47
+ EncounterStatusR5["Unknown"] = "unknown";
48
+ })(EncounterStatusR5 || (EncounterStatusR5 = {}));
33
49
  export const EncounterClaimSpecs = [
34
50
  { key: EncounterClaim.Identifier, meaning: 'Business identifier.', example: 'enc-001' },
35
51
  { key: EncounterClaim.Appointment, meaning: 'Originating appointment references (CSV).', example: 'Appointment/appointment-1' },
@@ -39,9 +55,12 @@ export const EncounterClaimSpecs = [
39
55
  { key: EncounterClaim.Subject, meaning: 'Canonical subject reference.', example: 'did:web:patient.example.org' },
40
56
  { key: EncounterClaim.Patient, meaning: 'Patient alias reference.', example: 'did:web:patient.example.org' },
41
57
  { key: EncounterClaim.Participant, meaning: 'Participant references (CSV).', example: 'Practitioner/prac-1' },
58
+ { key: EncounterClaim.ServiceType, meaning: 'HealthcareService references (CSV in R5).', example: 'HealthcareService/emergency-department-1' },
42
59
  { key: EncounterClaim.ServiceProvider, meaning: 'Service provider reference.', example: 'Organization/org-1' },
43
60
  { key: EncounterClaim.PeriodStart, meaning: 'Encounter start.', example: '2026-06-01T10:00:00Z' },
44
61
  { key: EncounterClaim.PeriodEnd, meaning: 'Encounter end.', example: '2026-06-01T10:30:00Z' },
62
+ { key: EncounterClaim.ActualPeriodStart, meaning: 'R5 actual encounter start.', example: '2026-06-01T10:00:00Z' },
63
+ { key: EncounterClaim.ActualPeriodEnd, meaning: 'R5 actual encounter end.', example: '2026-06-01T10:30:00Z' },
45
64
  { key: EncounterClaim.ReasonCode, meaning: 'Reason code token.', example: 'http://snomed.info/sct|65363002' },
46
65
  { key: EncounterClaim.Diagnosis, meaning: 'Diagnosis references (CSV).', example: 'Condition/cond-1' },
47
66
  { key: EncounterClaim.Location, meaning: 'Location references (CSV).', example: 'Location/loc-1' },
@@ -0,0 +1,20 @@
1
+ import type { ClaimSpec } from './claim-types.js';
2
+ /** Canonical flat claims for the FHIR R5 EncounterHistory resource. */
3
+ export declare const EncounterHistoryClaim: {
4
+ readonly Identifier: "EncounterHistory.identifier";
5
+ readonly Encounter: "EncounterHistory.encounter";
6
+ readonly Status: "EncounterHistory.status";
7
+ readonly Class: "EncounterHistory.class";
8
+ readonly Type: "EncounterHistory.type";
9
+ readonly ServiceType: "EncounterHistory.service-type";
10
+ readonly Subject: "EncounterHistory.subject";
11
+ readonly SubjectStatus: "EncounterHistory.subject-status";
12
+ readonly ActualPeriodStart: "EncounterHistory.actual-period-start";
13
+ readonly ActualPeriodEnd: "EncounterHistory.actual-period-end";
14
+ readonly PlannedStartDate: "EncounterHistory.planned-start-date";
15
+ readonly PlannedEndDate: "EncounterHistory.planned-end-date";
16
+ readonly Location: "EncounterHistory.location";
17
+ readonly LocationForm: "EncounterHistory.location-form";
18
+ };
19
+ export type EncounterHistoryClaimKey = typeof EncounterHistoryClaim[keyof typeof EncounterHistoryClaim];
20
+ export declare const EncounterHistoryClaimSpecs: ClaimSpec[];
@@ -0,0 +1,34 @@
1
+ // Copyright 2026 Conéctate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
2
+ /** Canonical flat claims for the FHIR R5 EncounterHistory resource. */
3
+ export const EncounterHistoryClaim = {
4
+ Identifier: 'EncounterHistory.identifier',
5
+ Encounter: 'EncounterHistory.encounter',
6
+ Status: 'EncounterHistory.status',
7
+ Class: 'EncounterHistory.class',
8
+ Type: 'EncounterHistory.type',
9
+ ServiceType: 'EncounterHistory.service-type',
10
+ Subject: 'EncounterHistory.subject',
11
+ SubjectStatus: 'EncounterHistory.subject-status',
12
+ ActualPeriodStart: 'EncounterHistory.actual-period-start',
13
+ ActualPeriodEnd: 'EncounterHistory.actual-period-end',
14
+ PlannedStartDate: 'EncounterHistory.planned-start-date',
15
+ PlannedEndDate: 'EncounterHistory.planned-end-date',
16
+ Location: 'EncounterHistory.location',
17
+ LocationForm: 'EncounterHistory.location-form',
18
+ };
19
+ export const EncounterHistoryClaimSpecs = [
20
+ { key: EncounterHistoryClaim.Identifier, meaning: 'Business identifier.', example: 'urn:uuid:encounter-history-1' },
21
+ { key: EncounterHistoryClaim.Encounter, meaning: 'Encounter whose milestone is recorded.', example: 'Encounter/encounter-1' },
22
+ { key: EncounterHistoryClaim.Status, meaning: 'FHIR R5 Encounter status at this milestone.', example: 'on-hold' },
23
+ { key: EncounterHistoryClaim.Class, meaning: 'Encounter class token.', example: 'http://terminology.hl7.org/CodeSystem/v3-ActCode|EMER' },
24
+ { key: EncounterHistoryClaim.Type, meaning: 'Encounter type tokens (CSV).', example: 'http://snomed.info/sct|50849002' },
25
+ { key: EncounterHistoryClaim.ServiceType, meaning: 'HealthcareService references (CSV).', example: 'HealthcareService/emergency-1' },
26
+ { key: EncounterHistoryClaim.Subject, meaning: 'Patient or Group reference.', example: 'Patient/patient-1' },
27
+ { key: EncounterHistoryClaim.SubjectStatus, meaning: 'Subject status token.', example: 'departed' },
28
+ { key: EncounterHistoryClaim.ActualPeriodStart, meaning: 'Milestone period start.', example: '2026-09-29T08:00:00Z' },
29
+ { key: EncounterHistoryClaim.ActualPeriodEnd, meaning: 'Milestone period end.', example: '2026-09-29T08:30:00Z' },
30
+ { key: EncounterHistoryClaim.PlannedStartDate, meaning: 'Planned start.', example: '2026-09-29T08:00:00Z' },
31
+ { key: EncounterHistoryClaim.PlannedEndDate, meaning: 'Planned end.', example: '2026-09-29T08:30:00Z' },
32
+ { key: EncounterHistoryClaim.Location, meaning: 'Location references (CSV).', example: 'Location/emergency-box-1' },
33
+ { key: EncounterHistoryClaim.LocationForm, meaning: 'Location form token.', example: 'http://terminology.hl7.org/CodeSystem/location-physical-type|ro' },
34
+ ];
@@ -16,6 +16,7 @@ export * from './device-use-statement-claims.js';
16
16
  export * from './diagnostic-report-claims.js';
17
17
  export * from './document-reference-claims.js';
18
18
  export * from './encounter-claims.js';
19
+ export * from './encounter-history-claims.js';
19
20
  export * from './flag-claims.js';
20
21
  export * from './immunization-claims.js';
21
22
  export * from './immunization-recommendation-claims.js';
@@ -16,6 +16,7 @@ export * from './device-use-statement-claims.js';
16
16
  export * from './diagnostic-report-claims.js';
17
17
  export * from './document-reference-claims.js';
18
18
  export * from './encounter-claims.js';
19
+ export * from './encounter-history-claims.js';
19
20
  export * from './flag-claims.js';
20
21
  export * from './immunization-claims.js';
21
22
  export * from './immunization-recommendation-claims.js';
@@ -116,6 +116,7 @@ export declare const FhirResourceTypes: Readonly<{
116
116
  }>;
117
117
  /** FHIR R5-only resources currently emitted by shared subscription contracts. */
118
118
  export declare const ResourceTypesFhirR5: Readonly<{
119
+ readonly EncounterHistory: "EncounterHistory";
119
120
  readonly Subscription: "Subscription";
120
121
  readonly SubscriptionTopic: "SubscriptionTopic";
121
122
  readonly SubscriptionStatus: "SubscriptionStatus";
@@ -65,6 +65,7 @@ export const FhirResourceTypes = ResourceTypesFhirR4;
65
65
  /** FHIR R5-only resources currently emitted by shared subscription contracts. */
66
66
  export const ResourceTypesFhirR5 = Object.freeze({
67
67
  ...ResourceTypesFhirR4,
68
+ EncounterHistory: 'EncounterHistory',
68
69
  Subscription: 'Subscription',
69
70
  SubscriptionTopic: 'SubscriptionTopic',
70
71
  SubscriptionStatus: 'SubscriptionStatus',
@@ -0,0 +1,5 @@
1
+ import type { FhirResource, FlatClaims } from './convert-shared.js';
2
+ /** Materializes canonical EncounterHistory claims as native FHIR R5. */
3
+ export declare function encounterHistoryFlatToFhirR5(claims: FlatClaims): FhirResource;
4
+ /** Extracts supported native FHIR R5 EncounterHistory fields into flat claims. */
5
+ export declare function encounterHistoryFhirR5ToFlat(resource: FhirResource): FlatClaims;
@@ -0,0 +1,83 @@
1
+ // Copyright 2026 Conéctate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
2
+ import { EncounterHistoryClaim } from '../claims/encounter-history-claims.js';
3
+ import { EncounterStatusR5 } from '../claims/encounter-claims.js';
4
+ import { codingFromValue, codingToValue, referenceToValue } from './convert-shared.js';
5
+ const R5_STATUSES = new Set(Object.values(EncounterStatusR5));
6
+ /** Materializes canonical EncounterHistory claims as native FHIR R5. */
7
+ export function encounterHistoryFlatToFhirR5(claims) {
8
+ const status = claims[EncounterHistoryClaim.Status];
9
+ if (!status || !R5_STATUSES.has(status))
10
+ throw new TypeError('encounter_history_r5_status_invalid');
11
+ const klass = claims[EncounterHistoryClaim.Class];
12
+ if (!klass)
13
+ throw new TypeError('encounter_history_r5_class_required');
14
+ return {
15
+ resourceType: 'EncounterHistory',
16
+ identifier: claims[EncounterHistoryClaim.Identifier] ? [{ value: claims[EncounterHistoryClaim.Identifier] }] : undefined,
17
+ encounter: claims[EncounterHistoryClaim.Encounter] ? { reference: claims[EncounterHistoryClaim.Encounter] } : undefined,
18
+ status,
19
+ class: { coding: codingFromValue(klass) },
20
+ type: concepts(claims[EncounterHistoryClaim.Type]),
21
+ serviceType: codeableReferences(claims[EncounterHistoryClaim.ServiceType]),
22
+ subject: claims[EncounterHistoryClaim.Subject] ? { reference: claims[EncounterHistoryClaim.Subject] } : undefined,
23
+ subjectStatus: claims[EncounterHistoryClaim.SubjectStatus] ? { coding: codingFromValue(claims[EncounterHistoryClaim.SubjectStatus]) } : undefined,
24
+ actualPeriod: period(claims[EncounterHistoryClaim.ActualPeriodStart], claims[EncounterHistoryClaim.ActualPeriodEnd]),
25
+ plannedStartDate: claims[EncounterHistoryClaim.PlannedStartDate],
26
+ plannedEndDate: claims[EncounterHistoryClaim.PlannedEndDate],
27
+ location: locations(claims[EncounterHistoryClaim.Location], claims[EncounterHistoryClaim.LocationForm]),
28
+ };
29
+ }
30
+ /** Extracts supported native FHIR R5 EncounterHistory fields into flat claims. */
31
+ export function encounterHistoryFhirR5ToFlat(resource) {
32
+ const klass = resource.class;
33
+ const actualPeriod = resource.actualPeriod;
34
+ const location = resource.location?.[0];
35
+ return {
36
+ [EncounterHistoryClaim.Identifier]: resource.identifier?.[0]?.value,
37
+ [EncounterHistoryClaim.Encounter]: referenceToValue(resource.encounter),
38
+ [EncounterHistoryClaim.Status]: resource.status,
39
+ [EncounterHistoryClaim.Class]: codingToValue(klass?.coding?.[0]),
40
+ [EncounterHistoryClaim.Type]: conceptsToCsv(resource.type),
41
+ [EncounterHistoryClaim.ServiceType]: codeableReferencesToCsv(resource.serviceType),
42
+ [EncounterHistoryClaim.Subject]: referenceToValue(resource.subject),
43
+ [EncounterHistoryClaim.SubjectStatus]: codingToValue(resource.subjectStatus?.coding?.[0]),
44
+ [EncounterHistoryClaim.ActualPeriodStart]: actualPeriod?.start,
45
+ [EncounterHistoryClaim.ActualPeriodEnd]: actualPeriod?.end,
46
+ [EncounterHistoryClaim.PlannedStartDate]: resource.plannedStartDate,
47
+ [EncounterHistoryClaim.PlannedEndDate]: resource.plannedEndDate,
48
+ [EncounterHistoryClaim.Location]: location?.location?.reference,
49
+ [EncounterHistoryClaim.LocationForm]: codingToValue(location?.form?.coding?.[0]),
50
+ };
51
+ }
52
+ function period(start, end) {
53
+ return start || end ? { start, end } : undefined;
54
+ }
55
+ function concepts(value) {
56
+ const values = value?.split(',').map(item => item.trim()).filter(Boolean) ?? [];
57
+ return values.length ? values.map(item => ({ coding: codingFromValue(item) })) : undefined;
58
+ }
59
+ function conceptsToCsv(value) {
60
+ if (!Array.isArray(value))
61
+ return undefined;
62
+ const values = value.map(item => codingToValue(item?.coding?.[0])).filter(Boolean);
63
+ return values.length ? values.join(',') : undefined;
64
+ }
65
+ function codeableReferences(value) {
66
+ const values = value?.split(',').map(item => item.trim()).filter(Boolean) ?? [];
67
+ return values.length ? values.map(reference => ({ reference: { reference } })) : undefined;
68
+ }
69
+ function codeableReferencesToCsv(value) {
70
+ if (!Array.isArray(value))
71
+ return undefined;
72
+ const values = value.map(item => referenceToValue(item?.reference)).filter(Boolean);
73
+ return values.length ? values.join(',') : undefined;
74
+ }
75
+ function locations(locationValue, formValue) {
76
+ const values = locationValue?.split(',').map(item => item.trim()).filter(Boolean) ?? [];
77
+ if (!values.length)
78
+ return undefined;
79
+ return values.map((reference, index) => ({
80
+ location: { reference },
81
+ form: index === 0 && formValue ? { coding: codingFromValue(formValue) } : undefined,
82
+ }));
83
+ }
@@ -1,3 +1,7 @@
1
1
  import type { FhirResource, FlatClaims } from './convert-shared.js';
2
2
  export declare function encounterFlatToFhirR4(claims: FlatClaims): FhirResource;
3
3
  export declare function encounterFhirR4ToFlat(resource: FhirResource): FlatClaims;
4
+ /** Materializes canonical Encounter claims as native FHIR R5. */
5
+ export declare function encounterFlatToFhirR5(claims: FlatClaims): FhirResource;
6
+ /** Extracts the supported R5 Encounter fields into canonical flat claims. */
7
+ export declare function encounterFhirR5ToFlat(resource: FhirResource): FlatClaims;
@@ -1,6 +1,6 @@
1
1
  // Copyright 2026 Conéctate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
2
2
  // File: src/utils/convert-encounter.ts
3
- import { EncounterClaim } from '../claims/encounter-claims.js';
3
+ import { EncounterClaim, EncounterStatusR5 } from '../claims/encounter-claims.js';
4
4
  import { codingFromValue, codingToValue, referenceListToCsv, referenceToValue } from './convert-shared.js';
5
5
  export function encounterFlatToFhirR4(claims) {
6
6
  const subject = claims[EncounterClaim.Subject] ?? claims[EncounterClaim.Patient];
@@ -72,3 +72,75 @@ export function encounterFhirR4ToFlat(resource) {
72
72
  [EncounterClaim.PartOf]: referenceToValue(resource.partOf),
73
73
  };
74
74
  }
75
+ const R5_STATUSES = new Set(Object.values(EncounterStatusR5));
76
+ /** Materializes canonical Encounter claims as native FHIR R5. */
77
+ export function encounterFlatToFhirR5(claims) {
78
+ const status = claims[EncounterClaim.Status];
79
+ if (status && !R5_STATUSES.has(status))
80
+ throw new TypeError('encounter_r5_status_invalid');
81
+ const subject = claims[EncounterClaim.Subject] ?? claims[EncounterClaim.Patient];
82
+ const actualPeriodStart = claims[EncounterClaim.ActualPeriodStart] ?? claims[EncounterClaim.PeriodStart];
83
+ const actualPeriodEnd = claims[EncounterClaim.ActualPeriodEnd] ?? claims[EncounterClaim.PeriodEnd];
84
+ return {
85
+ resourceType: 'Encounter',
86
+ identifier: claims[EncounterClaim.Identifier] ? [{ value: claims[EncounterClaim.Identifier] }] : undefined,
87
+ appointment: referenceList(claims[EncounterClaim.Appointment]),
88
+ status,
89
+ class: claims[EncounterClaim.Class] ? { coding: codingFromValue(claims[EncounterClaim.Class]) } : undefined,
90
+ type: claims[EncounterClaim.Type] ? [{ coding: codingFromValue(claims[EncounterClaim.Type]) }] : undefined,
91
+ serviceType: codeableReferenceList(claims[EncounterClaim.ServiceType]),
92
+ subject: subject ? { reference: subject } : undefined,
93
+ participant: claims[EncounterClaim.Participant]
94
+ ? claims[EncounterClaim.Participant].split(',').map(reference => ({ actor: { reference: reference.trim() } }))
95
+ : undefined,
96
+ serviceProvider: claims[EncounterClaim.ServiceProvider] ? { reference: claims[EncounterClaim.ServiceProvider] } : undefined,
97
+ actualPeriod: (actualPeriodStart || actualPeriodEnd) ? { start: actualPeriodStart, end: actualPeriodEnd } : undefined,
98
+ location: claims[EncounterClaim.Location]
99
+ ? claims[EncounterClaim.Location].split(',').map(reference => ({ location: { reference: reference.trim() } }))
100
+ : undefined,
101
+ partOf: claims[EncounterClaim.PartOf] ? { reference: claims[EncounterClaim.PartOf] } : undefined,
102
+ };
103
+ }
104
+ /** Extracts the supported R5 Encounter fields into canonical flat claims. */
105
+ export function encounterFhirR5ToFlat(resource) {
106
+ const klass = resource.class;
107
+ const actualPeriod = resource.actualPeriod;
108
+ return {
109
+ [EncounterClaim.Identifier]: resource.identifier?.[0]?.value,
110
+ [EncounterClaim.Appointment]: referenceListToCsv(resource.appointment),
111
+ [EncounterClaim.Status]: resource.status,
112
+ [EncounterClaim.Class]: codingToValue(klass?.coding?.[0]),
113
+ [EncounterClaim.Type]: codingToValue(resource.type?.[0]?.coding?.[0]),
114
+ [EncounterClaim.ServiceType]: codeableReferencesToCsv(resource.serviceType),
115
+ [EncounterClaim.Subject]: referenceToValue(resource.subject),
116
+ [EncounterClaim.Patient]: referenceToValue(resource.subject),
117
+ [EncounterClaim.Participant]: Array.isArray(resource.participant)
118
+ ? resource.participant.map(item => item.actor?.reference).filter(Boolean).join(',') || undefined
119
+ : undefined,
120
+ [EncounterClaim.ServiceProvider]: referenceToValue(resource.serviceProvider),
121
+ [EncounterClaim.ActualPeriodStart]: actualPeriod?.start,
122
+ [EncounterClaim.ActualPeriodEnd]: actualPeriod?.end,
123
+ [EncounterClaim.Location]: Array.isArray(resource.location)
124
+ ? resource.location.map(item => item.location?.reference).filter(Boolean).join(',') || undefined
125
+ : undefined,
126
+ [EncounterClaim.PartOf]: referenceToValue(resource.partOf),
127
+ };
128
+ }
129
+ function referenceList(value) {
130
+ const references = value?.split(',').map(reference => reference.trim()).filter(Boolean) ?? [];
131
+ return references.length ? references.map(reference => ({ reference })) : undefined;
132
+ }
133
+ function codeableReferenceList(value) {
134
+ const references = referenceList(value);
135
+ return references?.map(reference => ({ reference }));
136
+ }
137
+ function codeableReferencesToCsv(value) {
138
+ if (!Array.isArray(value))
139
+ return undefined;
140
+ const references = value.map(item => {
141
+ if (!item || typeof item !== 'object')
142
+ return undefined;
143
+ return referenceToValue(item.reference);
144
+ }).filter((item) => Boolean(item));
145
+ return references.length ? references.join(',') : undefined;
146
+ }
@@ -16,6 +16,7 @@ export * from './convert-device-metric.js';
16
16
  export * from './convert-diagnostic-report.js';
17
17
  export * from './convert-document-reference.js';
18
18
  export * from './convert-encounter.js';
19
+ export * from './convert-encounter-history.js';
19
20
  export * from './convert-flag.js';
20
21
  export * from './convert-immunization.js';
21
22
  export * from './convert-imaging-study.js';
@@ -16,6 +16,7 @@ export * from './convert-device-metric.js';
16
16
  export * from './convert-diagnostic-report.js';
17
17
  export * from './convert-document-reference.js';
18
18
  export * from './convert-encounter.js';
19
+ export * from './convert-encounter-history.js';
19
20
  export * from './convert-flag.js';
20
21
  export * from './convert-immunization.js';
21
22
  export * from './convert-imaging-study.js';
@@ -1,6 +1,6 @@
1
1
  import type { FhirResource } from '../fhir-types.js';
2
2
  export type ClaimsFirstFhirResource = Readonly<{
3
- resourceType: 'Organization' | 'HealthcareService' | 'Location' | 'Schedule' | 'Slot' | 'DeviceMetric' | 'Procedure' | 'RegulatedAuthorization' | 'DocumentReference';
3
+ resourceType: 'Organization' | 'HealthcareService' | 'Location' | 'Schedule' | 'Slot' | 'DeviceMetric' | 'Procedure' | 'RegulatedAuthorization' | 'DocumentReference' | 'Encounter' | 'EncounterHistory';
4
4
  id: string;
5
5
  meta: Readonly<{
6
6
  claims: Readonly<Record<string, unknown>>;
@@ -6,6 +6,8 @@ import { deviceMetricFlatToFhirR5 } from './convert-device-metric.js';
6
6
  import { procedureFlatToFhirR5 } from './convert-procedure.js';
7
7
  import { regulatedAuthorizationFlatToFhirR5 } from './convert-regulated-authorization.js';
8
8
  import { documentReferenceFlatToFhirR4 } from './convert-document-reference.js';
9
+ import { encounterFlatToFhirR5 } from './convert-encounter.js';
10
+ import { encounterHistoryFlatToFhirR5 } from './convert-encounter-history.js';
9
11
  const materializers = Object.freeze({
10
12
  Organization: organizationFlatToFhirR5,
11
13
  HealthcareService: healthcareServiceFlatToFhirR5,
@@ -16,6 +18,8 @@ const materializers = Object.freeze({
16
18
  Procedure: procedureFlatToFhirR5,
17
19
  RegulatedAuthorization: regulatedAuthorizationFlatToFhirR5,
18
20
  DocumentReference: documentReferenceFlatToFhirR4,
21
+ Encounter: encounterFlatToFhirR5,
22
+ EncounterHistory: encounterHistoryFlatToFhirR5,
19
23
  });
20
24
  /**
21
25
  * Materializes native FHIR R5 only at an explicit adapter boundary.
@@ -0,0 +1,41 @@
1
+ import { EncounterStatusR5 } from './claims/encounter-claims.js';
2
+ /** Product-neutral identifiers and references for cross-package Encounter transfer contract tests. */
3
+ export declare const FHIR_ENCOUNTER_TRANSFER_TEST_DATA: Readonly<{
4
+ communicationIdentifier: "urn:uuid:00000000-0000-4000-8000-000000000401";
5
+ subjectReference: "Patient/00000000-0000-4000-8000-000000000402";
6
+ senderReference: "Organization/00000000-0000-4000-8000-000000000403";
7
+ indexRecipientReference: "Organization/00000000-0000-4000-8000-000000000404";
8
+ occurredAt: "2026-09-30T10:10:00Z";
9
+ invalidOccurredAtWithoutTimezone: "2026-09-30T10:10:00";
10
+ class: Readonly<{
11
+ system: "http://terminology.hl7.org/CodeSystem/v3-ActCode";
12
+ code: "EMER";
13
+ }>;
14
+ source: Readonly<{
15
+ id: "00000000-0000-4000-8000-000000000405";
16
+ reference: "Encounter/00000000-0000-4000-8000-000000000405";
17
+ identifier: "urn:uuid:00000000-0000-4000-8000-000000000405";
18
+ serviceReference: "HealthcareService/00000000-0000-4000-8000-000000000406";
19
+ locationReference: "Location/00000000-0000-4000-8000-000000000407";
20
+ statusWhenReturning: EncounterStatusR5.OnHold;
21
+ statusWhenNotReturning: EncounterStatusR5.Completed;
22
+ }>;
23
+ destination: Readonly<{
24
+ id: "00000000-0000-4000-8000-000000000408";
25
+ reference: "Encounter/00000000-0000-4000-8000-000000000408";
26
+ identifier: "urn:uuid:00000000-0000-4000-8000-000000000408";
27
+ serviceReference: "HealthcareService/00000000-0000-4000-8000-000000000409";
28
+ locationReference: "Location/00000000-0000-4000-8000-000000000410";
29
+ statusOnArrival: EncounterStatusR5.InProgress;
30
+ }>;
31
+ sourceHistory: Readonly<{
32
+ id: "00000000-0000-4000-8000-000000000411";
33
+ identifier: "urn:uuid:00000000-0000-4000-8000-000000000411";
34
+ encounterReference: "Encounter/00000000-0000-4000-8000-000000000405";
35
+ }>;
36
+ destinationHistory: Readonly<{
37
+ id: "00000000-0000-4000-8000-000000000412";
38
+ identifier: "urn:uuid:00000000-0000-4000-8000-000000000412";
39
+ encounterReference: "Encounter/00000000-0000-4000-8000-000000000408";
40
+ }>;
41
+ }>;
@@ -0,0 +1,38 @@
1
+ import { EncounterStatusR5 } from './claims/encounter-claims.js';
2
+ /** Product-neutral identifiers and references for cross-package Encounter transfer contract tests. */
3
+ export const FHIR_ENCOUNTER_TRANSFER_TEST_DATA = Object.freeze({
4
+ communicationIdentifier: 'urn:uuid:00000000-0000-4000-8000-000000000401',
5
+ subjectReference: 'Patient/00000000-0000-4000-8000-000000000402',
6
+ senderReference: 'Organization/00000000-0000-4000-8000-000000000403',
7
+ indexRecipientReference: 'Organization/00000000-0000-4000-8000-000000000404',
8
+ occurredAt: '2026-09-30T10:10:00Z',
9
+ invalidOccurredAtWithoutTimezone: '2026-09-30T10:10:00',
10
+ class: Object.freeze({ system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'EMER' }),
11
+ source: Object.freeze({
12
+ id: '00000000-0000-4000-8000-000000000405',
13
+ reference: 'Encounter/00000000-0000-4000-8000-000000000405',
14
+ identifier: 'urn:uuid:00000000-0000-4000-8000-000000000405',
15
+ serviceReference: 'HealthcareService/00000000-0000-4000-8000-000000000406',
16
+ locationReference: 'Location/00000000-0000-4000-8000-000000000407',
17
+ statusWhenReturning: EncounterStatusR5.OnHold,
18
+ statusWhenNotReturning: EncounterStatusR5.Completed,
19
+ }),
20
+ destination: Object.freeze({
21
+ id: '00000000-0000-4000-8000-000000000408',
22
+ reference: 'Encounter/00000000-0000-4000-8000-000000000408',
23
+ identifier: 'urn:uuid:00000000-0000-4000-8000-000000000408',
24
+ serviceReference: 'HealthcareService/00000000-0000-4000-8000-000000000409',
25
+ locationReference: 'Location/00000000-0000-4000-8000-000000000410',
26
+ statusOnArrival: EncounterStatusR5.InProgress,
27
+ }),
28
+ sourceHistory: Object.freeze({
29
+ id: '00000000-0000-4000-8000-000000000411',
30
+ identifier: 'urn:uuid:00000000-0000-4000-8000-000000000411',
31
+ encounterReference: 'Encounter/00000000-0000-4000-8000-000000000405',
32
+ }),
33
+ destinationHistory: Object.freeze({
34
+ id: '00000000-0000-4000-8000-000000000412',
35
+ identifier: 'urn:uuid:00000000-0000-4000-8000-000000000412',
36
+ encounterReference: 'Encounter/00000000-0000-4000-8000-000000000408',
37
+ }),
38
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Derives whole Unix epoch seconds from a valid FHIR `instant`.
3
+ *
4
+ * A FHIR instant remains the authoritative ISO 8601 value with a timezone.
5
+ * Epoch seconds are a derived numeric representation and must never replace
6
+ * `Encounter.id`, `Encounter.identifier`, or the original clinical time.
7
+ */
8
+ export declare function fhirInstantToEpochSeconds(instant: string): number;
@@ -0,0 +1,16 @@
1
+ const FHIR_INSTANT_PATTERN = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
2
+ /**
3
+ * Derives whole Unix epoch seconds from a valid FHIR `instant`.
4
+ *
5
+ * A FHIR instant remains the authoritative ISO 8601 value with a timezone.
6
+ * Epoch seconds are a derived numeric representation and must never replace
7
+ * `Encounter.id`, `Encounter.identifier`, or the original clinical time.
8
+ */
9
+ export function fhirInstantToEpochSeconds(instant) {
10
+ if (!FHIR_INSTANT_PATTERN.test(instant))
11
+ throw new TypeError('fhir_instant_invalid');
12
+ const epochMilliseconds = Date.parse(instant);
13
+ if (!Number.isFinite(epochMilliseconds))
14
+ throw new TypeError('fhir_instant_invalid');
15
+ return Math.floor(epochMilliseconds / 1000);
16
+ }
package/dist/index.d.ts CHANGED
@@ -26,3 +26,5 @@ export * from './imaging-types.js';
26
26
  export * from './subscription-r5.js';
27
27
  export * from './healthcare-service-specialty-map.js';
28
28
  export * from './appointment-communication-test-data.js';
29
+ export * from './encounter-transfer-test-data.js';
30
+ export * from './fhir-instant.js';
package/dist/index.js CHANGED
@@ -26,3 +26,5 @@ export * from './imaging-types.js';
26
26
  export * from './subscription-r5.js';
27
27
  export * from './healthcare-service-specialty-map.js';
28
28
  export * from './appointment-communication-test-data.js';
29
+ export * from './encounter-transfer-test-data.js';
30
+ export * from './fhir-instant.js';
@@ -0,0 +1,19 @@
1
+ # Encounter identifiers and time
2
+
3
+ FHIR time and encounter identity are separate concepts.
4
+
5
+ - A FHIR `instant` is an ISO 8601 date and time with at least second precision
6
+ and an explicit timezone. It is not a Unix timestamp.
7
+ - Unix epoch seconds may be derived for sorting, interoperability or a
8
+ provider-owned business identifier, but they never replace the original
9
+ instant.
10
+ - `Encounter.id` is the FHIR resource identifier.
11
+ - `Encounter.identifier` contains the business identifiers assigned by the
12
+ care provider. Their namespace and uniqueness belong to the assigning
13
+ organization.
14
+ - A display code is not sufficient authorization or subject resolution. An
15
+ authoritative lookup must also verify the assigner and exact subject.
16
+
17
+ Reusable test data lives in `FHIR_ENCOUNTER_TRANSFER_TEST_DATA`. Tests and
18
+ downstream documentation import its named fields instead of duplicating raw
19
+ identifiers, timestamps or references.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhir-data-utils-ts",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "FHIR-version-neutral flat-claim and SearchParameter data utilities",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",