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 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
- The older `SoschainEmergency*` names remain deprecated compatibility aliases
76
- to the same types and implementation; they are not a second contract.
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(EmergencyTaskStatus).includes(status)) {
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 !== EmergencyTaskStatus.Accepted && task.status !== EmergencyTaskStatus.InProgress) {
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: { [EmergencyTaskStatus.Requested]: EmergencyTaskStatus.Accepted, [EmergencyTaskStatus.Received]: EmergencyTaskStatus.Accepted },
184
- start: { [EmergencyTaskStatus.Accepted]: EmergencyTaskStatus.InProgress, [EmergencyTaskStatus.Ready]: EmergencyTaskStatus.InProgress },
172
+ acknowledge: { [TaskStatus.Requested]: TaskStatus.Accepted, [TaskStatus.Received]: TaskStatus.Accepted },
173
+ start: { [TaskStatus.Accepted]: TaskStatus.InProgress, [TaskStatus.Ready]: TaskStatus.InProgress },
185
174
  escalate: {
186
- [EmergencyTaskStatus.Requested]: EmergencyTaskStatus.Requested,
187
- [EmergencyTaskStatus.Received]: EmergencyTaskStatus.Requested,
188
- [EmergencyTaskStatus.Accepted]: EmergencyTaskStatus.Requested,
189
- [EmergencyTaskStatus.InProgress]: EmergencyTaskStatus.Requested,
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
- [EmergencyTaskStatus.Requested]: EmergencyTaskStatus.Rejected,
193
- [EmergencyTaskStatus.Received]: EmergencyTaskStatus.Rejected,
194
- [EmergencyTaskStatus.Accepted]: EmergencyTaskStatus.Rejected,
181
+ [TaskStatus.Requested]: TaskStatus.Rejected,
182
+ [TaskStatus.Received]: TaskStatus.Rejected,
183
+ [TaskStatus.Accepted]: TaskStatus.Rejected,
195
184
  },
196
- complete: { [EmergencyTaskStatus.InProgress]: EmergencyTaskStatus.Completed },
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, EvidenceDomain } from 'sos-data-utils-ts/emergency-evidence';
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;
@@ -17,5 +17,3 @@ export function authorizeEmergencyAction(input) {
17
17
  }
18
18
  return [...evidenceIds];
19
19
  }
20
- /** @deprecated Use `authorizeEmergencyAction`. */
21
- export const authorizeSoschainEmergencyAction = 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, SoschainEmergencyCapabilities, SoschainEvidenceDomains, SoschainEvidenceRelationships, } from 'sos-data-utils-ts/emergency-evidence';
3
- export type { EvidenceRelationship, SoschainEvidenceRelationship, } from 'sos-data-utils-ts/emergency-evidence';
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, SoschainEmergencyCapabilities, SoschainEvidenceDomains, SoschainEvidenceRelationships, } from 'sos-data-utils-ts/emergency-evidence';
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 { CommunicationBundleEditor } from 'sos-sdk-core-ts'
27
+ import { AppointmentCommunicationBundleEditor } from 'sos-sdk-core-ts'
28
28
 
29
- const outerBatch = new CommunicationBundleEditor()
30
- .newCommunication()
31
- .setIdentifier(communicationIdentifier)
32
- .setStatus('completed')
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
- contentType: 'text/calendar',
42
- dataBase64: calendarDataBase64,
43
- title: 'appointment.ics',
44
- eventReferences: [appointmentReference],
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.4.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.0",
70
+ "fhir-data-utils-ts": "0.6.1",
67
71
  "gdc-common-utils-ts": "2.9.21",
68
- "sos-data-utils-ts": "0.5.1"
72
+ "sos-data-utils-ts": "0.6.0"
69
73
  },
70
74
  "devDependencies": {
71
75
  "@types/node": "^22.5.0",
@@ -1,2 +0,0 @@
1
- /** Compatibility subpath for the canonical shared emergency record contracts. */
2
- export * from 'sos-data-utils-ts/emergency-group';
@@ -1,2 +0,0 @@
1
- /** Compatibility subpath for the canonical shared emergency record contracts. */
2
- export * from 'sos-data-utils-ts/emergency-group';
@@ -1,2 +0,0 @@
1
- /** Compatibility subpath for the canonical shared card-target contracts. */
2
- export * from 'sos-data-utils-ts/place-card-association';
@@ -1,2 +0,0 @@
1
- /** Compatibility subpath for the canonical shared card-target contracts. */
2
- export * from 'sos-data-utils-ts/place-card-association';