vet-data-utils-ts 0.5.37 → 0.7.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/README.md +33 -9
- package/dist/place-service-directory.d.ts +75 -1
- package/dist/place-service-directory.js +95 -2
- package/dist/scheduling.d.ts +17 -11
- package/dist/scheduling.js +49 -42
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -23,15 +23,20 @@ and fear context as a claims-first social-history Observation. It deliberately
|
|
|
23
23
|
uses narrative text rather than inventing a veterinary diagnosis code.
|
|
24
24
|
|
|
25
25
|
The product-local `place-service-directory` export models persistent public
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`HealthcareService
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
departments as claims-only Schema.org `Organization` resources, their sites as
|
|
27
|
+
`Place`, and their offerings as `Service` or `Product`. A department has one
|
|
28
|
+
parent legal organization, one or more Places and several offered Services.
|
|
29
|
+
Each offered Service projects through
|
|
30
|
+
`projectServiceToFhirR5HealthcareService(...)` to its own stable FHIR
|
|
31
|
+
`HealthcareService`; the department remains the provider Organization and
|
|
32
|
+
communication sender. The old aggregate department projection fails closed. A care organization
|
|
33
|
+
publishes services; an insurance organization publishes products in a
|
|
34
|
+
Schema.org `OfferCatalog`. Logos, images, accessibility, coordinates, address
|
|
35
|
+
and opening-hours specifications remain neutral indexed claims. Native FHIR R5
|
|
36
|
+
`Organization`, `Location`, `HealthcareService` and `InsurancePlan` are
|
|
37
|
+
produced only by explicit projection helpers. Member-specific FHIR `Coverage`
|
|
38
|
+
is never a public catalog entry. The shared `gdc-*` claim catalog remains
|
|
39
|
+
unchanged while this profile is validated in VetChain.
|
|
35
40
|
|
|
36
41
|
The customer-facing Schema.org `OfferCatalog` is distinct from the Eclipse
|
|
37
42
|
Dataspace Protocol catalog. `buildDataspaceCatalogAdvertisement(...)` exposes a
|
|
@@ -122,6 +127,25 @@ and applies weekly or monthly-ordinal exceptions that restrict or extend one or
|
|
|
122
127
|
more consultation Locations. It converts clinic-local times through an IANA
|
|
123
128
|
time zone, including daylight-saving changes.
|
|
124
129
|
|
|
130
|
+
Every Schedule identifies exactly one HealthcareService and the resources that
|
|
131
|
+
must be available together. A child Location is the independently reservable
|
|
132
|
+
capacity unit; `PractitionerRole` and `Device` actors are optional and included
|
|
133
|
+
only when that professional or particular machine constrains capacity. Slot duration comes only from its start/end instants.
|
|
134
|
+
Schedule and Slot business identity is independent from those mutable actors.
|
|
135
|
+
Callers supply one canonical service-owned Schedule URN for each reservable
|
|
136
|
+
Location. `Schedule.identifier` preserves that URN and `Schedule.id` is its
|
|
137
|
+
SHA3-256 base58btc multihash. `Slot.identifier` appends one
|
|
138
|
+
`<startEpochSeconds>-<endEpochSeconds>` interval segment and `Slot.id` is
|
|
139
|
+
derived the same way. Do not concatenate practitioner, Location, Device or DID
|
|
140
|
+
values into either identity.
|
|
141
|
+
Reservable rooms, booths, chairs and beds are child Locations; machines remain
|
|
142
|
+
Devices. The neutral builders and canonical Schedule/Slot claim catalogs come
|
|
143
|
+
from `fhir-data-utils-ts` rather than being duplicated here.
|
|
144
|
+
|
|
145
|
+
An equipment failure is not itself an appointment cancellation. The equipment
|
|
146
|
+
record captures the fault or availability change; scheduling separately marks
|
|
147
|
+
only the affected future Slots unavailable and reconciles their appointments.
|
|
148
|
+
|
|
125
149
|
The official searchable facts remain resource-qualified flat claims. The
|
|
126
150
|
exported `VeterinarySlotSearchParameters` catalogue identifies the supported
|
|
127
151
|
FHIR R5 Slot query codes; generated resources include `Slot.identifier`,
|
|
@@ -21,6 +21,19 @@ export declare enum ClaimsPlaceSchemaorg {
|
|
|
21
21
|
specialOpeningHoursSpecification = "org.schema.Place.specialOpeningHoursSpecification",
|
|
22
22
|
userSelected = "Place.user-selected"
|
|
23
23
|
}
|
|
24
|
+
/** Schema.org flat claims for a public department/subOrganization. */
|
|
25
|
+
export declare enum ClaimsDirectoryOrganizationSchemaorg {
|
|
26
|
+
identifier = "org.schema.Organization.identifier",
|
|
27
|
+
name = "org.schema.Organization.name",
|
|
28
|
+
description = "org.schema.Organization.description",
|
|
29
|
+
parentOrganizationIdentifier = "org.schema.Organization.parentOrganization.identifier",
|
|
30
|
+
additionalType = "org.schema.Organization.additionalType",
|
|
31
|
+
sameAs = "org.schema.Organization.sameAs",
|
|
32
|
+
locationIdentifier = "org.schema.Organization.location.identifier",
|
|
33
|
+
logoContentUrl = "org.schema.Organization.logo.contentUrl",
|
|
34
|
+
imageContentUrl = "org.schema.Organization.image.contentUrl",
|
|
35
|
+
userSelected = "Organization.user-selected"
|
|
36
|
+
}
|
|
24
37
|
/** Schema.org Service claims needed to join a public service to one or more Places. */
|
|
25
38
|
export declare enum ClaimsDirectoryServiceSchemaorg {
|
|
26
39
|
identifier = "org.schema.Service.identifier",
|
|
@@ -55,6 +68,7 @@ export declare enum ClaimsProductSchemaorg {
|
|
|
55
68
|
}
|
|
56
69
|
/** Product-local allowlist for the experimental Place, Service and Product directory profile. */
|
|
57
70
|
export declare const DirectoryFlatClaimCatalog: Readonly<{
|
|
71
|
+
readonly Organization: readonly ClaimsDirectoryOrganizationSchemaorg[];
|
|
58
72
|
readonly Place: readonly ClaimsPlaceSchemaorg[];
|
|
59
73
|
readonly Service: readonly ClaimsDirectoryServiceSchemaorg[];
|
|
60
74
|
readonly Product: readonly ClaimsProductSchemaorg[];
|
|
@@ -89,6 +103,21 @@ export type SpecialOpeningHoursInput = Readonly<{
|
|
|
89
103
|
opens?: string;
|
|
90
104
|
closes?: string;
|
|
91
105
|
}>;
|
|
106
|
+
export type DepartmentDirectoryInput = Readonly<{
|
|
107
|
+
id: string;
|
|
108
|
+
legalOrganizationIdentifier: string;
|
|
109
|
+
name: string;
|
|
110
|
+
description?: string;
|
|
111
|
+
specialty?: Readonly<{
|
|
112
|
+
system: 'http://loinc.org';
|
|
113
|
+
code: string;
|
|
114
|
+
display: string;
|
|
115
|
+
}>;
|
|
116
|
+
placeIdentifiers: readonly string[];
|
|
117
|
+
logoUrl?: string;
|
|
118
|
+
imageUrls?: readonly string[];
|
|
119
|
+
userSelected?: boolean;
|
|
120
|
+
}>;
|
|
92
121
|
export type PlaceDirectoryInput = Readonly<{
|
|
93
122
|
id: string;
|
|
94
123
|
organizationIdentifier: string;
|
|
@@ -165,6 +194,14 @@ export declare const DataspaceProtocol2025: Readonly<{
|
|
|
165
194
|
readonly context: "https://w3id.org/dspace/2025/1/context.jsonld";
|
|
166
195
|
readonly httpEndpointType: "https://w3id.org/idsa/v4.1/HTTP";
|
|
167
196
|
}>;
|
|
197
|
+
/**
|
|
198
|
+
* Builds the claims-only public department Organization.
|
|
199
|
+
*
|
|
200
|
+
* One department is one Schema.org/FHIR Organization. Each offered Service
|
|
201
|
+
* remains a separate directory record and owns its own HealthcareService
|
|
202
|
+
* projection through providerIdentifier.
|
|
203
|
+
*/
|
|
204
|
+
export declare function buildDepartmentDirectoryResource(input: DepartmentDirectoryInput): DirectoryResource;
|
|
168
205
|
/** Builds the claims-only public Place authored by an organization. */
|
|
169
206
|
export declare function buildPlaceDirectoryResource(input: PlaceDirectoryInput): DirectoryResource;
|
|
170
207
|
/** Builds a claims-only Schema.org Service joined to its provider and public Places. */
|
|
@@ -173,6 +210,43 @@ export declare function buildServiceDirectoryResource(input: ServiceDirectoryInp
|
|
|
173
210
|
export declare function buildProductDirectoryResource(input: ProductDirectoryInput): DirectoryResource;
|
|
174
211
|
/** Rejects native fields and converts a directory resource into deterministic array-valued claims. */
|
|
175
212
|
export declare function normalizeDirectoryFlatClaimsResource(input: unknown): DirectoryFlatClaimsResource;
|
|
213
|
+
/** Explicitly projects one public department to a native FHIR R5 Organization. */
|
|
214
|
+
export declare function projectDepartmentToFhirR5Organization(department: DirectoryResource | DirectoryFlatClaimsResource): Readonly<{
|
|
215
|
+
meta: {
|
|
216
|
+
claims: {
|
|
217
|
+
'Organization.identifier': string;
|
|
218
|
+
'Organization.active': string;
|
|
219
|
+
'Organization.name': string | undefined;
|
|
220
|
+
'Organization.part-of': string;
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
type?: {
|
|
224
|
+
coding: {
|
|
225
|
+
system: string;
|
|
226
|
+
code: string;
|
|
227
|
+
}[];
|
|
228
|
+
}[] | undefined;
|
|
229
|
+
partOf: {
|
|
230
|
+
reference: string;
|
|
231
|
+
};
|
|
232
|
+
description?: string | undefined;
|
|
233
|
+
resourceType: "Organization";
|
|
234
|
+
id: string;
|
|
235
|
+
active: true;
|
|
236
|
+
identifier: {
|
|
237
|
+
value: string;
|
|
238
|
+
}[];
|
|
239
|
+
name: string;
|
|
240
|
+
}>;
|
|
241
|
+
/**
|
|
242
|
+
* @deprecated Aggregate department HealthcareService projection is forbidden.
|
|
243
|
+
* Project each offered Service independently with
|
|
244
|
+
* projectServiceToFhirR5HealthcareService(...).
|
|
245
|
+
*/
|
|
246
|
+
export declare function projectDepartmentToFhirR5HealthcareService(department: DirectoryResource | DirectoryFlatClaimsResource, input: Readonly<{
|
|
247
|
+
services: readonly (DirectoryResource | DirectoryFlatClaimsResource)[];
|
|
248
|
+
places: readonly (DirectoryResource | DirectoryFlatClaimsResource)[];
|
|
249
|
+
}>): void;
|
|
176
250
|
/** Explicitly projects one neutral Place to native FHIR R5 Location for export or interop. */
|
|
177
251
|
export declare function projectPlaceToFhirR5Location(place: DirectoryResource | DirectoryFlatClaimsResource): Readonly<{
|
|
178
252
|
resourceType: "Location";
|
|
@@ -340,7 +414,7 @@ export declare function buildOrganizationOfferCatalog(input: Readonly<{
|
|
|
340
414
|
itemListElement: readonly Readonly<{
|
|
341
415
|
'@type': "Offer";
|
|
342
416
|
itemOffered: Readonly<{
|
|
343
|
-
'@type': "Place" | "Service" | "Product";
|
|
417
|
+
'@type': "Organization" | "Place" | "Service" | "Product";
|
|
344
418
|
identifier: string;
|
|
345
419
|
name: string;
|
|
346
420
|
}>;
|
|
@@ -23,6 +23,20 @@ export var ClaimsPlaceSchemaorg;
|
|
|
23
23
|
ClaimsPlaceSchemaorg["specialOpeningHoursSpecification"] = "org.schema.Place.specialOpeningHoursSpecification";
|
|
24
24
|
ClaimsPlaceSchemaorg["userSelected"] = "Place.user-selected";
|
|
25
25
|
})(ClaimsPlaceSchemaorg || (ClaimsPlaceSchemaorg = {}));
|
|
26
|
+
/** Schema.org flat claims for a public department/subOrganization. */
|
|
27
|
+
export var ClaimsDirectoryOrganizationSchemaorg;
|
|
28
|
+
(function (ClaimsDirectoryOrganizationSchemaorg) {
|
|
29
|
+
ClaimsDirectoryOrganizationSchemaorg["identifier"] = "org.schema.Organization.identifier";
|
|
30
|
+
ClaimsDirectoryOrganizationSchemaorg["name"] = "org.schema.Organization.name";
|
|
31
|
+
ClaimsDirectoryOrganizationSchemaorg["description"] = "org.schema.Organization.description";
|
|
32
|
+
ClaimsDirectoryOrganizationSchemaorg["parentOrganizationIdentifier"] = "org.schema.Organization.parentOrganization.identifier";
|
|
33
|
+
ClaimsDirectoryOrganizationSchemaorg["additionalType"] = "org.schema.Organization.additionalType";
|
|
34
|
+
ClaimsDirectoryOrganizationSchemaorg["sameAs"] = "org.schema.Organization.sameAs";
|
|
35
|
+
ClaimsDirectoryOrganizationSchemaorg["locationIdentifier"] = "org.schema.Organization.location.identifier";
|
|
36
|
+
ClaimsDirectoryOrganizationSchemaorg["logoContentUrl"] = "org.schema.Organization.logo.contentUrl";
|
|
37
|
+
ClaimsDirectoryOrganizationSchemaorg["imageContentUrl"] = "org.schema.Organization.image.contentUrl";
|
|
38
|
+
ClaimsDirectoryOrganizationSchemaorg["userSelected"] = "Organization.user-selected";
|
|
39
|
+
})(ClaimsDirectoryOrganizationSchemaorg || (ClaimsDirectoryOrganizationSchemaorg = {}));
|
|
26
40
|
/** Schema.org Service claims needed to join a public service to one or more Places. */
|
|
27
41
|
export var ClaimsDirectoryServiceSchemaorg;
|
|
28
42
|
(function (ClaimsDirectoryServiceSchemaorg) {
|
|
@@ -59,6 +73,7 @@ export var ClaimsProductSchemaorg;
|
|
|
59
73
|
})(ClaimsProductSchemaorg || (ClaimsProductSchemaorg = {}));
|
|
60
74
|
/** Product-local allowlist for the experimental Place, Service and Product directory profile. */
|
|
61
75
|
export const DirectoryFlatClaimCatalog = Object.freeze({
|
|
76
|
+
Organization: Object.freeze(Object.values(ClaimsDirectoryOrganizationSchemaorg)),
|
|
62
77
|
Place: Object.freeze(Object.values(ClaimsPlaceSchemaorg)),
|
|
63
78
|
Service: Object.freeze(Object.values(ClaimsDirectoryServiceSchemaorg)),
|
|
64
79
|
Product: Object.freeze(Object.values(ClaimsProductSchemaorg)),
|
|
@@ -151,6 +166,51 @@ function encodeSpecialOpeningHours(value) {
|
|
|
151
166
|
throw new TypeError('place_special_opening_hours_invalid');
|
|
152
167
|
return JSON.stringify({ validFrom, validThrough, opens: value.opens, closes: value.closes });
|
|
153
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Builds the claims-only public department Organization.
|
|
171
|
+
*
|
|
172
|
+
* One department is one Schema.org/FHIR Organization. Each offered Service
|
|
173
|
+
* remains a separate directory record and owns its own HealthcareService
|
|
174
|
+
* projection through providerIdentifier.
|
|
175
|
+
*/
|
|
176
|
+
export function buildDepartmentDirectoryResource(input) {
|
|
177
|
+
const id = required(input.id, 'department_id_required');
|
|
178
|
+
if (!RESOURCE_ID_PATTERN.test(id))
|
|
179
|
+
throw new TypeError('department_id_invalid');
|
|
180
|
+
const placeIdentifiers = unique(input.placeIdentifiers, 'department_place_invalid');
|
|
181
|
+
if (!placeIdentifiers.length)
|
|
182
|
+
throw new TypeError('department_place_required');
|
|
183
|
+
const logoUrl = optionalHttpsUrl(input.logoUrl, 'department_logo_url_invalid');
|
|
184
|
+
const imageUrls = unique(input.imageUrls, 'department_image_url_invalid').map(value => {
|
|
185
|
+
const imageUrl = optionalHttpsUrl(value, 'department_image_url_invalid');
|
|
186
|
+
if (!imageUrl)
|
|
187
|
+
throw new TypeError('department_image_url_invalid');
|
|
188
|
+
return imageUrl;
|
|
189
|
+
});
|
|
190
|
+
const claims = {
|
|
191
|
+
[ClaimsDirectoryOrganizationSchemaorg.identifier]: id,
|
|
192
|
+
[ClaimsDirectoryOrganizationSchemaorg.name]: required(input.name, 'department_name_required'),
|
|
193
|
+
[ClaimsDirectoryOrganizationSchemaorg.parentOrganizationIdentifier]: required(input.legalOrganizationIdentifier, 'department_parent_organization_required'),
|
|
194
|
+
[ClaimsDirectoryOrganizationSchemaorg.sameAs]: `Organization/${id}`,
|
|
195
|
+
[ClaimsDirectoryOrganizationSchemaorg.locationIdentifier]: placeIdentifiers,
|
|
196
|
+
[ClaimsDirectoryOrganizationSchemaorg.userSelected]: input.userSelected ?? false,
|
|
197
|
+
};
|
|
198
|
+
const description = optional(input.description);
|
|
199
|
+
if (description)
|
|
200
|
+
claims[ClaimsDirectoryOrganizationSchemaorg.description] = description;
|
|
201
|
+
if (input.specialty) {
|
|
202
|
+
if (input.specialty.system !== 'http://loinc.org'
|
|
203
|
+
|| !/^L(?:A|P)\d+(?:-\d+)?$/.test(input.specialty.code)
|
|
204
|
+
|| !optional(input.specialty.display))
|
|
205
|
+
throw new TypeError('department_specialty_invalid');
|
|
206
|
+
claims[ClaimsDirectoryOrganizationSchemaorg.additionalType] = `${input.specialty.system}/${input.specialty.code}`;
|
|
207
|
+
}
|
|
208
|
+
if (logoUrl)
|
|
209
|
+
claims[ClaimsDirectoryOrganizationSchemaorg.logoContentUrl] = logoUrl;
|
|
210
|
+
if (imageUrls.length)
|
|
211
|
+
claims[ClaimsDirectoryOrganizationSchemaorg.imageContentUrl] = Object.freeze(imageUrls);
|
|
212
|
+
return Object.freeze({ resourceType: 'Organization', id, meta: Object.freeze({ claims: Object.freeze(claims) }) });
|
|
213
|
+
}
|
|
154
214
|
/** Builds the claims-only public Place authored by an organization. */
|
|
155
215
|
export function buildPlaceDirectoryResource(input) {
|
|
156
216
|
const id = required(input.id, 'place_id_required');
|
|
@@ -302,7 +362,7 @@ export function normalizeDirectoryFlatClaimsResource(input) {
|
|
|
302
362
|
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
303
363
|
throw new TypeError('directory_flat_resource_invalid');
|
|
304
364
|
const value = input;
|
|
305
|
-
if ((value.resourceType !== 'Place' && value.resourceType !== 'Service' && value.resourceType !== 'Product') || Object.keys(value).some(key => !['resourceType', 'id', 'meta'].includes(key)))
|
|
365
|
+
if ((value.resourceType !== 'Organization' && value.resourceType !== 'Place' && value.resourceType !== 'Service' && value.resourceType !== 'Product') || Object.keys(value).some(key => !['resourceType', 'id', 'meta'].includes(key)))
|
|
306
366
|
throw new TypeError('directory_flat_resource_invalid');
|
|
307
367
|
const resourceType = value.resourceType;
|
|
308
368
|
const id = required(value.id, 'directory_flat_resource_invalid');
|
|
@@ -338,6 +398,38 @@ function parseJson(value, code) {
|
|
|
338
398
|
throw new TypeError(code);
|
|
339
399
|
}
|
|
340
400
|
}
|
|
401
|
+
/** Explicitly projects one public department to a native FHIR R5 Organization. */
|
|
402
|
+
export function projectDepartmentToFhirR5Organization(department) {
|
|
403
|
+
if (department.resourceType !== 'Organization')
|
|
404
|
+
throw new TypeError('department_resource_required');
|
|
405
|
+
const parent = required(first(department, ClaimsDirectoryOrganizationSchemaorg.parentOrganizationIdentifier), 'department_parent_organization_required');
|
|
406
|
+
const additionalType = first(department, ClaimsDirectoryOrganizationSchemaorg.additionalType);
|
|
407
|
+
return Object.freeze({
|
|
408
|
+
resourceType: 'Organization', id: department.id, active: true,
|
|
409
|
+
identifier: [{ value: department.id }],
|
|
410
|
+
name: required(first(department, ClaimsDirectoryOrganizationSchemaorg.name), 'department_name_required'),
|
|
411
|
+
...(first(department, ClaimsDirectoryOrganizationSchemaorg.description)
|
|
412
|
+
? { description: first(department, ClaimsDirectoryOrganizationSchemaorg.description) } : {}),
|
|
413
|
+
partOf: { reference: parent },
|
|
414
|
+
...(additionalType ? { type: [{ coding: [{ system: 'http://loinc.org', code: additionalType.replace('http://loinc.org/', '') }] }] } : {}),
|
|
415
|
+
meta: { claims: {
|
|
416
|
+
'Organization.identifier': department.id,
|
|
417
|
+
'Organization.active': 'true',
|
|
418
|
+
'Organization.name': first(department, ClaimsDirectoryOrganizationSchemaorg.name),
|
|
419
|
+
'Organization.part-of': parent,
|
|
420
|
+
} },
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* @deprecated Aggregate department HealthcareService projection is forbidden.
|
|
425
|
+
* Project each offered Service independently with
|
|
426
|
+
* projectServiceToFhirR5HealthcareService(...).
|
|
427
|
+
*/
|
|
428
|
+
export function projectDepartmentToFhirR5HealthcareService(department, input) {
|
|
429
|
+
void department;
|
|
430
|
+
void input;
|
|
431
|
+
throw new TypeError('department_aggregate_healthcare_service_forbidden');
|
|
432
|
+
}
|
|
341
433
|
/** Explicitly projects one neutral Place to native FHIR R5 Location for export or interop. */
|
|
342
434
|
export function projectPlaceToFhirR5Location(place) {
|
|
343
435
|
if (place.resourceType !== 'Place')
|
|
@@ -387,7 +479,8 @@ export function projectServiceToFhirR5HealthcareService(service, input) {
|
|
|
387
479
|
});
|
|
388
480
|
const serviceTypes = values(service, ClaimsDirectoryServiceSchemaorg.serviceType);
|
|
389
481
|
const categories = values(service, ClaimsDirectoryServiceSchemaorg.category);
|
|
390
|
-
const
|
|
482
|
+
const rawOrganization = required(first(service, ClaimsDirectoryServiceSchemaorg.providerIdentifier), 'service_organization_required');
|
|
483
|
+
const organization = rawOrganization.startsWith('Organization/') ? rawOrganization : `Organization/${rawOrganization}`;
|
|
391
484
|
const photoUrl = places.map(place => first(place, ClaimsPlaceSchemaorg.photoContentUrl)).find(Boolean);
|
|
392
485
|
const concept = (code) => ({ coding: [{ code }] });
|
|
393
486
|
return Object.freeze({
|
package/dist/scheduling.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildSchedulableLocationFlatResource } from 'fhir-data-utils-ts';
|
|
1
2
|
/** FHIR R5 Slot statuses accepted by reusable scheduling. */
|
|
2
3
|
export declare const SlotStatuses: Readonly<{
|
|
3
4
|
readonly Busy: "busy";
|
|
@@ -28,12 +29,12 @@ export declare const AppointmentStatuses: Readonly<{
|
|
|
28
29
|
readonly Waitlist: "waitlist";
|
|
29
30
|
}>;
|
|
30
31
|
/** Official FHIR R5 Slot resource-specific SearchParameter codes. */
|
|
31
|
-
export declare const SlotSearchParameters: readonly [
|
|
32
|
+
export declare const SlotSearchParameters: readonly string[];
|
|
32
33
|
/** Official FHIR R5 SearchParameter codes used by reusable scheduling. */
|
|
33
34
|
export declare const SchedulingSearchParameterCatalog: Readonly<{
|
|
34
35
|
readonly Location: readonly string[];
|
|
35
36
|
readonly Schedule: readonly string[];
|
|
36
|
-
readonly Slot: readonly [
|
|
37
|
+
readonly Slot: readonly string[];
|
|
37
38
|
readonly Appointment: readonly string[];
|
|
38
39
|
readonly AppointmentResponse: readonly string[];
|
|
39
40
|
}>;
|
|
@@ -47,7 +48,7 @@ export type SchedulingFlatClaimsResource = Readonly<Record<string, unknown> & {
|
|
|
47
48
|
claims: Readonly<Record<string, readonly string[]>>;
|
|
48
49
|
}>;
|
|
49
50
|
}>;
|
|
50
|
-
/**
|
|
51
|
+
/** Rejects nested native FHIR and normalizes governed flat claims to strings. */
|
|
51
52
|
export declare function normalizeSchedulingFlatClaimsResource(input: unknown): SchedulingFlatClaimsResource;
|
|
52
53
|
export type ClaimsFirstResource = Readonly<Record<string, unknown> & {
|
|
53
54
|
resourceType: string;
|
|
@@ -68,15 +69,18 @@ export declare function buildLocationResource(input: Readonly<{
|
|
|
68
69
|
characteristicCodes?: readonly string[];
|
|
69
70
|
photoUrl?: string;
|
|
70
71
|
}>): ClaimsFirstResource;
|
|
71
|
-
/** Builds
|
|
72
|
+
/** Builds one service-specific Schedule through the neutral FHIR claims graph. */
|
|
72
73
|
export declare function buildScheduleResource(input: Readonly<{
|
|
73
|
-
|
|
74
|
+
scheduleUrn: string;
|
|
74
75
|
name: string;
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
healthcareServiceReference: string;
|
|
77
|
+
resourceActorReferences: readonly string[];
|
|
78
|
+
serviceType: string;
|
|
77
79
|
planningHorizonStart: string;
|
|
78
80
|
planningHorizonEnd: string;
|
|
79
81
|
}>): ClaimsFirstResource;
|
|
82
|
+
/** Builds one independently reservable child Location; equipment remains Device. */
|
|
83
|
+
export declare const buildSchedulableLocationResource: typeof buildSchedulableLocationFlatResource;
|
|
80
84
|
export type AvailabilityPeriod = Readonly<{
|
|
81
85
|
daysOfWeek: readonly number[];
|
|
82
86
|
startTime: string;
|
|
@@ -101,11 +105,13 @@ export type ExpandedSlot = Readonly<{
|
|
|
101
105
|
locationReference: string;
|
|
102
106
|
resource: ClaimsFirstResource;
|
|
103
107
|
}>;
|
|
104
|
-
/** Produces the stable Schedule id for one practitioner/location availability stream. */
|
|
105
|
-
export declare function scheduleIdForLocation(baseScheduleId: string, locationReference: string): string;
|
|
106
108
|
/** Expands bounded weekly availability into claims-first Slots, applying recurring restrictions or expansions. */
|
|
107
109
|
export declare function expandAvailability(input: Readonly<{
|
|
108
|
-
|
|
110
|
+
/** One service-owned Schedule URN for each independently reservable Location. */
|
|
111
|
+
scheduleUrnsByLocation: Readonly<Record<string, string>>;
|
|
112
|
+
healthcareServiceReference: string;
|
|
113
|
+
serviceType: string;
|
|
114
|
+
resourceActorReferencesByLocation: Readonly<Record<string, readonly string[]>>;
|
|
109
115
|
startDate: string;
|
|
110
116
|
endDate: string;
|
|
111
117
|
timeZone: string;
|
|
@@ -161,7 +167,7 @@ export declare function buildAppointmentNotificationResource(input: Readonly<{
|
|
|
161
167
|
reason: string;
|
|
162
168
|
}>): ClaimsFirstResource;
|
|
163
169
|
/** @deprecated Use {@link SlotSearchParameters}. */
|
|
164
|
-
export declare const VeterinarySlotSearchParameters: readonly [
|
|
170
|
+
export declare const VeterinarySlotSearchParameters: readonly string[];
|
|
165
171
|
/** @deprecated Use {@link SchedulingFlatClaimCatalog}. */
|
|
166
172
|
export declare const VeterinarySchedulingFlatClaimCatalog: Readonly<Record<"Appointment" | "AppointmentResponse" | "Location" | "Schedule" | "Slot", readonly string[]>>;
|
|
167
173
|
/** @deprecated Use {@link SchedulingResourceType}. */
|
package/dist/scheduling.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ScheduleR5SearchParameterCatalog, SlotR5SearchParameterCatalog, buildSchedulableLocationFlatResource, buildServiceScheduleFlatGraph, } from 'fhir-data-utils-ts';
|
|
2
|
+
import { buildScheduleSlotIdentity, deriveFhirResourceIdFromUrn, } from 'sos-data-utils-ts/organization-scoped-resource-identity';
|
|
1
3
|
/** FHIR R5 Slot statuses accepted by reusable scheduling. */
|
|
2
4
|
export const SlotStatuses = Object.freeze({ Busy: 'busy', Free: 'free', BusyUnavailable: 'busy-unavailable', BusyTentative: 'busy-tentative', EnteredInError: 'entered-in-error' });
|
|
3
5
|
/** FHIR R5 AppointmentResponse participant statuses. */
|
|
@@ -8,11 +10,11 @@ export const AppointmentStatuses = Object.freeze({
|
|
|
8
10
|
Cancelled: 'cancelled', NoShow: 'noshow', EnteredInError: 'entered-in-error', CheckedIn: 'checked-in', Waitlist: 'waitlist',
|
|
9
11
|
});
|
|
10
12
|
/** Official FHIR R5 Slot resource-specific SearchParameter codes. */
|
|
11
|
-
export const SlotSearchParameters = Object.freeze(
|
|
13
|
+
export const SlotSearchParameters = Object.freeze(SlotR5SearchParameterCatalog.map(definition => definition.claim.slice('Slot.'.length)));
|
|
12
14
|
/** Official FHIR R5 SearchParameter codes used by reusable scheduling. */
|
|
13
15
|
export const SchedulingSearchParameterCatalog = Object.freeze({
|
|
14
16
|
Location: Object.freeze(['characteristic', 'identifier', 'name', 'near', 'organization', 'status', 'type']),
|
|
15
|
-
Schedule: Object.freeze(
|
|
17
|
+
Schedule: Object.freeze(ScheduleR5SearchParameterCatalog.map(definition => definition.claim.slice('Schedule.'.length))),
|
|
16
18
|
Slot: SlotSearchParameters,
|
|
17
19
|
Appointment: Object.freeze(['actor', 'appointment-type', 'based-on', 'date', 'identifier', 'location', 'part-status', 'patient', 'practitioner', 'reason-code', 'reason-reference', 'service-category', 'service-type', 'slot', 'specialty', 'status', 'subject', 'supporting-info']),
|
|
18
20
|
AppointmentResponse: Object.freeze(['_lastUpdated', 'actor', 'appointment', 'group', 'identifier', 'location', 'part-status', 'patient', 'practitioner']),
|
|
@@ -22,17 +24,12 @@ export const SchedulingFlatClaimCatalog = Object.freeze(Object.fromEntries(Objec
|
|
|
22
24
|
resourceType,
|
|
23
25
|
Object.freeze([
|
|
24
26
|
...parameters.map(parameter => `${resourceType}.${parameter}`),
|
|
27
|
+
...(resourceType === 'Schedule' ? ['Schedule.planning-horizon-start', 'Schedule.planning-horizon-end'] : []),
|
|
28
|
+
...(resourceType === 'Slot' ? ['Slot.end'] : []),
|
|
25
29
|
...(resourceType === 'Appointment' ? ['Appointment.user-selected'] : []),
|
|
26
30
|
]),
|
|
27
31
|
])));
|
|
28
|
-
|
|
29
|
-
Location: new Set(['identifier', 'status', 'mode', 'name', 'description', 'type', 'telecom', 'address', 'physicalType', 'position', 'managingOrganization', 'characteristic', 'contained']),
|
|
30
|
-
Schedule: new Set(['identifier', 'active', 'serviceCategory', 'serviceType', 'specialty', 'name', 'actor', 'planningHorizon', 'comment']),
|
|
31
|
-
Slot: new Set(['identifier', 'serviceCategory', 'serviceType', 'specialty', 'appointmentType', 'schedule', 'status', 'start', 'end', 'overbooked', 'comment']),
|
|
32
|
-
Appointment: new Set(['identifier', 'status', 'serviceCategory', 'serviceType', 'specialty', 'appointmentType', 'reason', 'description', 'start', 'end', 'slot', 'participant', 'subject', 'note', 'supportingInformation', 'contained']),
|
|
33
|
-
AppointmentResponse: new Set(['identifier', 'appointment', 'proposedNewTime', 'start', 'end', 'participantType', 'actor', 'participantStatus', 'comment', 'recurring', 'occurrenceDate', 'recurrenceId', 'contained']),
|
|
34
|
-
});
|
|
35
|
-
/** Validates native FHIR scheduling data and normalizes indexed SearchParameter claims to strings. */
|
|
32
|
+
/** Rejects nested native FHIR and normalizes governed flat claims to strings. */
|
|
36
33
|
export function normalizeSchedulingFlatClaimsResource(input) {
|
|
37
34
|
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
38
35
|
throw new TypeError('scheduling_flat_resource_invalid');
|
|
@@ -40,7 +37,7 @@ export function normalizeSchedulingFlatClaimsResource(input) {
|
|
|
40
37
|
if (typeof value.resourceType !== 'string' || !(value.resourceType in SchedulingFlatClaimCatalog))
|
|
41
38
|
throw new TypeError('scheduling_flat_resource_invalid');
|
|
42
39
|
const resourceType = value.resourceType;
|
|
43
|
-
if (Object.keys(value).some(key => !['resourceType', 'id', 'meta'].includes(key)
|
|
40
|
+
if (Object.keys(value).some(key => !['resourceType', 'id', 'meta'].includes(key)))
|
|
44
41
|
throw new TypeError('scheduling_flat_resource_invalid');
|
|
45
42
|
const id = required(String(value.id || ''), 'scheduling_flat_resource_invalid');
|
|
46
43
|
if (!/^[A-Za-z0-9\-.]{1,64}$/.test(id))
|
|
@@ -90,31 +87,25 @@ export function buildLocationResource(input) {
|
|
|
90
87
|
'Location.near': `${input.latitude}|${input.longitude}`,
|
|
91
88
|
'Location.organization': organizationReference,
|
|
92
89
|
...(characteristicCodes.length ? { 'Location.characteristic': characteristicCodes } : {}),
|
|
93
|
-
}, {
|
|
94
|
-
identifier: [{ value: input.id }], status: 'active', mode: 'instance', name,
|
|
95
|
-
...(input.description ? { description: input.description.trim() } : {}),
|
|
96
|
-
position: { latitude: input.latitude, longitude: input.longitude, ...(input.altitude === undefined ? {} : { altitude: input.altitude }) },
|
|
97
|
-
managingOrganization: { reference: organizationReference },
|
|
98
|
-
...(characteristicCodes.length ? { characteristic: characteristicCodes.map(code => ({ coding: [{ code }] })) } : {}),
|
|
99
|
-
...(input.photoUrl ? { contained: [{ resourceType: 'DocumentReference', id: 'location-photo', status: 'current', content: [{ attachment: { url: input.photoUrl } }] }] } : {}),
|
|
100
90
|
});
|
|
101
91
|
}
|
|
102
|
-
/** Builds
|
|
92
|
+
/** Builds one service-specific Schedule through the neutral FHIR claims graph. */
|
|
103
93
|
export function buildScheduleResource(input) {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
identifier: [{ value: input.id }], active: true, name,
|
|
115
|
-
actor: actors.map(reference => ({ reference })), planningHorizon: { start, end },
|
|
94
|
+
const id = deriveFhirResourceIdFromUrn(input.scheduleUrn);
|
|
95
|
+
const graph = buildServiceScheduleFlatGraph({
|
|
96
|
+
scheduleId: id,
|
|
97
|
+
scheduleIdentifier: input.scheduleUrn,
|
|
98
|
+
name: input.name,
|
|
99
|
+
healthcareServiceReference: input.healthcareServiceReference,
|
|
100
|
+
resourceActorReferences: input.resourceActorReferences,
|
|
101
|
+
serviceType: input.serviceType,
|
|
102
|
+
planningHorizon: { start: input.planningHorizonStart, end: input.planningHorizonEnd },
|
|
103
|
+
slots: [],
|
|
116
104
|
});
|
|
105
|
+
return graph[0];
|
|
117
106
|
}
|
|
107
|
+
/** Builds one independently reservable child Location; equipment remains Device. */
|
|
108
|
+
export const buildSchedulableLocationResource = buildSchedulableLocationFlatResource;
|
|
118
109
|
const minutes = (value) => { const [hour, minute] = assertTime(value).split(':').map(Number); return hour * 60 + minute; };
|
|
119
110
|
const timeFromMinutes = (value) => `${String(Math.floor(value / 60)).padStart(2, '0')}:${String(value % 60).padStart(2, '0')}`;
|
|
120
111
|
const dates = (start, end) => { const result = []; const cursor = new Date(`${start}T00:00:00Z`); const last = new Date(`${end}T00:00:00Z`); while (cursor <= last) {
|
|
@@ -140,12 +131,6 @@ const matchesExceptionDate = (date, recurrence) => { const [, , day] = dateParts
|
|
|
140
131
|
const validateRange = (startTime, endTime, code) => { const start = minutes(startTime); const end = minutes(endTime); if (start >= end)
|
|
141
132
|
throw new Error(code); return [start, end]; };
|
|
142
133
|
const slotKey = (location, date, start, end) => `${location}|${date}|${start}|${end}`;
|
|
143
|
-
/** Produces the stable Schedule id for one practitioner/location availability stream. */
|
|
144
|
-
export function scheduleIdForLocation(baseScheduleId, locationReference) {
|
|
145
|
-
const base = required(baseScheduleId, 'schedule_id_required');
|
|
146
|
-
const locationId = required(locationReference, 'availability_location_required').split('/').at(-1);
|
|
147
|
-
return `${base}-${locationId}`;
|
|
148
|
-
}
|
|
149
134
|
/** Expands bounded weekly availability into claims-first Slots, applying recurring restrictions or expansions. */
|
|
150
135
|
export function expandAvailability(input) {
|
|
151
136
|
const startDate = assertDate(input.startDate);
|
|
@@ -201,11 +186,33 @@ export function expandAvailability(input) {
|
|
|
201
186
|
return Object.freeze([...base.values()].sort((left, right) => slotKey(left.locationReference, left.localDate, left.localStart, left.localEnd).localeCompare(slotKey(right.locationReference, right.localDate, right.localStart, right.localEnd))).map(slot => {
|
|
202
187
|
const start = zonedInstant(slot.localDate, slot.localStart, input.timeZone);
|
|
203
188
|
const end = zonedInstant(slot.localDate, slot.localEnd, input.timeZone);
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
189
|
+
const scheduleUrn = required(input.scheduleUrnsByLocation[slot.locationReference] || '', 'availability_schedule_urn_required');
|
|
190
|
+
const scheduleResourceId = deriveFhirResourceIdFromUrn(scheduleUrn);
|
|
191
|
+
const slotIdentity = buildScheduleSlotIdentity({ scheduleUrn, start, end });
|
|
192
|
+
const scheduleReference = `Schedule/${scheduleResourceId}`;
|
|
193
|
+
const actors = input.resourceActorReferencesByLocation[slot.locationReference];
|
|
194
|
+
if (!actors?.includes(slot.locationReference))
|
|
195
|
+
throw new Error('availability_resource_actors_required');
|
|
196
|
+
const graph = buildServiceScheduleFlatGraph({
|
|
197
|
+
scheduleId: scheduleReference.slice('Schedule/'.length),
|
|
198
|
+
scheduleIdentifier: scheduleReference,
|
|
199
|
+
name: scheduleReference,
|
|
200
|
+
healthcareServiceReference: input.healthcareServiceReference,
|
|
201
|
+
resourceActorReferences: actors,
|
|
202
|
+
serviceType: input.serviceType,
|
|
203
|
+
planningHorizon: {
|
|
204
|
+
start: zonedInstant(startDate, '00:00', input.timeZone),
|
|
205
|
+
end: new Date(Date.parse(zonedInstant(endDate, '23:59', input.timeZone)) + 60_000).toISOString(),
|
|
206
|
+
},
|
|
207
|
+
slots: [{
|
|
208
|
+
slotId: slotIdentity.id,
|
|
209
|
+
slotIdentifier: slotIdentity.identifier,
|
|
210
|
+
status: SlotStatuses.Free,
|
|
211
|
+
start,
|
|
212
|
+
end,
|
|
213
|
+
}],
|
|
214
|
+
});
|
|
215
|
+
return Object.freeze({ ...slot, resource: graph[1] });
|
|
209
216
|
}));
|
|
210
217
|
}
|
|
211
218
|
/** Builds a claims-first response that confirms, declines or proposes a new appointment time. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vet-data-utils-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Browser-safe governed VetChain data contracts",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Connecting Solution & Applications Ltd",
|
|
@@ -157,10 +157,10 @@
|
|
|
157
157
|
"typescript": "^5.5.4"
|
|
158
158
|
},
|
|
159
159
|
"dependencies": {
|
|
160
|
-
"fhir-data-utils-ts": "0.
|
|
160
|
+
"fhir-data-utils-ts": "0.6.0",
|
|
161
161
|
"gdc-common-utils-ts": "2.9.12",
|
|
162
162
|
"pako": "^2.2.0",
|
|
163
|
-
"sos-data-utils-ts": "0.
|
|
163
|
+
"sos-data-utils-ts": "0.5.1"
|
|
164
164
|
},
|
|
165
165
|
"engines": {
|
|
166
166
|
"node": ">=20"
|