sos-sdk-core-ts 0.4.0 → 0.5.0
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 +24 -0
- package/README.md +7 -5
- package/dist/bundle-changes-response.d.ts +32 -0
- package/dist/bundle-changes-response.js +154 -0
- package/dist/care-emergency-alert-orchestration.js +12 -23
- package/dist/communication-bundle.d.ts +34 -0
- package/dist/communication-bundle.js +64 -1
- package/dist/emergency-authorization.d.ts +1 -11
- package/dist/emergency-authorization.js +0 -2
- package/dist/encounter-transfer-communication.d.ts +40 -0
- package/dist/encounter-transfer-communication.js +120 -0
- package/dist/fhir-scheduling.d.ts +40 -0
- package/dist/fhir-scheduling.js +127 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/docs/COMMUNICATION_BUNDLE_EDITOR_READER.md +18 -17
- package/docs/ENCOUNTER_TRANSFER_PROJECTION.md +38 -0
- package/docs/FHIR_SCHEDULING_INTEROPERABILITY.md +28 -0
- package/package.json +15 -11
- package/dist/emergency-group.d.ts +0 -2
- package/dist/emergency-group.js +0 -2
- package/dist/place-card-association.d.ts +0 -2
- package/dist/place-card-association.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Add the claims-first Encounter transfer projection helper. It keeps a
|
|
6
|
+
returning source `on-hold`, completes a definitive source departure, starts
|
|
7
|
+
the destination `in-progress`, records both `EncounterHistory` milestones,
|
|
8
|
+
and sends the already-committed result to the subject index in one completed
|
|
9
|
+
Communication batch.
|
|
10
|
+
- Document that movement does not imply `discharged`, that department sender,
|
|
11
|
+
HealthcareService and authenticated audit actor remain distinct, and that
|
|
12
|
+
emergency-contact files stay DocumentReference resources inside the Bundle.
|
|
13
|
+
|
|
14
|
+
- Add the neutral `bundle-changes-response` contract and publish its scheduling
|
|
15
|
+
ownership guide. UHC consumers no longer expose a provisional copy.
|
|
16
|
+
|
|
17
|
+
- Move the standard Appointment-in-Communication authoring flow and the
|
|
18
|
+
capability-driven FHIR scheduling client into the shared SOS/Vet/UHC layer.
|
|
19
|
+
Product packages no longer need human or veterinary wrappers for standard
|
|
20
|
+
FHIR resources.
|
|
21
|
+
- Remove temporary SOSChain-prefixed emergency authorization aliases and data
|
|
22
|
+
reexport subpaths. Emergency Group and card-to-Place records now come from
|
|
23
|
+
their single owner, `sos-data-utils-ts@0.6.0`.
|
|
24
|
+
- Consume the complete shared Task status vocabulary directly instead of
|
|
25
|
+
maintaining a local compatibility overlay.
|
|
26
|
+
|
|
3
27
|
## 0.4.0 - 2026-09-29
|
|
4
28
|
|
|
5
29
|
- Add the shared high-level Communication Bundle editor and reader for SOS,
|
package/README.md
CHANGED
|
@@ -57,6 +57,10 @@ contains exactly one inner document, batch or collection Bundle in its
|
|
|
57
57
|
audio or `text/calendar` are DocumentReference entries inside the inner Bundle.
|
|
58
58
|
Native FHIR R4/R5 is materialized only at an explicit adapter boundary. See
|
|
59
59
|
[Communication Bundle editor and reader](docs/COMMUNICATION_BUNDLE_EDITOR_READER.md).
|
|
60
|
+
The shared scheduling and response-correlation boundary is documented in
|
|
61
|
+
[FHIR scheduling interoperability](docs/FHIR_SCHEDULING_INTEROPERABILITY.md).
|
|
62
|
+
Source-to-destination care movement and subject-index projection are documented
|
|
63
|
+
in [Encounter transfer projection](docs/ENCOUNTER_TRANSFER_PROJECTION.md).
|
|
60
64
|
|
|
61
65
|
The emergency-group contract stores subject references only on an enumerated
|
|
62
66
|
FHIR Group. A group assistance request references that Group from one standard,
|
|
@@ -72,11 +76,9 @@ The `incident-reporter` relationship represents a short-lived grant issued by
|
|
|
72
76
|
a trusted scan resolver to an already verified person at the scene; it does
|
|
73
77
|
not imply guardianship, control or professional status.
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
`buildSoschainPlaceCardAssociationClaims` records a UHC/VetChain card link to
|
|
79
|
-
a canonical SOSChain Place or vehicle without merging either identity.
|
|
79
|
+
Emergency evidence, Group records and card-to-Place associations are imported
|
|
80
|
+
directly from `sos-data-utils-ts`; this orchestration package does not retain
|
|
81
|
+
temporary reexport subpaths or product-prefixed authorization aliases.
|
|
80
82
|
|
|
81
83
|
## Care, emergency and alert orchestration
|
|
82
84
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BundleEntry, BundleJsonApi } from 'gdc-common-utils-ts/models/bundle';
|
|
2
|
+
export declare const BundleChangeResponseStates: Readonly<{
|
|
3
|
+
readonly Successful: "successful";
|
|
4
|
+
readonly Failed: "failed";
|
|
5
|
+
readonly Pending: "pending";
|
|
6
|
+
}>;
|
|
7
|
+
export type BundleChangeResponseState = typeof BundleChangeResponseStates[keyof typeof BundleChangeResponseStates];
|
|
8
|
+
export type BundleChangeResponseItem = Readonly<{
|
|
9
|
+
identifier: string;
|
|
10
|
+
changeEntryIndex: number;
|
|
11
|
+
responseEntryIndex?: number;
|
|
12
|
+
state: BundleChangeResponseState;
|
|
13
|
+
responseStatus?: string;
|
|
14
|
+
diagnostics: readonly string[];
|
|
15
|
+
}>;
|
|
16
|
+
export type BundleChangesResponseAnalysis = Readonly<{
|
|
17
|
+
entries: readonly BundleChangeResponseItem[];
|
|
18
|
+
successfulIdentifiers: readonly string[];
|
|
19
|
+
failedIdentifiers: readonly string[];
|
|
20
|
+
pendingIdentifiers: readonly string[];
|
|
21
|
+
diagnostics: readonly string[];
|
|
22
|
+
}>;
|
|
23
|
+
/** Reads every stable identifier carried by one Bundle entry in preference order. */
|
|
24
|
+
export declare function readBundleEntryCorrelationIdentifiers(entry: BundleEntry): readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* Correlates the submitted changes Bundle with the GW Bundle response.
|
|
27
|
+
*
|
|
28
|
+
* Correlation uses stable identifiers only, never array position. A missing,
|
|
29
|
+
* malformed or ambiguous response remains pending until the caller performs
|
|
30
|
+
* the resource-specific authoritative search.
|
|
31
|
+
*/
|
|
32
|
+
export declare function analyzeBundleChangesResponse(changesBundle: BundleJsonApi<BundleEntry>, responseBundle: BundleJsonApi<BundleEntry>): BundleChangesResponseAnalysis;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Copyright 2026 Connecting Solution & Applications Ltd under the Apache License, Version 2.0.
|
|
2
|
+
export const BundleChangeResponseStates = Object.freeze({
|
|
3
|
+
Successful: 'successful',
|
|
4
|
+
Failed: 'failed',
|
|
5
|
+
Pending: 'pending',
|
|
6
|
+
});
|
|
7
|
+
function asRecord(value) {
|
|
8
|
+
return value !== null && typeof value === 'object'
|
|
9
|
+
? value
|
|
10
|
+
: {};
|
|
11
|
+
}
|
|
12
|
+
function asNonEmptyString(value) {
|
|
13
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
14
|
+
? value.trim()
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
function appendIdentifier(target, value) {
|
|
18
|
+
const identifier = asNonEmptyString(value);
|
|
19
|
+
if (identifier && !target.includes(identifier))
|
|
20
|
+
target.push(identifier);
|
|
21
|
+
}
|
|
22
|
+
function appendClaimIdentifiers(target, claims) {
|
|
23
|
+
for (const [name, value] of Object.entries(claims)) {
|
|
24
|
+
if (name === 'identifier' || name.endsWith('.identifier') || name.endsWith('.identifier.value')) {
|
|
25
|
+
appendIdentifier(target, value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Reads every stable identifier carried by one Bundle entry in preference order. */
|
|
30
|
+
export function readBundleEntryCorrelationIdentifiers(entry) {
|
|
31
|
+
const identifiers = [];
|
|
32
|
+
const resource = asRecord(entry.resource);
|
|
33
|
+
const resourceMeta = asRecord(resource.meta);
|
|
34
|
+
const entryMeta = asRecord(entry.meta);
|
|
35
|
+
appendIdentifier(identifiers, entry.fullUrl);
|
|
36
|
+
appendIdentifier(identifiers, resource.id);
|
|
37
|
+
appendIdentifier(identifiers, entry.id);
|
|
38
|
+
appendClaimIdentifiers(identifiers, asRecord(resourceMeta.claims));
|
|
39
|
+
appendClaimIdentifiers(identifiers, asRecord(entryMeta.claims));
|
|
40
|
+
return identifiers;
|
|
41
|
+
}
|
|
42
|
+
function readBundleEntryResponseOutcome(entry) {
|
|
43
|
+
const response = asRecord(entry.response);
|
|
44
|
+
const outcome = asRecord(response.outcome);
|
|
45
|
+
const issues = Array.isArray(outcome.issue) ? outcome.issue.map(asRecord) : [];
|
|
46
|
+
const severities = [];
|
|
47
|
+
const diagnostics = [];
|
|
48
|
+
for (const issue of issues) {
|
|
49
|
+
const severity = asNonEmptyString(issue.severity)?.toLowerCase();
|
|
50
|
+
if (severity)
|
|
51
|
+
severities.push(severity);
|
|
52
|
+
const diagnostic = asNonEmptyString(issue.diagnostics)
|
|
53
|
+
?? asNonEmptyString(asRecord(issue.details).text);
|
|
54
|
+
if (diagnostic)
|
|
55
|
+
diagnostics.push(diagnostic);
|
|
56
|
+
}
|
|
57
|
+
const status = asNonEmptyString(response.status);
|
|
58
|
+
return { ...(status ? { status } : {}), severities, diagnostics };
|
|
59
|
+
}
|
|
60
|
+
function isSuccessfulStatus(status) {
|
|
61
|
+
return !!status && /^2\d\d(?:\s|$)/.test(status);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Correlates the submitted changes Bundle with the GW Bundle response.
|
|
65
|
+
*
|
|
66
|
+
* Correlation uses stable identifiers only, never array position. A missing,
|
|
67
|
+
* malformed or ambiguous response remains pending until the caller performs
|
|
68
|
+
* the resource-specific authoritative search.
|
|
69
|
+
*/
|
|
70
|
+
export function analyzeBundleChangesResponse(changesBundle, responseBundle) {
|
|
71
|
+
const responseEntries = responseBundle.data.map((entry, index) => ({
|
|
72
|
+
entry,
|
|
73
|
+
index,
|
|
74
|
+
identifiers: readBundleEntryCorrelationIdentifiers(entry),
|
|
75
|
+
}));
|
|
76
|
+
const seenChangeIdentifiers = new Set();
|
|
77
|
+
const claimedResponseIndexes = new Set();
|
|
78
|
+
const diagnostics = [];
|
|
79
|
+
const entries = [];
|
|
80
|
+
const changes = changesBundle.data.map((changeEntry, changeEntryIndex) => {
|
|
81
|
+
const identifiers = readBundleEntryCorrelationIdentifiers(changeEntry);
|
|
82
|
+
const identifier = identifiers[0];
|
|
83
|
+
if (!identifier) {
|
|
84
|
+
throw new Error(`Changes Bundle entry ${changeEntryIndex} requires a stable fullUrl, resource.id, entry.id or identifier claim.`);
|
|
85
|
+
}
|
|
86
|
+
if (seenChangeIdentifiers.has(identifier)) {
|
|
87
|
+
throw new Error(`Changes Bundle contains duplicate primary identifier '${identifier}'.`);
|
|
88
|
+
}
|
|
89
|
+
seenChangeIdentifiers.add(identifier);
|
|
90
|
+
return {
|
|
91
|
+
index: changeEntryIndex,
|
|
92
|
+
identifier,
|
|
93
|
+
matchingResponses: responseEntries.filter((candidate) => candidate.identifiers.some((candidateIdentifier) => identifiers.includes(candidateIdentifier))),
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
const responseMatchCounts = new Map();
|
|
97
|
+
changes.forEach(({ matchingResponses }) => matchingResponses.forEach(({ index }) => {
|
|
98
|
+
responseMatchCounts.set(index, (responseMatchCounts.get(index) ?? 0) + 1);
|
|
99
|
+
}));
|
|
100
|
+
changes.forEach(({ identifier, index: changeEntryIndex, matchingResponses }) => {
|
|
101
|
+
if (matchingResponses.length !== 1) {
|
|
102
|
+
const message = matchingResponses.length === 0
|
|
103
|
+
? `No response entry carries a stable identifier for change '${identifier}'.`
|
|
104
|
+
: `More than one response entry matches change '${identifier}'.`;
|
|
105
|
+
diagnostics.push(message);
|
|
106
|
+
entries.push({ identifier, changeEntryIndex, state: BundleChangeResponseStates.Pending, diagnostics: [message] });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const match = matchingResponses[0];
|
|
110
|
+
if ((responseMatchCounts.get(match.index) ?? 0) > 1) {
|
|
111
|
+
const message = `Response entry ${match.index} ambiguously matches more than one submitted change.`;
|
|
112
|
+
diagnostics.push(message);
|
|
113
|
+
entries.push({
|
|
114
|
+
identifier,
|
|
115
|
+
changeEntryIndex,
|
|
116
|
+
responseEntryIndex: match.index,
|
|
117
|
+
state: BundleChangeResponseStates.Pending,
|
|
118
|
+
diagnostics: [message],
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
claimedResponseIndexes.add(match.index);
|
|
123
|
+
const outcome = readBundleEntryResponseOutcome(match.entry);
|
|
124
|
+
const hasError = outcome.severities.some((severity) => severity === 'fatal' || severity === 'error');
|
|
125
|
+
const state = !outcome.status
|
|
126
|
+
? BundleChangeResponseStates.Pending
|
|
127
|
+
: isSuccessfulStatus(outcome.status) && !hasError
|
|
128
|
+
? BundleChangeResponseStates.Successful
|
|
129
|
+
: BundleChangeResponseStates.Failed;
|
|
130
|
+
const itemDiagnostics = [...outcome.diagnostics];
|
|
131
|
+
if (!outcome.status)
|
|
132
|
+
itemDiagnostics.push(`Response entry ${match.index} has no status.`);
|
|
133
|
+
entries.push({
|
|
134
|
+
identifier,
|
|
135
|
+
changeEntryIndex,
|
|
136
|
+
responseEntryIndex: match.index,
|
|
137
|
+
state,
|
|
138
|
+
...(outcome.status ? { responseStatus: outcome.status } : {}),
|
|
139
|
+
diagnostics: itemDiagnostics,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
responseEntries.forEach(({ index }) => {
|
|
143
|
+
if (!claimedResponseIndexes.has(index)) {
|
|
144
|
+
diagnostics.push(`Response entry ${index} does not correlate with one submitted change.`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
entries,
|
|
149
|
+
successfulIdentifiers: entries.filter(({ state }) => state === BundleChangeResponseStates.Successful).map(({ identifier }) => identifier),
|
|
150
|
+
failedIdentifiers: entries.filter(({ state }) => state === BundleChangeResponseStates.Failed).map(({ identifier }) => identifier),
|
|
151
|
+
pendingIdentifiers: entries.filter(({ state }) => state === BundleChangeResponseStates.Pending).map(({ identifier }) => identifier),
|
|
152
|
+
diagnostics,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -89,17 +89,6 @@ export function orchestrateEmergencyRequest(input) {
|
|
|
89
89
|
});
|
|
90
90
|
return Object.freeze({ intake, serviceRequestEntry, triageTaskEntry });
|
|
91
91
|
}
|
|
92
|
-
// Compatibility overlay for the published data package used by clean installs.
|
|
93
|
-
// The next grouped release exposes the same standard values directly there.
|
|
94
|
-
const EmergencyTaskStatus = Object.freeze({
|
|
95
|
-
...TaskStatus,
|
|
96
|
-
Received: 'received',
|
|
97
|
-
Accepted: 'accepted',
|
|
98
|
-
Rejected: 'rejected',
|
|
99
|
-
Ready: 'ready',
|
|
100
|
-
OnHold: 'on-hold',
|
|
101
|
-
EnteredInError: 'entered-in-error',
|
|
102
|
-
});
|
|
103
92
|
/**
|
|
104
93
|
* Reads only the standard, claims-first coordinates required by a responder
|
|
105
94
|
* console. It does not resolve contact endpoints or grant callback authority.
|
|
@@ -110,7 +99,7 @@ export function readEmergencyTriageTask(taskEntry) {
|
|
|
110
99
|
}
|
|
111
100
|
const claims = taskEntry.claims;
|
|
112
101
|
const status = scalarClaim(claims[TaskClaim.Status], 'emergency_triage_status_required');
|
|
113
|
-
if (!Object.values(
|
|
102
|
+
if (!Object.values(TaskStatus).includes(status)) {
|
|
114
103
|
throw new TypeError('emergency_triage_status_invalid');
|
|
115
104
|
}
|
|
116
105
|
return Object.freeze({
|
|
@@ -161,7 +150,7 @@ export function prepareEmergencyResponderCallback(input) {
|
|
|
161
150
|
const responderReference = reference(input.responderReference, 'PractitionerRole', 'emergency_callback_responder_invalid');
|
|
162
151
|
if (task.ownerReference !== responderReference)
|
|
163
152
|
throw new TypeError('emergency_callback_task_owner_required');
|
|
164
|
-
if (task.status !==
|
|
153
|
+
if (task.status !== TaskStatus.Accepted && task.status !== TaskStatus.InProgress) {
|
|
165
154
|
throw new TypeError('emergency_callback_active_task_required');
|
|
166
155
|
}
|
|
167
156
|
if (!task.communicationThreadId)
|
|
@@ -180,20 +169,20 @@ export function prepareEmergencyResponderCallback(input) {
|
|
|
180
169
|
}
|
|
181
170
|
function emergencyTriageTransition(currentStatus, action) {
|
|
182
171
|
const transitions = {
|
|
183
|
-
acknowledge: { [
|
|
184
|
-
start: { [
|
|
172
|
+
acknowledge: { [TaskStatus.Requested]: TaskStatus.Accepted, [TaskStatus.Received]: TaskStatus.Accepted },
|
|
173
|
+
start: { [TaskStatus.Accepted]: TaskStatus.InProgress, [TaskStatus.Ready]: TaskStatus.InProgress },
|
|
185
174
|
escalate: {
|
|
186
|
-
[
|
|
187
|
-
[
|
|
188
|
-
[
|
|
189
|
-
[
|
|
175
|
+
[TaskStatus.Requested]: TaskStatus.Requested,
|
|
176
|
+
[TaskStatus.Received]: TaskStatus.Requested,
|
|
177
|
+
[TaskStatus.Accepted]: TaskStatus.Requested,
|
|
178
|
+
[TaskStatus.InProgress]: TaskStatus.Requested,
|
|
190
179
|
},
|
|
191
180
|
reject: {
|
|
192
|
-
[
|
|
193
|
-
[
|
|
194
|
-
[
|
|
181
|
+
[TaskStatus.Requested]: TaskStatus.Rejected,
|
|
182
|
+
[TaskStatus.Received]: TaskStatus.Rejected,
|
|
183
|
+
[TaskStatus.Accepted]: TaskStatus.Rejected,
|
|
195
184
|
},
|
|
196
|
-
complete: { [
|
|
185
|
+
complete: { [TaskStatus.InProgress]: TaskStatus.Completed },
|
|
197
186
|
};
|
|
198
187
|
const nextStatus = transitions[action]?.[currentStatus];
|
|
199
188
|
if (!nextStatus)
|
|
@@ -15,6 +15,28 @@ export type AttachmentDocumentInput = Readonly<{
|
|
|
15
15
|
authorReferences?: readonly string[];
|
|
16
16
|
eventReferences?: readonly string[];
|
|
17
17
|
}>;
|
|
18
|
+
export type AppointmentCommunicationBundleInput = Readonly<{
|
|
19
|
+
communicationIdentifier: string;
|
|
20
|
+
status: string;
|
|
21
|
+
category?: string | readonly string[];
|
|
22
|
+
topic?: string;
|
|
23
|
+
text?: string;
|
|
24
|
+
subjectReference: string;
|
|
25
|
+
senderReference: string;
|
|
26
|
+
recipientReferences: readonly string[];
|
|
27
|
+
sent?: string;
|
|
28
|
+
appointment: FhirResource & Readonly<{
|
|
29
|
+
resourceType: 'Appointment';
|
|
30
|
+
id: string;
|
|
31
|
+
}>;
|
|
32
|
+
calendarDocument?: Readonly<{
|
|
33
|
+
identifier: string;
|
|
34
|
+
contentType: 'text/calendar';
|
|
35
|
+
dataBase64?: string;
|
|
36
|
+
url?: string;
|
|
37
|
+
title?: string;
|
|
38
|
+
}>;
|
|
39
|
+
}>;
|
|
18
40
|
/**
|
|
19
41
|
* SOS/Vet/UHC extension of the existing GDC BundleEditor.
|
|
20
42
|
*
|
|
@@ -30,6 +52,18 @@ export declare class CommunicationBundleEditor extends BundleEditor {
|
|
|
30
52
|
/** Builds the inherited claims-first outer batch after validating every attached Bundle. */
|
|
31
53
|
build(): ReturnType<BundleEditor['build']>;
|
|
32
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Authors the standard FHIR Appointment delivery flow for every sector.
|
|
57
|
+
*
|
|
58
|
+
* Product SDKs use this class directly. They must not introduce human or
|
|
59
|
+
* veterinary Appointment wrappers unless they add a genuinely sector-owned
|
|
60
|
+
* rule that is absent from the FHIR and shared data contracts.
|
|
61
|
+
*/
|
|
62
|
+
export declare class AppointmentCommunicationBundleEditor extends CommunicationBundleEditor {
|
|
63
|
+
addAppointmentCommunication(input: AppointmentCommunicationBundleInput): this;
|
|
64
|
+
}
|
|
65
|
+
/** Authors one standard Appointment delivery without a product-specific wrapper. */
|
|
66
|
+
export declare function prepareAppointmentCommunicationBundle(input: AppointmentCommunicationBundleInput): Record<string, unknown>;
|
|
33
67
|
/** High-level message editor extending the established attached-Bundle session. */
|
|
34
68
|
export declare class CommunicationMessageEditor extends CommunicationAttachedBundleSession {
|
|
35
69
|
#private;
|
|
@@ -15,7 +15,7 @@ import { BundleReader } from 'gdc-common-utils-ts/utils/bundle-reader';
|
|
|
15
15
|
import { CommunicationAttachedBundleSession, CommunicationClaimsContext, } from 'gdc-common-utils-ts/utils/communication-attached-bundle-session';
|
|
16
16
|
import { decodeAttachedBundleFromCommunicationClaims } from 'gdc-common-utils-ts/utils/communication-didcomm-payload';
|
|
17
17
|
import { CommunicationClaim } from 'gdc-common-utils-ts/models/interoperable-claims/communication-claims';
|
|
18
|
-
import { DocumentReferenceClaim } from 'fhir-data-utils-ts';
|
|
18
|
+
import { AppointmentClaim, DocumentReferenceClaim } from 'fhir-data-utils-ts';
|
|
19
19
|
/**
|
|
20
20
|
* SOS/Vet/UHC extension of the existing GDC BundleEditor.
|
|
21
21
|
*
|
|
@@ -62,6 +62,52 @@ _CommunicationBundleEditor_messages = new WeakMap(), _CommunicationBundleEditor_
|
|
|
62
62
|
entry.setClaim(claim, value);
|
|
63
63
|
entry.create();
|
|
64
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Authors the standard FHIR Appointment delivery flow for every sector.
|
|
67
|
+
*
|
|
68
|
+
* Product SDKs use this class directly. They must not introduce human or
|
|
69
|
+
* veterinary Appointment wrappers unless they add a genuinely sector-owned
|
|
70
|
+
* rule that is absent from the FHIR and shared data contracts.
|
|
71
|
+
*/
|
|
72
|
+
export class AppointmentCommunicationBundleEditor extends CommunicationBundleEditor {
|
|
73
|
+
addAppointmentCommunication(input) {
|
|
74
|
+
if (input.appointment.resourceType !== 'Appointment')
|
|
75
|
+
throw new TypeError('fhir_appointment_required');
|
|
76
|
+
const appointment = normalizeAppointmentIdentifier(input.appointment);
|
|
77
|
+
const message = this.newCommunication()
|
|
78
|
+
.setIdentifier(input.communicationIdentifier)
|
|
79
|
+
.setStatus(input.status)
|
|
80
|
+
.setSubject(input.subjectReference)
|
|
81
|
+
.setSender(input.senderReference)
|
|
82
|
+
.setRecipients(input.recipientReferences)
|
|
83
|
+
.setAttachmentTitle('appointment-bundle.json')
|
|
84
|
+
.setInnerBundleType('collection')
|
|
85
|
+
.addResource(appointment);
|
|
86
|
+
if (input.category)
|
|
87
|
+
message.setCommunicationCategory(input.category);
|
|
88
|
+
if (input.topic)
|
|
89
|
+
message.setCommunicationTopic(input.topic);
|
|
90
|
+
if (input.text)
|
|
91
|
+
message.setCommunicationText(input.text);
|
|
92
|
+
if (input.sent)
|
|
93
|
+
message.setSent(input.sent);
|
|
94
|
+
if (input.calendarDocument) {
|
|
95
|
+
message.addAttachmentDocument({
|
|
96
|
+
...input.calendarDocument,
|
|
97
|
+
subjectReference: input.subjectReference,
|
|
98
|
+
eventReferences: [`Appointment/${input.appointment.id}`],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
message.doneCommunication();
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Authors one standard Appointment delivery without a product-specific wrapper. */
|
|
106
|
+
export function prepareAppointmentCommunicationBundle(input) {
|
|
107
|
+
return new AppointmentCommunicationBundleEditor()
|
|
108
|
+
.addAppointmentCommunication(input)
|
|
109
|
+
.build();
|
|
110
|
+
}
|
|
65
111
|
/** High-level message editor extending the established attached-Bundle session. */
|
|
66
112
|
export class CommunicationMessageEditor extends CommunicationAttachedBundleSession {
|
|
67
113
|
constructor(parent) {
|
|
@@ -217,6 +263,23 @@ function claimsFromResource(resource) {
|
|
|
217
263
|
function resourceIdentifier(resource, claims) {
|
|
218
264
|
return requireText(resource.id || claims[`${resource.resourceType}.identifier`], 'resource_identifier_required');
|
|
219
265
|
}
|
|
266
|
+
function normalizeAppointmentIdentifier(appointment) {
|
|
267
|
+
const meta = appointment.meta;
|
|
268
|
+
const claims = meta?.claims && typeof meta.claims === 'object' && !Array.isArray(meta.claims)
|
|
269
|
+
? meta.claims
|
|
270
|
+
: undefined;
|
|
271
|
+
if (!claims)
|
|
272
|
+
throw new TypeError('claims_first_resource_required');
|
|
273
|
+
if (claims[AppointmentClaim.Identifier] !== undefined)
|
|
274
|
+
return appointment;
|
|
275
|
+
return {
|
|
276
|
+
...appointment,
|
|
277
|
+
meta: {
|
|
278
|
+
...meta,
|
|
279
|
+
claims: { ...claims, [AppointmentClaim.Identifier]: [appointment.id] },
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
220
283
|
function validateAttachedBundle(value) {
|
|
221
284
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
222
285
|
throw new TypeError('fhir_bundle_required');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EmergencyAuthorizationEvidence, EmergencyCapability
|
|
1
|
+
import type { EmergencyAuthorizationEvidence, EmergencyCapability } from 'sos-data-utils-ts/emergency-evidence';
|
|
2
2
|
export type { EmergencyAuthorizationEvidence, EmergencyCapability, EvidenceDomain, } from 'sos-data-utils-ts/emergency-evidence';
|
|
3
3
|
/** Runtime-neutral endpoint selected after authorization succeeds. */
|
|
4
4
|
export type EmergencyDeliveryTarget = Readonly<{
|
|
@@ -18,13 +18,3 @@ export declare function authorizeEmergencyAction(input: Readonly<{
|
|
|
18
18
|
evidence: readonly EmergencyAuthorizationEvidence[];
|
|
19
19
|
now?: string;
|
|
20
20
|
}>): readonly string[];
|
|
21
|
-
/** @deprecated Use `EmergencyDeliveryTarget`. */
|
|
22
|
-
export type SoschainEmergencyDeliveryTarget = EmergencyDeliveryTarget;
|
|
23
|
-
/** @deprecated Import `EmergencyAuthorizationEvidence` from this module. */
|
|
24
|
-
export type SoschainEmergencyAuthorizationEvidence = EmergencyAuthorizationEvidence;
|
|
25
|
-
/** @deprecated Import `EmergencyCapability` from this module. */
|
|
26
|
-
export type SoschainEmergencyCapability = EmergencyCapability;
|
|
27
|
-
/** @deprecated Import `EvidenceDomain` from this module. */
|
|
28
|
-
export type SoschainEvidenceDomain = EvidenceDomain;
|
|
29
|
-
/** @deprecated Use `authorizeEmergencyAction`. */
|
|
30
|
-
export declare const authorizeSoschainEmergencyAction: typeof authorizeEmergencyAction;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ClinicalCoding, type FhirResource } from 'fhir-data-utils-ts';
|
|
2
|
+
export type EncounterTransferSourceDisposition = 'returning' | 'not-returning';
|
|
3
|
+
export type EncounterTransferProjectionInput = Readonly<{
|
|
4
|
+
communicationIdentifier: string;
|
|
5
|
+
subjectReference: string;
|
|
6
|
+
/** Department Organization that performed and communicates the transfer. */
|
|
7
|
+
departmentSenderReference: string;
|
|
8
|
+
/** Owning subject-index provider, not the emergency contact recipient. */
|
|
9
|
+
indexRecipientReferences: readonly string[];
|
|
10
|
+
occurredAt: string;
|
|
11
|
+
sourceDisposition: EncounterTransferSourceDisposition;
|
|
12
|
+
sourceEncounter: FhirResource & Readonly<{
|
|
13
|
+
resourceType: 'Encounter';
|
|
14
|
+
id: string;
|
|
15
|
+
}>;
|
|
16
|
+
destinationEncounter: FhirResource & Readonly<{
|
|
17
|
+
resourceType: 'Encounter';
|
|
18
|
+
id: string;
|
|
19
|
+
}>;
|
|
20
|
+
sourceHistoryIdentifier: string;
|
|
21
|
+
destinationHistoryIdentifier: string;
|
|
22
|
+
encounterClass: ClinicalCoding;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Builds the completed Communication used to project an already-committed
|
|
26
|
+
* provider transfer to the subject index.
|
|
27
|
+
*
|
|
28
|
+
* This helper is not the provider transaction itself. The authorized provider
|
|
29
|
+
* first persists the two Encounter updates and two EncounterHistory milestones;
|
|
30
|
+
* only then may this Communication be marked `completed`. If the person will
|
|
31
|
+
* return, the source Encounter becomes `on-hold`; otherwise it becomes
|
|
32
|
+
* `completed`. `discharged` is reserved for the distinct clinical discharge
|
|
33
|
+
* stage and is never inferred merely from movement. The destination becomes
|
|
34
|
+
* `in-progress` on arrival.
|
|
35
|
+
*
|
|
36
|
+
* `Communication.sender` remains the department Organization. The authenticated
|
|
37
|
+
* professional/device is transport audit evidence and must not be substituted
|
|
38
|
+
* into the FHIR sender, author or attester fields.
|
|
39
|
+
*/
|
|
40
|
+
export declare function prepareEncounterTransferProjectionCommunication(input: EncounterTransferProjectionInput): Record<string, unknown>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { EncounterClaim, EncounterHistoryClaim, EncounterStatusR5, buildEncounterHistoryR5FlatEntry, } from 'fhir-data-utils-ts';
|
|
2
|
+
import { CommunicationBundleEditor } from './communication-bundle.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the completed Communication used to project an already-committed
|
|
5
|
+
* provider transfer to the subject index.
|
|
6
|
+
*
|
|
7
|
+
* This helper is not the provider transaction itself. The authorized provider
|
|
8
|
+
* first persists the two Encounter updates and two EncounterHistory milestones;
|
|
9
|
+
* only then may this Communication be marked `completed`. If the person will
|
|
10
|
+
* return, the source Encounter becomes `on-hold`; otherwise it becomes
|
|
11
|
+
* `completed`. `discharged` is reserved for the distinct clinical discharge
|
|
12
|
+
* stage and is never inferred merely from movement. The destination becomes
|
|
13
|
+
* `in-progress` on arrival.
|
|
14
|
+
*
|
|
15
|
+
* `Communication.sender` remains the department Organization. The authenticated
|
|
16
|
+
* professional/device is transport audit evidence and must not be substituted
|
|
17
|
+
* into the FHIR sender, author or attester fields.
|
|
18
|
+
*/
|
|
19
|
+
export function prepareEncounterTransferProjectionCommunication(input) {
|
|
20
|
+
requireDateTime(input.occurredAt);
|
|
21
|
+
const source = normalizeEncounter(input.sourceEncounter, input.subjectReference);
|
|
22
|
+
const destination = normalizeEncounter(input.destinationEncounter, input.subjectReference);
|
|
23
|
+
if (source.id === destination.id)
|
|
24
|
+
throw new TypeError('distinct_encounters_required');
|
|
25
|
+
const sourceStatus = input.sourceDisposition === 'returning'
|
|
26
|
+
? EncounterStatusR5.OnHold
|
|
27
|
+
: input.sourceDisposition === 'not-returning'
|
|
28
|
+
? EncounterStatusR5.Completed
|
|
29
|
+
: (() => { throw new TypeError('source_disposition_invalid'); })();
|
|
30
|
+
const sourceUpdated = withEncounterStatus(source, sourceStatus, input.occurredAt);
|
|
31
|
+
const destinationUpdated = withEncounterStatus(destination, EncounterStatusR5.InProgress, input.occurredAt);
|
|
32
|
+
const sourceHistory = historyResource({
|
|
33
|
+
identifier: input.sourceHistoryIdentifier,
|
|
34
|
+
encounter: sourceUpdated,
|
|
35
|
+
status: sourceStatus,
|
|
36
|
+
occurredAt: input.occurredAt,
|
|
37
|
+
encounterClass: input.encounterClass,
|
|
38
|
+
});
|
|
39
|
+
const destinationHistory = historyResource({
|
|
40
|
+
identifier: input.destinationHistoryIdentifier,
|
|
41
|
+
encounter: destinationUpdated,
|
|
42
|
+
status: EncounterStatusR5.InProgress,
|
|
43
|
+
occurredAt: input.occurredAt,
|
|
44
|
+
encounterClass: input.encounterClass,
|
|
45
|
+
});
|
|
46
|
+
const editor = new CommunicationBundleEditor();
|
|
47
|
+
editor.newCommunication()
|
|
48
|
+
.setIdentifier(input.communicationIdentifier)
|
|
49
|
+
.setStatus('completed')
|
|
50
|
+
.setSubject(input.subjectReference)
|
|
51
|
+
.setSender(input.departmentSenderReference)
|
|
52
|
+
.setRecipients(input.indexRecipientReferences)
|
|
53
|
+
.setSent(input.occurredAt)
|
|
54
|
+
.setAttachmentTitle('encounter-transfer-bundle.json')
|
|
55
|
+
.setInnerBundleType('batch')
|
|
56
|
+
.addResource(sourceUpdated, { method: 'PUT', url: `Encounter/${sourceUpdated.id}` })
|
|
57
|
+
.addResource(sourceHistory, { method: 'POST', url: 'EncounterHistory' })
|
|
58
|
+
.addResource(destinationUpdated, { method: 'PUT', url: `Encounter/${destinationUpdated.id}` })
|
|
59
|
+
.addResource(destinationHistory, { method: 'POST', url: 'EncounterHistory' })
|
|
60
|
+
.doneCommunication();
|
|
61
|
+
return editor.build();
|
|
62
|
+
}
|
|
63
|
+
function normalizeEncounter(resource, subjectReference) {
|
|
64
|
+
if (resource.resourceType !== 'Encounter')
|
|
65
|
+
throw new TypeError('fhir_encounter_required');
|
|
66
|
+
const claims = claimsOf(resource);
|
|
67
|
+
if (claims[EncounterClaim.Subject] !== subjectReference)
|
|
68
|
+
throw new TypeError('encounter_subject_mismatch');
|
|
69
|
+
if (!text(claims[EncounterClaim.ServiceType]))
|
|
70
|
+
throw new TypeError('encounter_service_required');
|
|
71
|
+
return clone(resource);
|
|
72
|
+
}
|
|
73
|
+
function withEncounterStatus(resource, status, occurredAt) {
|
|
74
|
+
const claims = claimsOf(resource);
|
|
75
|
+
const nextClaims = {
|
|
76
|
+
...claims,
|
|
77
|
+
[EncounterClaim.Status]: status,
|
|
78
|
+
...(status === EncounterStatusR5.Completed ? { [EncounterClaim.ActualPeriodEnd]: occurredAt } : {}),
|
|
79
|
+
...(status === EncounterStatusR5.InProgress && !claims[EncounterClaim.ActualPeriodStart]
|
|
80
|
+
? { [EncounterClaim.ActualPeriodStart]: occurredAt }
|
|
81
|
+
: {}),
|
|
82
|
+
};
|
|
83
|
+
return { ...clone(resource), meta: { ...resource.meta, claims: nextClaims } };
|
|
84
|
+
}
|
|
85
|
+
function historyResource(input) {
|
|
86
|
+
const encounterClaims = claimsOf(input.encounter);
|
|
87
|
+
const id = uuidIdentifier(input.identifier);
|
|
88
|
+
const entry = buildEncounterHistoryR5FlatEntry({
|
|
89
|
+
entryId: id,
|
|
90
|
+
identifier: input.identifier,
|
|
91
|
+
encounterReference: `Encounter/${input.encounter.id}`,
|
|
92
|
+
status: input.status,
|
|
93
|
+
class: input.encounterClass,
|
|
94
|
+
serviceReference: text(encounterClaims[EncounterClaim.ServiceType]),
|
|
95
|
+
subjectReference: text(encounterClaims[EncounterClaim.Subject]),
|
|
96
|
+
actualPeriodStart: input.occurredAt,
|
|
97
|
+
locationReference: text(encounterClaims[EncounterClaim.Location]),
|
|
98
|
+
});
|
|
99
|
+
if (entry.claims[EncounterHistoryClaim.Status] !== input.status)
|
|
100
|
+
throw new TypeError('encounter_history_status_mismatch');
|
|
101
|
+
return { resourceType: 'EncounterHistory', id, meta: { claims: entry.claims } };
|
|
102
|
+
}
|
|
103
|
+
function claimsOf(resource) {
|
|
104
|
+
const meta = resource.meta;
|
|
105
|
+
if (!meta?.claims || typeof meta.claims !== 'object' || Array.isArray(meta.claims))
|
|
106
|
+
throw new TypeError('claims_first_resource_required');
|
|
107
|
+
return clone(meta.claims);
|
|
108
|
+
}
|
|
109
|
+
function uuidIdentifier(value) {
|
|
110
|
+
const normalized = text(value);
|
|
111
|
+
if (!normalized?.startsWith('urn:uuid:') || normalized.length <= 'urn:uuid:'.length)
|
|
112
|
+
throw new TypeError('encounter_history_identifier_invalid');
|
|
113
|
+
return normalized.slice('urn:uuid:'.length);
|
|
114
|
+
}
|
|
115
|
+
function requireDateTime(value) {
|
|
116
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(value) || Number.isNaN(Date.parse(value)))
|
|
117
|
+
throw new TypeError('transfer_occurred_at_invalid');
|
|
118
|
+
}
|
|
119
|
+
function text(value) { return typeof value === 'string' && value.trim() ? value.trim() : undefined; }
|
|
120
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)); }
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const FHIR_SCHEDULING_REQUEST_TIMEOUT_MS = 3000;
|
|
2
|
+
export type FhirSchedulingResourceType = 'Appointment' | 'AppointmentResponse' | 'Schedule' | 'Slot' | 'Subscription';
|
|
3
|
+
export type FhirSchedulingResourceCapability = Readonly<{
|
|
4
|
+
resourceType: FhirSchedulingResourceType;
|
|
5
|
+
supported: boolean;
|
|
6
|
+
interactions: readonly string[];
|
|
7
|
+
}>;
|
|
8
|
+
export type FhirSchedulingCapabilitySummary = Readonly<{
|
|
9
|
+
fhirVersion: string;
|
|
10
|
+
softwareName?: string;
|
|
11
|
+
resources: readonly FhirSchedulingResourceCapability[];
|
|
12
|
+
}>;
|
|
13
|
+
export type FhirAccessTokenProvider = Readonly<{
|
|
14
|
+
getAccessToken(): Promise<string>;
|
|
15
|
+
}>;
|
|
16
|
+
export type FhirFetchResponse = Readonly<{
|
|
17
|
+
ok: boolean;
|
|
18
|
+
status: number;
|
|
19
|
+
text(): Promise<string>;
|
|
20
|
+
}>;
|
|
21
|
+
export type FhirFetch = (url: string, init?: Readonly<Record<string, unknown>>) => Promise<FhirFetchResponse>;
|
|
22
|
+
export type FhirSchedulingClientOptions = Readonly<{
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
fetchFn?: FhirFetch;
|
|
25
|
+
accessTokenProvider?: FhirAccessTokenProvider;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
accept?: string;
|
|
28
|
+
}>;
|
|
29
|
+
/** Capability-driven FHIR scheduling client shared by every sector. */
|
|
30
|
+
export declare class FhirSchedulingClient {
|
|
31
|
+
#private;
|
|
32
|
+
private readonly options;
|
|
33
|
+
readonly baseUrl: string;
|
|
34
|
+
constructor(options: FhirSchedulingClientOptions);
|
|
35
|
+
discoverCapabilities(): Promise<FhirSchedulingCapabilitySummary>;
|
|
36
|
+
searchAppointments(parameters: Readonly<Record<string, string>>): Promise<Record<string, unknown>>;
|
|
37
|
+
searchSlots(parameters: Readonly<Record<string, string>>): Promise<Record<string, unknown>>;
|
|
38
|
+
createAppointment(appointment: Readonly<Record<string, unknown>>): Promise<Record<string, unknown>>;
|
|
39
|
+
createAppointmentResponse(response: Readonly<Record<string, unknown>>): Promise<Record<string, unknown>>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _FhirSchedulingClient_instances, _FhirSchedulingClient_fetchFn, _FhirSchedulingClient_timeoutMs, _FhirSchedulingClient_capability, _FhirSchedulingClient_search, _FhirSchedulingClient_requireInteraction, _FhirSchedulingClient_request;
|
|
13
|
+
export const FHIR_SCHEDULING_REQUEST_TIMEOUT_MS = 3000;
|
|
14
|
+
const schedulingResourceTypes = Object.freeze([
|
|
15
|
+
'Appointment',
|
|
16
|
+
'AppointmentResponse',
|
|
17
|
+
'Schedule',
|
|
18
|
+
'Slot',
|
|
19
|
+
'Subscription',
|
|
20
|
+
]);
|
|
21
|
+
/** Capability-driven FHIR scheduling client shared by every sector. */
|
|
22
|
+
export class FhirSchedulingClient {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
_FhirSchedulingClient_instances.add(this);
|
|
25
|
+
this.options = options;
|
|
26
|
+
_FhirSchedulingClient_fetchFn.set(this, void 0);
|
|
27
|
+
_FhirSchedulingClient_timeoutMs.set(this, void 0);
|
|
28
|
+
_FhirSchedulingClient_capability.set(this, void 0);
|
|
29
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
30
|
+
__classPrivateFieldSet(this, _FhirSchedulingClient_fetchFn, options.fetchFn ?? globalThis.fetch, "f");
|
|
31
|
+
__classPrivateFieldSet(this, _FhirSchedulingClient_timeoutMs, requireSchedulingTimeout(options.timeoutMs), "f");
|
|
32
|
+
}
|
|
33
|
+
async discoverCapabilities() {
|
|
34
|
+
const metadata = await __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_request).call(this, 'metadata');
|
|
35
|
+
if (metadata.resourceType !== 'CapabilityStatement') {
|
|
36
|
+
throw new TypeError('fhir_capability_statement_required');
|
|
37
|
+
}
|
|
38
|
+
const resources = array(metadata.rest).flatMap(rest => array(record(rest).resource));
|
|
39
|
+
__classPrivateFieldSet(this, _FhirSchedulingClient_capability, Object.freeze({
|
|
40
|
+
fhirVersion: text(metadata.fhirVersion),
|
|
41
|
+
...(optionalText(record(metadata.software).name)
|
|
42
|
+
? { softwareName: optionalText(record(metadata.software).name) }
|
|
43
|
+
: {}),
|
|
44
|
+
resources: Object.freeze(schedulingResourceTypes.map(resourceType => {
|
|
45
|
+
const resource = resources.map(record).find(candidate => candidate.type === resourceType);
|
|
46
|
+
return Object.freeze({
|
|
47
|
+
resourceType,
|
|
48
|
+
supported: Boolean(resource),
|
|
49
|
+
interactions: Object.freeze(array(resource?.interaction).map(item => text(record(item).code))),
|
|
50
|
+
});
|
|
51
|
+
})),
|
|
52
|
+
}), "f");
|
|
53
|
+
return __classPrivateFieldGet(this, _FhirSchedulingClient_capability, "f");
|
|
54
|
+
}
|
|
55
|
+
async searchAppointments(parameters) {
|
|
56
|
+
await __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_requireInteraction).call(this, 'Appointment', 'search-type');
|
|
57
|
+
return __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_search).call(this, 'Appointment', parameters);
|
|
58
|
+
}
|
|
59
|
+
async searchSlots(parameters) {
|
|
60
|
+
await __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_requireInteraction).call(this, 'Slot', 'search-type');
|
|
61
|
+
return __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_search).call(this, 'Slot', parameters);
|
|
62
|
+
}
|
|
63
|
+
async createAppointment(appointment) {
|
|
64
|
+
await __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_requireInteraction).call(this, 'Appointment', 'create');
|
|
65
|
+
return __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_request).call(this, 'Appointment', { method: 'POST', body: JSON.stringify(appointment) });
|
|
66
|
+
}
|
|
67
|
+
async createAppointmentResponse(response) {
|
|
68
|
+
await __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_requireInteraction).call(this, 'AppointmentResponse', 'create');
|
|
69
|
+
return __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_request).call(this, 'AppointmentResponse', { method: 'POST', body: JSON.stringify(response) });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
_FhirSchedulingClient_fetchFn = new WeakMap(), _FhirSchedulingClient_timeoutMs = new WeakMap(), _FhirSchedulingClient_capability = new WeakMap(), _FhirSchedulingClient_instances = new WeakSet(), _FhirSchedulingClient_search = async function _FhirSchedulingClient_search(resourceType, parameters) {
|
|
73
|
+
return __classPrivateFieldGet(this, _FhirSchedulingClient_instances, "m", _FhirSchedulingClient_request).call(this, `${resourceType}?${new URLSearchParams(parameters)}`);
|
|
74
|
+
}, _FhirSchedulingClient_requireInteraction = async function _FhirSchedulingClient_requireInteraction(resourceType, interaction) {
|
|
75
|
+
const capability = __classPrivateFieldGet(this, _FhirSchedulingClient_capability, "f") ?? await this.discoverCapabilities();
|
|
76
|
+
const resource = capability.resources.find(candidate => candidate.resourceType === resourceType);
|
|
77
|
+
if (!resource?.supported || !resource.interactions.includes(interaction)) {
|
|
78
|
+
throw new TypeError(`Configured FHIR server does not advertise ${resourceType}.${interaction}.`);
|
|
79
|
+
}
|
|
80
|
+
}, _FhirSchedulingClient_request = async function _FhirSchedulingClient_request(path, init = {}) {
|
|
81
|
+
const token = this.options.accessTokenProvider
|
|
82
|
+
? await this.options.accessTokenProvider.getAccessToken()
|
|
83
|
+
: undefined;
|
|
84
|
+
const response = await __classPrivateFieldGet(this, _FhirSchedulingClient_fetchFn, "f").call(this, `${this.baseUrl}/${path}`, {
|
|
85
|
+
...init,
|
|
86
|
+
signal: AbortSignal.timeout(__classPrivateFieldGet(this, _FhirSchedulingClient_timeoutMs, "f")),
|
|
87
|
+
headers: {
|
|
88
|
+
accept: this.options.accept ?? 'application/fhir+json',
|
|
89
|
+
...(init.body ? { 'content-type': 'application/fhir+json' } : {}),
|
|
90
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const responseText = await response.text();
|
|
94
|
+
const body = parseResponseBody(responseText);
|
|
95
|
+
if (!response.ok)
|
|
96
|
+
throw new Error(`FHIR scheduling request failed (${response.status}).`);
|
|
97
|
+
return record(body);
|
|
98
|
+
};
|
|
99
|
+
function normalizeBaseUrl(value) {
|
|
100
|
+
const baseUrl = String(value ?? '').trim().replace(/\/+$/u, '');
|
|
101
|
+
if (!baseUrl)
|
|
102
|
+
throw new TypeError('fhir_base_url_required');
|
|
103
|
+
return baseUrl;
|
|
104
|
+
}
|
|
105
|
+
function requireSchedulingTimeout(value) {
|
|
106
|
+
const timeoutMs = value ?? FHIR_SCHEDULING_REQUEST_TIMEOUT_MS;
|
|
107
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > FHIR_SCHEDULING_REQUEST_TIMEOUT_MS) {
|
|
108
|
+
throw new RangeError(`FHIR scheduling timeout must be between 1 and ${FHIR_SCHEDULING_REQUEST_TIMEOUT_MS} ms.`);
|
|
109
|
+
}
|
|
110
|
+
return timeoutMs;
|
|
111
|
+
}
|
|
112
|
+
function parseResponseBody(value) {
|
|
113
|
+
if (!value)
|
|
114
|
+
return {};
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(value);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { text: value };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function record(value) {
|
|
123
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
124
|
+
}
|
|
125
|
+
function array(value) { return Array.isArray(value) ? value : []; }
|
|
126
|
+
function optionalText(value) { return typeof value === 'string' && value.trim() ? value.trim() : undefined; }
|
|
127
|
+
function text(value) { return optionalText(value) ?? ''; }
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** Shared runtime-neutral orchestration remains independent from product SDKs. */
|
|
2
|
-
export { EmergencyCapabilities, EvidenceDomains, EvidenceRelationships,
|
|
3
|
-
export type { EvidenceRelationship,
|
|
4
|
-
export * from './emergency-group.js';
|
|
2
|
+
export { EmergencyCapabilities, EvidenceDomains, EvidenceRelationships, } from 'sos-data-utils-ts/emergency-evidence';
|
|
3
|
+
export type { EvidenceRelationship, } from 'sos-data-utils-ts/emergency-evidence';
|
|
5
4
|
export * from './emergency-authorization.js';
|
|
6
5
|
export * from './care-emergency-alert-orchestration.js';
|
|
7
|
-
export * from './place-card-association.js';
|
|
8
6
|
export * from './secure-message-crypto.js';
|
|
9
7
|
export * from './equipment-compliance-orchestration.js';
|
|
10
8
|
export * from './communication-bundle.js';
|
|
9
|
+
export * from './fhir-scheduling.js';
|
|
10
|
+
export * from './bundle-changes-response.js';
|
|
11
|
+
export * from './encounter-transfer-communication.js';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Shared runtime-neutral orchestration remains independent from product SDKs. */
|
|
2
|
-
export { EmergencyCapabilities, EvidenceDomains, EvidenceRelationships,
|
|
3
|
-
export * from './emergency-group.js';
|
|
2
|
+
export { EmergencyCapabilities, EvidenceDomains, EvidenceRelationships, } from 'sos-data-utils-ts/emergency-evidence';
|
|
4
3
|
export * from './emergency-authorization.js';
|
|
5
4
|
export * from './care-emergency-alert-orchestration.js';
|
|
6
|
-
export * from './place-card-association.js';
|
|
7
5
|
export * from './secure-message-crypto.js';
|
|
8
6
|
export * from './equipment-compliance-orchestration.js';
|
|
9
7
|
export * from './communication-bundle.js';
|
|
8
|
+
export * from './fhir-scheduling.js';
|
|
9
|
+
export * from './bundle-changes-response.js';
|
|
10
|
+
export * from './encounter-transfer-communication.js';
|
|
@@ -24,26 +24,23 @@ unordered group such as an Appointment and its calendar attachment.
|
|
|
24
24
|
## Author
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
|
-
import {
|
|
27
|
+
import { AppointmentCommunicationBundleEditor } from 'sos-sdk-core-ts'
|
|
28
28
|
|
|
29
|
-
const outerBatch = new
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.setSubject(subjectReference)
|
|
34
|
-
.setSender(departmentOrganizationReference)
|
|
35
|
-
.setRecipients(recipientReferences)
|
|
36
|
-
.setInnerBundleType('collection')
|
|
37
|
-
.addResource(appointment)
|
|
38
|
-
.addAttachmentDocument({
|
|
39
|
-
identifier: calendarDocumentIdentifier,
|
|
29
|
+
const outerBatch = new AppointmentCommunicationBundleEditor()
|
|
30
|
+
.addAppointmentCommunication({
|
|
31
|
+
communicationIdentifier,
|
|
32
|
+
status: 'completed',
|
|
40
33
|
subjectReference,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
senderReference: departmentOrganizationReference,
|
|
35
|
+
recipientReferences,
|
|
36
|
+
appointment,
|
|
37
|
+
calendarDocument: {
|
|
38
|
+
identifier: calendarDocumentIdentifier,
|
|
39
|
+
contentType: 'text/calendar',
|
|
40
|
+
dataBase64: calendarDataBase64,
|
|
41
|
+
title: 'appointment.ics',
|
|
42
|
+
},
|
|
45
43
|
})
|
|
46
|
-
.doneCommunication()
|
|
47
44
|
.build()
|
|
48
45
|
```
|
|
49
46
|
|
|
@@ -82,3 +79,7 @@ outer resource that is not a batch Bundle.
|
|
|
82
79
|
- The SOS editor subclasses the established GDC Bundle editor/reader and the
|
|
83
80
|
attached-Bundle session. A future antifraud-common implementation can replace
|
|
84
81
|
that base behind this shared SOS API without migrating Vet and UHC separately.
|
|
82
|
+
- `AppointmentCommunicationBundleEditor` and `FhirSchedulingClient` are named
|
|
83
|
+
after their FHIR responsibilities. Product packages must not introduce
|
|
84
|
+
`HumanAppointment`, `VeterinaryAppointment`, `HumanCommunication` or similar
|
|
85
|
+
wrappers for standard resources.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Encounter transfer projection
|
|
2
|
+
|
|
3
|
+
The care provider owns the authoritative movement transaction. When a person
|
|
4
|
+
leaves one operational service and arrives at another, the provider persists:
|
|
5
|
+
|
|
6
|
+
1. the source `Encounter` as `on-hold` when the person will return, or
|
|
7
|
+
`completed` when the departure is definitive;
|
|
8
|
+
2. one source `EncounterHistory` milestone with the same state;
|
|
9
|
+
3. the destination `Encounter` as `in-progress` on confirmed arrival; and
|
|
10
|
+
4. one destination `EncounterHistory` milestone.
|
|
11
|
+
|
|
12
|
+
`discharged` is not a movement synonym. It is reserved for the separate
|
|
13
|
+
clinical discharge stage. A diagnostic test, operation, ward move or ICU
|
|
14
|
+
admission therefore does not automatically discharge the source encounter.
|
|
15
|
+
|
|
16
|
+
After the provider has committed those four records, it can call
|
|
17
|
+
`prepareEncounterTransferProjectionCommunication(...)`. The helper produces a
|
|
18
|
+
completed `Communication` whose inner batch Bundle projects the same four
|
|
19
|
+
changes to the subject-index provider. The Communication is continuity and
|
|
20
|
+
discovery evidence; it is not a substitute for the provider transaction.
|
|
21
|
+
|
|
22
|
+
The department Organization is `Communication.sender`. Each Encounter keeps an
|
|
23
|
+
explicit `HealthcareService`. The authenticated professional/device remains
|
|
24
|
+
separate audit evidence and must not be copied into sender, document author or
|
|
25
|
+
attester fields.
|
|
26
|
+
|
|
27
|
+
Emergency-contact delivery follows the existing Communication Bundle rules:
|
|
28
|
+
|
|
29
|
+
- written text may be Communication content;
|
|
30
|
+
- audio, PDF and calendar files are `DocumentReference` entries;
|
|
31
|
+
- an arbitrary FHIR payload must be a validated inner Bundle;
|
|
32
|
+
- a live voice or video call is not an `Appointment`;
|
|
33
|
+
- a scheduled conversation may carry `Appointment`, later
|
|
34
|
+
`AppointmentResponse`, and an optional `.ics` DocumentReference.
|
|
35
|
+
|
|
36
|
+
Portals should explain the outcome in plain language first. FHIR resource names
|
|
37
|
+
belong in optional technical details, SDK documentation and professional
|
|
38
|
+
support material—not in the primary user instruction.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# FHIR scheduling interoperability
|
|
2
|
+
|
|
3
|
+
`sos-sdk-core-ts/fhir-scheduling` is the product-neutral scheduling boundary
|
|
4
|
+
shared by SOS, Vet and UHC channels. It discovers the remote
|
|
5
|
+
`CapabilityStatement` before using `Schedule`, `Slot` or `Appointment` and
|
|
6
|
+
fails closed when the provider does not advertise the required interaction.
|
|
7
|
+
|
|
8
|
+
Portal, telephone, chatbot and other channel adapters call the same client.
|
|
9
|
+
They must not introduce human- or animal-prefixed Appointment types. Sector
|
|
10
|
+
policy decides who may search or write; the FHIR resource contract stays the
|
|
11
|
+
same.
|
|
12
|
+
|
|
13
|
+
An appointment message is authored with
|
|
14
|
+
`AppointmentCommunicationBundleEditor`. The outer transport is a Bundle of
|
|
15
|
+
FHIR `Communication` resources. Each Communication carries one inner document,
|
|
16
|
+
batch or collection Bundle; an optional calendar file is a `DocumentReference`
|
|
17
|
+
inside that inner Bundle, never a bare `text/calendar` Communication payload.
|
|
18
|
+
|
|
19
|
+
`analyzeBundleChangesResponse(changesBundle, responseBundle)` correlates a GW
|
|
20
|
+
response by stable identifiers. Missing, malformed or ambiguous responses stay
|
|
21
|
+
pending until the caller performs an authoritative resource search; array
|
|
22
|
+
position is never treated as identity.
|
|
23
|
+
|
|
24
|
+
Executable contracts:
|
|
25
|
+
|
|
26
|
+
- `tests/fhir-scheduling.test.mjs`
|
|
27
|
+
- `tests/communication-bundle.test.mjs`
|
|
28
|
+
- `tests/bundle-changes-response.test.mjs`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sos-sdk-core-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Runtime-neutral shared orchestration for SOS, Vet and UHC products",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Connecting Solution & Applications Ltd",
|
|
@@ -16,10 +16,6 @@
|
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"default": "./dist/index.js"
|
|
18
18
|
},
|
|
19
|
-
"./emergency-group": {
|
|
20
|
-
"types": "./dist/emergency-group.d.ts",
|
|
21
|
-
"default": "./dist/emergency-group.js"
|
|
22
|
-
},
|
|
23
19
|
"./care-emergency-alert-orchestration": {
|
|
24
20
|
"types": "./dist/care-emergency-alert-orchestration.d.ts",
|
|
25
21
|
"default": "./dist/care-emergency-alert-orchestration.js"
|
|
@@ -28,10 +24,6 @@
|
|
|
28
24
|
"types": "./dist/emergency-authorization.d.ts",
|
|
29
25
|
"default": "./dist/emergency-authorization.js"
|
|
30
26
|
},
|
|
31
|
-
"./place-card-association": {
|
|
32
|
-
"types": "./dist/place-card-association.d.ts",
|
|
33
|
-
"default": "./dist/place-card-association.js"
|
|
34
|
-
},
|
|
35
27
|
"./secure-message-crypto": {
|
|
36
28
|
"types": "./dist/secure-message-crypto.d.ts",
|
|
37
29
|
"default": "./dist/secure-message-crypto.js"
|
|
@@ -43,6 +35,18 @@
|
|
|
43
35
|
"./communication-bundle": {
|
|
44
36
|
"types": "./dist/communication-bundle.d.ts",
|
|
45
37
|
"default": "./dist/communication-bundle.js"
|
|
38
|
+
},
|
|
39
|
+
"./fhir-scheduling": {
|
|
40
|
+
"types": "./dist/fhir-scheduling.d.ts",
|
|
41
|
+
"default": "./dist/fhir-scheduling.js"
|
|
42
|
+
},
|
|
43
|
+
"./bundle-changes-response": {
|
|
44
|
+
"types": "./dist/bundle-changes-response.d.ts",
|
|
45
|
+
"default": "./dist/bundle-changes-response.js"
|
|
46
|
+
},
|
|
47
|
+
"./encounter-transfer-communication": {
|
|
48
|
+
"types": "./dist/encounter-transfer-communication.d.ts",
|
|
49
|
+
"default": "./dist/encounter-transfer-communication.js"
|
|
46
50
|
}
|
|
47
51
|
},
|
|
48
52
|
"files": [
|
|
@@ -63,9 +67,9 @@
|
|
|
63
67
|
"@noble/ciphers": "2.4.0",
|
|
64
68
|
"@noble/hashes": "2.4.0",
|
|
65
69
|
"@noble/post-quantum": "0.7.1",
|
|
66
|
-
"fhir-data-utils-ts": "0.6.
|
|
70
|
+
"fhir-data-utils-ts": "0.6.1",
|
|
67
71
|
"gdc-common-utils-ts": "2.9.21",
|
|
68
|
-
"sos-data-utils-ts": "0.
|
|
72
|
+
"sos-data-utils-ts": "0.6.0"
|
|
69
73
|
},
|
|
70
74
|
"devDependencies": {
|
|
71
75
|
"@types/node": "^22.5.0",
|
package/dist/emergency-group.js
DELETED