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 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
- sites as claims-only Schema.org `Place` resources and their offerings as
27
- claims-only `Service` or `Product` resources. A care organization publishes
28
- services; an insurance organization publishes products in a Schema.org
29
- `OfferCatalog`. Photos, accessibility, coordinates, address and opening-hours
30
- specifications remain neutral indexed claims. Native FHIR R5 `Location`,
31
- `HealthcareService` and `InsurancePlan` are produced only by explicit
32
- projection helpers. Member-specific FHIR `Coverage` is never a public catalog
33
- entry. The shared `gdc-*` claim catalog remains unchanged while this profile is
34
- validated in VetChain.
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 organization = required(first(service, ClaimsDirectoryServiceSchemaorg.providerIdentifier), 'service_organization_required');
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({
@@ -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 ["appointment-type", "identifier", "schedule", "service-category", "service-type", "service-type-reference", "specialty", "start", "status"];
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 ["appointment-type", "identifier", "schedule", "service-category", "service-type", "service-type-reference", "specialty", "start", "status"];
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
- /** Validates native FHIR scheduling data and normalizes indexed SearchParameter claims to strings. */
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 the claims-first FHIR R5 Schedule joining one practitioner and consultation Location. */
72
+ /** Builds one service-specific Schedule through the neutral FHIR claims graph. */
72
73
  export declare function buildScheduleResource(input: Readonly<{
73
- id: string;
74
+ scheduleUrn: string;
74
75
  name: string;
75
- practitionerReference: string;
76
- locationReference: string;
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
- scheduleId: string;
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 ["appointment-type", "identifier", "schedule", "service-category", "service-type", "service-type-reference", "specialty", "start", "status"];
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}. */
@@ -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(['appointment-type', 'identifier', 'schedule', 'service-category', 'service-type', 'service-type-reference', 'specialty', 'start', 'status']);
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(['active', 'actor', 'date', 'identifier', 'name', 'service-category', 'service-type', 'specialty']),
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
- const nativeFieldsByResource = Object.freeze({
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) && !nativeFieldsByResource[resourceType].has(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 the claims-first FHIR R5 Schedule joining one practitioner and consultation Location. */
92
+ /** Builds one service-specific Schedule through the neutral FHIR claims graph. */
103
93
  export function buildScheduleResource(input) {
104
- const start = assertDate(input.planningHorizonStart);
105
- const end = assertDate(input.planningHorizonEnd);
106
- if (start > end)
107
- throw new Error('schedule_horizon_invalid');
108
- const name = required(input.name, 'schedule_name_required');
109
- const actors = Object.freeze([required(input.practitionerReference, 'schedule_practitioner_required'), required(input.locationReference, 'schedule_location_required')]);
110
- return claimsResource('Schedule', input.id, {
111
- 'Schedule.identifier': input.id, 'Schedule.active': true, 'Schedule.name': name,
112
- 'Schedule.actor': actors, 'Schedule.date': Object.freeze([start, end]),
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 id = `${input.scheduleId}-${slot.localDate}-${slot.localStart.replace(':', '')}-${slot.locationReference.split('/').at(-1)}`;
205
- const scheduleReference = `Schedule/${scheduleIdForLocation(input.scheduleId, slot.locationReference)}`;
206
- return Object.freeze({ ...slot, resource: claimsResource('Slot', id, {
207
- 'Slot.identifier': id, 'Slot.schedule': scheduleReference, 'Slot.status': SlotStatuses.Free, 'Slot.start': start,
208
- }, { identifier: [{ value: id }], schedule: { reference: scheduleReference }, status: SlotStatuses.Free, start, end }) });
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.5.37",
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.2.4",
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.3.7"
163
+ "sos-data-utils-ts": "0.5.1"
164
164
  },
165
165
  "engines": {
166
166
  "node": ">=20"