vet-data-utils-ts 0.5.20 → 0.5.22

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
@@ -1,5 +1,25 @@
1
1
  # vet-data-utils-ts
2
2
 
3
+ The product-local `place-service-directory` export models persistent public
4
+ sites as claims-only Schema.org `Place` resources and their offerings as
5
+ claims-only `Service` or `Product` resources. A care organization publishes
6
+ services; an insurance organization publishes products in a Schema.org
7
+ `OfferCatalog`. Photos, accessibility, coordinates, address and opening-hours
8
+ specifications remain neutral indexed claims. Native FHIR R5 `Location`,
9
+ `HealthcareService` and `InsurancePlan` are produced only by explicit
10
+ projection helpers. Member-specific FHIR `Coverage` is never a public catalog
11
+ entry. The shared `gdc-*` claim catalog remains unchanged while this profile is
12
+ validated in VetChain.
13
+
14
+ The customer-facing Schema.org `OfferCatalog` is distinct from the Eclipse
15
+ Dataspace Protocol catalog. `buildDataspaceCatalogAdvertisement(...)` exposes a
16
+ transferable catalog snapshot as a DSP 2025-1 Dataset with an ODRL use Offer;
17
+ `buildDataspaceDataAddress(...)` describes the endpoint only in the negotiated
18
+ transfer flow. The package deliberately has no third-party DSP runtime
19
+ dependency: the Eclipse project publishes the specification and Java TCK/EDC
20
+ implementation, while currently available npm implementations are independent
21
+ projects and are not adopted implicitly.
22
+
3
23
  ## Veterinary immunization credentials
4
24
 
5
25
  `buildVeterinaryImmunization(...)` creates a FHIR R4 `Immunization` with the
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * from './index-projection-tags.js';
13
13
  export * from './iso-jurisdictions.js';
14
14
  export * from './organization-application.js';
15
15
  export * from './payment.js';
16
+ export * from './place-service-directory.js';
16
17
  export * from './research-study.js';
17
18
  export * from './sectors.js';
18
19
  export * from './shc.js';
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export * from './index-projection-tags.js';
13
13
  export * from './iso-jurisdictions.js';
14
14
  export * from './organization-application.js';
15
15
  export * from './payment.js';
16
+ export * from './place-service-directory.js';
16
17
  export * from './research-study.js';
17
18
  export * from './sectors.js';
18
19
  export * from './shc.js';
@@ -0,0 +1,412 @@
1
+ /** Schema.org flat claims for a persistent, sector-neutral public Place. */
2
+ export declare enum ClaimsPlaceSchemaorg {
3
+ identifier = "org.schema.Place.identifier",
4
+ additionalType = "org.schema.Place.additionalType",
5
+ name = "org.schema.Place.name",
6
+ description = "org.schema.Place.description",
7
+ ownerIdentifier = "org.schema.Place.owner.identifier",
8
+ addressCountry = "org.schema.Place.address.addressCountry",
9
+ addressRegion = "org.schema.Place.address.addressRegion",
10
+ addressLocality = "org.schema.Place.address.addressLocality",
11
+ extendedAddress = "org.schema.Place.address.extendedAddress",
12
+ postalCode = "org.schema.Place.address.postalCode",
13
+ streetAddress = "org.schema.Place.address.streetAddress",
14
+ latitude = "org.schema.Place.geo.latitude",
15
+ longitude = "org.schema.Place.geo.longitude",
16
+ elevation = "org.schema.Place.geo.elevation",
17
+ photoContentUrl = "org.schema.Place.photo.contentUrl",
18
+ amenityFeature = "org.schema.Place.amenityFeature",
19
+ publicAccess = "org.schema.Place.publicAccess",
20
+ openingHoursSpecification = "org.schema.Place.openingHoursSpecification",
21
+ specialOpeningHoursSpecification = "org.schema.Place.specialOpeningHoursSpecification",
22
+ userSelected = "Place.user-selected"
23
+ }
24
+ /** Schema.org Service claims needed to join a public service to one or more Places. */
25
+ export declare enum ClaimsDirectoryServiceSchemaorg {
26
+ identifier = "org.schema.Service.identifier",
27
+ name = "org.schema.Service.name",
28
+ description = "org.schema.Service.description",
29
+ providerIdentifier = "org.schema.Service.provider.identifier",
30
+ placeIdentifier = "org.schema.Service.availableChannel.serviceLocation.identifier",
31
+ serviceType = "org.schema.Service.serviceType",
32
+ category = "org.schema.Service.category",
33
+ areaServed = "org.schema.Service.areaServed",
34
+ userSelected = "Service.user-selected"
35
+ }
36
+ /** Schema.org Product claims for an organization's public commercial offering. */
37
+ export declare enum ClaimsProductSchemaorg {
38
+ identifier = "org.schema.Product.identifier",
39
+ additionalType = "org.schema.Product.additionalType",
40
+ name = "org.schema.Product.name",
41
+ description = "org.schema.Product.description",
42
+ providerIdentifier = "org.schema.Product.provider.identifier",
43
+ category = "org.schema.Product.category",
44
+ areaServed = "org.schema.Product.areaServed",
45
+ url = "org.schema.Product.url",
46
+ imageContentUrl = "org.schema.Product.image.contentUrl",
47
+ availabilityStarts = "org.schema.Product.offers.availabilityStarts",
48
+ availabilityEnds = "org.schema.Product.offers.availabilityEnds",
49
+ price = "org.schema.Product.offers.price",
50
+ priceCurrency = "org.schema.Product.offers.priceCurrency",
51
+ insuranceStatus = "org.schema.Product.additionalProperty.insuranceStatus",
52
+ insuranceCoverageType = "org.schema.Product.additionalProperty.insuranceCoverageType",
53
+ insuranceNetwork = "org.schema.Product.additionalProperty.insuranceNetwork",
54
+ userSelected = "Product.user-selected"
55
+ }
56
+ /** Product-local allowlist for the experimental Place, Service and Product directory profile. */
57
+ export declare const DirectoryFlatClaimCatalog: Readonly<{
58
+ readonly Place: readonly ClaimsPlaceSchemaorg[];
59
+ readonly Service: readonly ClaimsDirectoryServiceSchemaorg[];
60
+ readonly Product: readonly ClaimsProductSchemaorg[];
61
+ }>;
62
+ export type DirectoryResourceType = keyof typeof DirectoryFlatClaimCatalog;
63
+ export type DirectoryClaimValue = string | number | boolean | readonly string[];
64
+ export type DirectoryResource = Readonly<{
65
+ resourceType: DirectoryResourceType;
66
+ id: string;
67
+ meta: Readonly<{
68
+ claims: Readonly<Record<string, DirectoryClaimValue>>;
69
+ }>;
70
+ }>;
71
+ export type DirectoryFlatClaimsResource = Readonly<{
72
+ resourceType: DirectoryResourceType;
73
+ id: string;
74
+ meta: Readonly<{
75
+ claims: Readonly<Record<string, readonly string[]>>;
76
+ }>;
77
+ }>;
78
+ export type OpeningHoursInput = Readonly<{
79
+ daysOfWeek: readonly string[];
80
+ opens: string;
81
+ closes: string;
82
+ validFrom?: string;
83
+ validThrough?: string;
84
+ }>;
85
+ export type SpecialOpeningHoursInput = Readonly<{
86
+ validFrom: string;
87
+ validThrough: string;
88
+ closed?: boolean;
89
+ opens?: string;
90
+ closes?: string;
91
+ }>;
92
+ export type PlaceDirectoryInput = Readonly<{
93
+ id: string;
94
+ organizationIdentifier: string;
95
+ name: string;
96
+ description?: string;
97
+ additionalType?: string;
98
+ address: Readonly<{
99
+ addressCountry: string;
100
+ addressRegion?: string;
101
+ addressLocality?: string;
102
+ extendedAddress?: string;
103
+ postalCode: string;
104
+ streetAddress?: string;
105
+ }>;
106
+ position: Readonly<{
107
+ latitude: number;
108
+ longitude: number;
109
+ elevation?: number;
110
+ }>;
111
+ photoUrl?: string;
112
+ accessibilityFeatures?: readonly string[];
113
+ publicAccess?: boolean;
114
+ openingHours?: readonly OpeningHoursInput[];
115
+ specialOpeningHours?: readonly SpecialOpeningHoursInput[];
116
+ userSelected?: boolean;
117
+ }>;
118
+ export type ServiceDirectoryInput = Readonly<{
119
+ id: string;
120
+ organizationIdentifier: string;
121
+ placeIdentifiers: readonly string[];
122
+ name: string;
123
+ description?: string;
124
+ serviceTypes: readonly string[];
125
+ categories?: readonly string[];
126
+ areaServed?: readonly string[];
127
+ userSelected?: boolean;
128
+ }>;
129
+ export type ProductDirectoryInput = Readonly<{
130
+ id: string;
131
+ organizationIdentifier: string;
132
+ name: string;
133
+ description?: string;
134
+ productType: string;
135
+ categories?: readonly string[];
136
+ areaServed?: readonly string[];
137
+ url?: string;
138
+ imageUrl?: string;
139
+ availableFrom?: string;
140
+ availableThrough?: string;
141
+ price?: number;
142
+ priceCurrency?: string;
143
+ insurance?: Readonly<{
144
+ status: 'draft' | 'active' | 'retired' | 'unknown';
145
+ coverageTypes: readonly string[];
146
+ networks?: readonly string[];
147
+ }>;
148
+ userSelected?: boolean;
149
+ }>;
150
+ /** Sector-owned offering kind. It drives presentation only and grants no authority. */
151
+ export declare const OrganizationOfferingKinds: Readonly<{
152
+ readonly 'animal-care': "Service";
153
+ readonly 'animal-insurance': "Product";
154
+ }>;
155
+ /** Stable Eclipse Dataspace Protocol 2025-1 identifiers, including current editorial errata. */
156
+ export declare const DataspaceProtocol2025: Readonly<{
157
+ readonly release: "2025-1-err1";
158
+ readonly context: "https://w3id.org/dspace/2025/1/context.jsonld";
159
+ readonly httpEndpointType: "https://w3id.org/idsa/v4.1/HTTP";
160
+ }>;
161
+ /** Builds the claims-only public Place authored by an organization. */
162
+ export declare function buildPlaceDirectoryResource(input: PlaceDirectoryInput): DirectoryResource;
163
+ /** Builds a claims-only Schema.org Service joined to its provider and public Places. */
164
+ export declare function buildServiceDirectoryResource(input: ServiceDirectoryInput): DirectoryResource;
165
+ /** Builds a claims-only Schema.org Product; insurance details remain a public plan definition, not personal Coverage. */
166
+ export declare function buildProductDirectoryResource(input: ProductDirectoryInput): DirectoryResource;
167
+ /** Rejects native fields and converts a directory resource into deterministic array-valued claims. */
168
+ export declare function normalizeDirectoryFlatClaimsResource(input: unknown): DirectoryFlatClaimsResource;
169
+ /** Explicitly projects one neutral Place to native FHIR R5 Location for export or interop. */
170
+ export declare function projectPlaceToFhirR5Location(place: DirectoryResource | DirectoryFlatClaimsResource): Readonly<{
171
+ resourceType: "Location";
172
+ id: string;
173
+ status: "active";
174
+ mode: "instance";
175
+ name: string | undefined;
176
+ description: string | undefined;
177
+ address: {
178
+ line?: (string | undefined)[] | undefined;
179
+ country: string | undefined;
180
+ state: string | undefined;
181
+ city: string | undefined;
182
+ postalCode: string;
183
+ };
184
+ position: {
185
+ altitude?: number | undefined;
186
+ latitude: number;
187
+ longitude: number;
188
+ };
189
+ managingOrganization: {
190
+ reference: string;
191
+ };
192
+ characteristic: {
193
+ coding: {
194
+ code: string;
195
+ }[];
196
+ }[];
197
+ hoursOfOperation: {
198
+ availableTime: {
199
+ daysOfWeek: string[];
200
+ availableStartTime: string;
201
+ availableEndTime: string;
202
+ }[];
203
+ notAvailableTime: {
204
+ description: string;
205
+ during: {
206
+ start: string;
207
+ end: string;
208
+ };
209
+ }[];
210
+ }[];
211
+ meta: {
212
+ claims: {
213
+ 'Location.characteristic'?: readonly string[] | undefined;
214
+ 'Location.identifier': string;
215
+ 'Location.status': string;
216
+ 'Location.name': string | undefined;
217
+ 'Location.address-postalcode': string;
218
+ 'Location.near': string;
219
+ 'Location.organization': string;
220
+ };
221
+ };
222
+ }>;
223
+ /** Explicitly projects a neutral Service and its Places to native FHIR R5 HealthcareService. */
224
+ export declare function projectServiceToFhirR5HealthcareService(service: DirectoryResource | DirectoryFlatClaimsResource, input: Readonly<{
225
+ places: readonly (DirectoryResource | DirectoryFlatClaimsResource)[];
226
+ }>): Readonly<{
227
+ meta: {
228
+ claims: {
229
+ 'HealthcareService.service-category'?: readonly string[] | undefined;
230
+ 'HealthcareService.identifier': string;
231
+ 'HealthcareService.organization': string;
232
+ 'HealthcareService.name': string | undefined;
233
+ 'HealthcareService.location': string[];
234
+ 'HealthcareService.service-type': readonly string[];
235
+ };
236
+ };
237
+ photo?: {
238
+ url: string;
239
+ } | undefined;
240
+ resourceType: "HealthcareService";
241
+ id: string;
242
+ active: true;
243
+ providedBy: {
244
+ reference: string;
245
+ };
246
+ name: string | undefined;
247
+ comment: string | undefined;
248
+ category: {
249
+ coding: {
250
+ code: string;
251
+ }[];
252
+ }[];
253
+ type: {
254
+ coding: {
255
+ code: string;
256
+ }[];
257
+ }[];
258
+ location: {
259
+ reference: string;
260
+ }[];
261
+ }>;
262
+ /** Explicitly projects a public insurance Product definition to FHIR R5 InsurancePlan. */
263
+ export declare function projectProductToFhirR5InsurancePlan(product: DirectoryResource | DirectoryFlatClaimsResource): Readonly<{
264
+ ownedBy: {
265
+ reference: string;
266
+ };
267
+ coverageArea: {
268
+ reference: string;
269
+ }[];
270
+ network: {
271
+ reference: string;
272
+ }[];
273
+ coverage: {
274
+ type: {
275
+ coding: {
276
+ code: string;
277
+ }[];
278
+ };
279
+ network: {
280
+ reference: string;
281
+ }[];
282
+ benefit: {
283
+ type: {
284
+ coding: {
285
+ code: string;
286
+ }[];
287
+ };
288
+ }[];
289
+ }[];
290
+ meta: {
291
+ claims: {
292
+ 'InsurancePlan.identifier': string;
293
+ 'InsurancePlan.status': string;
294
+ 'InsurancePlan.name': string;
295
+ 'InsurancePlan.owned-by': string;
296
+ 'InsurancePlan.type': string;
297
+ };
298
+ };
299
+ period?: {
300
+ start: string;
301
+ end: string;
302
+ } | undefined;
303
+ resourceType: "InsurancePlan";
304
+ id: string;
305
+ identifier: {
306
+ value: string;
307
+ }[];
308
+ status: string;
309
+ type: {
310
+ coding: {
311
+ code: string;
312
+ }[];
313
+ }[];
314
+ name: string;
315
+ }>;
316
+ /** Builds the Schema.org JSON-LD OfferCatalog presented by one organization. */
317
+ export declare function buildOrganizationOfferCatalog(input: Readonly<{
318
+ id: string;
319
+ name: string;
320
+ organizationIdentifier: string;
321
+ sector: keyof typeof OrganizationOfferingKinds;
322
+ offerings: readonly (DirectoryResource | DirectoryFlatClaimsResource)[];
323
+ }>): Readonly<{
324
+ '@context': "https://schema.org";
325
+ '@type': "OfferCatalog";
326
+ identifier: string;
327
+ name: string;
328
+ offeredBy: Readonly<{
329
+ '@type': "Organization";
330
+ identifier: string;
331
+ }>;
332
+ numberOfItems: number;
333
+ itemListElement: readonly Readonly<{
334
+ '@type': "Offer";
335
+ itemOffered: Readonly<{
336
+ '@type': "Service" | "Product";
337
+ identifier: string;
338
+ name: string;
339
+ }>;
340
+ }>[];
341
+ }>;
342
+ /** Advertises a transferable catalog snapshot as a DSP Dataset with an ODRL use Offer. */
343
+ export declare function buildDataspaceCatalogAdvertisement(input: Readonly<{
344
+ catalogId: string;
345
+ participantId: string;
346
+ dataServiceId: string;
347
+ endpointUrl: string;
348
+ datasetId: string;
349
+ policyId: string;
350
+ distributionFormat: string;
351
+ }>): Readonly<{
352
+ '@context': readonly "https://w3id.org/dspace/2025/1/context.jsonld"[];
353
+ '@id': string;
354
+ '@type': "Catalog";
355
+ participantId: string;
356
+ service: readonly Readonly<{
357
+ '@id': string;
358
+ '@type': "DataService";
359
+ endpointURL: string;
360
+ }>[];
361
+ dataset: readonly Readonly<{
362
+ '@id': string;
363
+ '@type': "Dataset";
364
+ hasPolicy: readonly Readonly<{
365
+ '@id': string;
366
+ '@type': "Offer";
367
+ permission: readonly Readonly<{
368
+ action: "use";
369
+ }>[];
370
+ }>[];
371
+ distribution: readonly Readonly<{
372
+ '@type': "Distribution";
373
+ format: string;
374
+ accessService: string;
375
+ }>[];
376
+ }>[];
377
+ }>;
378
+ /** Builds the DSP DataAddress exchanged only after agreement negotiation starts a transfer. */
379
+ export declare function buildDataspaceDataAddress(input: Readonly<{
380
+ endpointType: string;
381
+ endpoint?: string;
382
+ endpointProperties?: readonly Readonly<{
383
+ name: string;
384
+ value: string;
385
+ }>[];
386
+ }>): Readonly<{
387
+ endpointProperties?: readonly Readonly<{
388
+ '@type': "EndpointProperty";
389
+ name: string;
390
+ value: string;
391
+ }>[] | undefined;
392
+ endpoint?: string | undefined;
393
+ '@type': "DataAddress";
394
+ endpointType: string;
395
+ }>;
396
+ export type ClinicMarketplaceResult = Readonly<{
397
+ place: DirectoryResource | DirectoryFlatClaimsResource;
398
+ services: readonly (DirectoryResource | DirectoryFlatClaimsResource)[];
399
+ distanceKm?: number;
400
+ }>;
401
+ /** Joins and filters a public clinic directory by postal code, service and optional geographic radius. */
402
+ export declare function searchClinicMarketplace(input: Readonly<{
403
+ places: readonly (DirectoryResource | DirectoryFlatClaimsResource)[];
404
+ services: readonly (DirectoryResource | DirectoryFlatClaimsResource)[];
405
+ postalCode?: string;
406
+ serviceTypes?: readonly string[];
407
+ near?: Readonly<{
408
+ latitude: number;
409
+ longitude: number;
410
+ radiusKm: number;
411
+ }>;
412
+ }>): readonly ClinicMarketplaceResult[];
@@ -0,0 +1,550 @@
1
+ // Copyright 2026 Connecting Solution & Applications Ltd under the Apache License, Version 2.0.
2
+ /** Schema.org flat claims for a persistent, sector-neutral public Place. */
3
+ export var ClaimsPlaceSchemaorg;
4
+ (function (ClaimsPlaceSchemaorg) {
5
+ ClaimsPlaceSchemaorg["identifier"] = "org.schema.Place.identifier";
6
+ ClaimsPlaceSchemaorg["additionalType"] = "org.schema.Place.additionalType";
7
+ ClaimsPlaceSchemaorg["name"] = "org.schema.Place.name";
8
+ ClaimsPlaceSchemaorg["description"] = "org.schema.Place.description";
9
+ ClaimsPlaceSchemaorg["ownerIdentifier"] = "org.schema.Place.owner.identifier";
10
+ ClaimsPlaceSchemaorg["addressCountry"] = "org.schema.Place.address.addressCountry";
11
+ ClaimsPlaceSchemaorg["addressRegion"] = "org.schema.Place.address.addressRegion";
12
+ ClaimsPlaceSchemaorg["addressLocality"] = "org.schema.Place.address.addressLocality";
13
+ ClaimsPlaceSchemaorg["extendedAddress"] = "org.schema.Place.address.extendedAddress";
14
+ ClaimsPlaceSchemaorg["postalCode"] = "org.schema.Place.address.postalCode";
15
+ ClaimsPlaceSchemaorg["streetAddress"] = "org.schema.Place.address.streetAddress";
16
+ ClaimsPlaceSchemaorg["latitude"] = "org.schema.Place.geo.latitude";
17
+ ClaimsPlaceSchemaorg["longitude"] = "org.schema.Place.geo.longitude";
18
+ ClaimsPlaceSchemaorg["elevation"] = "org.schema.Place.geo.elevation";
19
+ ClaimsPlaceSchemaorg["photoContentUrl"] = "org.schema.Place.photo.contentUrl";
20
+ ClaimsPlaceSchemaorg["amenityFeature"] = "org.schema.Place.amenityFeature";
21
+ ClaimsPlaceSchemaorg["publicAccess"] = "org.schema.Place.publicAccess";
22
+ ClaimsPlaceSchemaorg["openingHoursSpecification"] = "org.schema.Place.openingHoursSpecification";
23
+ ClaimsPlaceSchemaorg["specialOpeningHoursSpecification"] = "org.schema.Place.specialOpeningHoursSpecification";
24
+ ClaimsPlaceSchemaorg["userSelected"] = "Place.user-selected";
25
+ })(ClaimsPlaceSchemaorg || (ClaimsPlaceSchemaorg = {}));
26
+ /** Schema.org Service claims needed to join a public service to one or more Places. */
27
+ export var ClaimsDirectoryServiceSchemaorg;
28
+ (function (ClaimsDirectoryServiceSchemaorg) {
29
+ ClaimsDirectoryServiceSchemaorg["identifier"] = "org.schema.Service.identifier";
30
+ ClaimsDirectoryServiceSchemaorg["name"] = "org.schema.Service.name";
31
+ ClaimsDirectoryServiceSchemaorg["description"] = "org.schema.Service.description";
32
+ ClaimsDirectoryServiceSchemaorg["providerIdentifier"] = "org.schema.Service.provider.identifier";
33
+ ClaimsDirectoryServiceSchemaorg["placeIdentifier"] = "org.schema.Service.availableChannel.serviceLocation.identifier";
34
+ ClaimsDirectoryServiceSchemaorg["serviceType"] = "org.schema.Service.serviceType";
35
+ ClaimsDirectoryServiceSchemaorg["category"] = "org.schema.Service.category";
36
+ ClaimsDirectoryServiceSchemaorg["areaServed"] = "org.schema.Service.areaServed";
37
+ ClaimsDirectoryServiceSchemaorg["userSelected"] = "Service.user-selected";
38
+ })(ClaimsDirectoryServiceSchemaorg || (ClaimsDirectoryServiceSchemaorg = {}));
39
+ /** Schema.org Product claims for an organization's public commercial offering. */
40
+ export var ClaimsProductSchemaorg;
41
+ (function (ClaimsProductSchemaorg) {
42
+ ClaimsProductSchemaorg["identifier"] = "org.schema.Product.identifier";
43
+ ClaimsProductSchemaorg["additionalType"] = "org.schema.Product.additionalType";
44
+ ClaimsProductSchemaorg["name"] = "org.schema.Product.name";
45
+ ClaimsProductSchemaorg["description"] = "org.schema.Product.description";
46
+ ClaimsProductSchemaorg["providerIdentifier"] = "org.schema.Product.provider.identifier";
47
+ ClaimsProductSchemaorg["category"] = "org.schema.Product.category";
48
+ ClaimsProductSchemaorg["areaServed"] = "org.schema.Product.areaServed";
49
+ ClaimsProductSchemaorg["url"] = "org.schema.Product.url";
50
+ ClaimsProductSchemaorg["imageContentUrl"] = "org.schema.Product.image.contentUrl";
51
+ ClaimsProductSchemaorg["availabilityStarts"] = "org.schema.Product.offers.availabilityStarts";
52
+ ClaimsProductSchemaorg["availabilityEnds"] = "org.schema.Product.offers.availabilityEnds";
53
+ ClaimsProductSchemaorg["price"] = "org.schema.Product.offers.price";
54
+ ClaimsProductSchemaorg["priceCurrency"] = "org.schema.Product.offers.priceCurrency";
55
+ ClaimsProductSchemaorg["insuranceStatus"] = "org.schema.Product.additionalProperty.insuranceStatus";
56
+ ClaimsProductSchemaorg["insuranceCoverageType"] = "org.schema.Product.additionalProperty.insuranceCoverageType";
57
+ ClaimsProductSchemaorg["insuranceNetwork"] = "org.schema.Product.additionalProperty.insuranceNetwork";
58
+ ClaimsProductSchemaorg["userSelected"] = "Product.user-selected";
59
+ })(ClaimsProductSchemaorg || (ClaimsProductSchemaorg = {}));
60
+ /** Product-local allowlist for the experimental Place, Service and Product directory profile. */
61
+ export const DirectoryFlatClaimCatalog = Object.freeze({
62
+ Place: Object.freeze(Object.values(ClaimsPlaceSchemaorg)),
63
+ Service: Object.freeze(Object.values(ClaimsDirectoryServiceSchemaorg)),
64
+ Product: Object.freeze(Object.values(ClaimsProductSchemaorg)),
65
+ });
66
+ /** Sector-owned offering kind. It drives presentation only and grants no authority. */
67
+ export const OrganizationOfferingKinds = Object.freeze({
68
+ 'animal-care': 'Service',
69
+ 'animal-insurance': 'Product',
70
+ });
71
+ /** Stable Eclipse Dataspace Protocol 2025-1 identifiers, including current editorial errata. */
72
+ export const DataspaceProtocol2025 = Object.freeze({
73
+ release: '2025-1-err1',
74
+ context: 'https://w3id.org/dspace/2025/1/context.jsonld',
75
+ httpEndpointType: 'https://w3id.org/idsa/v4.1/HTTP',
76
+ });
77
+ const RESOURCE_ID_PATTERN = /^[A-Za-z0-9\-.]{1,64}$/;
78
+ const TIME_PATTERN = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
79
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
80
+ const DAY_NAMES = new Set(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']);
81
+ function required(value, code) {
82
+ const normalized = String(value ?? '').trim();
83
+ if (!normalized)
84
+ throw new TypeError(code);
85
+ return normalized;
86
+ }
87
+ function optional(value) {
88
+ const normalized = String(value ?? '').trim();
89
+ return normalized || undefined;
90
+ }
91
+ function optionalHttpsUrl(value, code) {
92
+ const normalized = optional(value);
93
+ if (!normalized)
94
+ return undefined;
95
+ try {
96
+ const parsed = new URL(normalized);
97
+ if (parsed.protocol !== 'https:')
98
+ throw new Error();
99
+ return parsed.toString();
100
+ }
101
+ catch {
102
+ throw new TypeError(code);
103
+ }
104
+ }
105
+ function unique(values, code) {
106
+ const result = [...new Set((values ?? []).map(value => required(value, code)))];
107
+ return Object.freeze(result);
108
+ }
109
+ function assertCoordinate(value, minimum, maximum, code) {
110
+ if (!Number.isFinite(value) || value < minimum || value > maximum)
111
+ throw new TypeError(code);
112
+ }
113
+ function assertDate(value, code) {
114
+ if (!DATE_PATTERN.test(value) || Number.isNaN(Date.parse(`${value}T00:00:00Z`)))
115
+ throw new TypeError(code);
116
+ return value;
117
+ }
118
+ function timeMinutes(value, code) {
119
+ if (!TIME_PATTERN.test(value))
120
+ throw new TypeError(code);
121
+ const [hour, minute] = value.split(':').map(Number);
122
+ return hour * 60 + minute;
123
+ }
124
+ function encodeOpeningHours(value) {
125
+ const daysOfWeek = unique(value.daysOfWeek, 'place_opening_hours_invalid');
126
+ if (!daysOfWeek.length || daysOfWeek.some(day => !DAY_NAMES.has(day)))
127
+ throw new TypeError('place_opening_hours_invalid');
128
+ if (timeMinutes(value.opens, 'place_opening_hours_invalid') >= timeMinutes(value.closes, 'place_opening_hours_invalid'))
129
+ throw new TypeError('place_opening_hours_invalid');
130
+ const validFrom = value.validFrom ? assertDate(value.validFrom, 'place_opening_hours_invalid') : undefined;
131
+ const validThrough = value.validThrough ? assertDate(value.validThrough, 'place_opening_hours_invalid') : undefined;
132
+ if (Boolean(validFrom) !== Boolean(validThrough) || (validFrom && validThrough && validFrom > validThrough))
133
+ throw new TypeError('place_opening_hours_invalid');
134
+ return JSON.stringify({ daysOfWeek, opens: value.opens, closes: value.closes, ...(validFrom ? { validFrom, validThrough } : {}) });
135
+ }
136
+ function encodeSpecialOpeningHours(value) {
137
+ const validFrom = assertDate(value.validFrom, 'place_special_opening_hours_invalid');
138
+ const validThrough = assertDate(value.validThrough, 'place_special_opening_hours_invalid');
139
+ if (validFrom > validThrough)
140
+ throw new TypeError('place_special_opening_hours_invalid');
141
+ if (value.closed)
142
+ return JSON.stringify({ validFrom, validThrough, closed: true });
143
+ if (!value.opens || !value.closes || timeMinutes(value.opens, 'place_special_opening_hours_invalid') >= timeMinutes(value.closes, 'place_special_opening_hours_invalid'))
144
+ throw new TypeError('place_special_opening_hours_invalid');
145
+ return JSON.stringify({ validFrom, validThrough, opens: value.opens, closes: value.closes });
146
+ }
147
+ /** Builds the claims-only public Place authored by an organization. */
148
+ export function buildPlaceDirectoryResource(input) {
149
+ const id = required(input.id, 'place_id_required');
150
+ if (!RESOURCE_ID_PATTERN.test(id))
151
+ throw new TypeError('place_id_invalid');
152
+ assertCoordinate(input.position.latitude, -90, 90, 'place_latitude_invalid');
153
+ assertCoordinate(input.position.longitude, -180, 180, 'place_longitude_invalid');
154
+ if (input.position.elevation !== undefined && !Number.isFinite(input.position.elevation))
155
+ throw new TypeError('place_elevation_invalid');
156
+ let photoUrl;
157
+ if (input.photoUrl) {
158
+ try {
159
+ const parsed = new URL(input.photoUrl);
160
+ if (parsed.protocol !== 'https:')
161
+ throw new Error();
162
+ photoUrl = parsed.toString();
163
+ }
164
+ catch {
165
+ throw new TypeError('place_photo_url_invalid');
166
+ }
167
+ }
168
+ const accessibilityFeatures = unique(input.accessibilityFeatures, 'place_amenity_feature_invalid');
169
+ const openingHours = Object.freeze((input.openingHours ?? []).map(encodeOpeningHours));
170
+ const specialOpeningHours = Object.freeze((input.specialOpeningHours ?? []).map(encodeSpecialOpeningHours));
171
+ const claims = {
172
+ [ClaimsPlaceSchemaorg.identifier]: id,
173
+ [ClaimsPlaceSchemaorg.ownerIdentifier]: required(input.organizationIdentifier, 'place_organization_required'),
174
+ [ClaimsPlaceSchemaorg.name]: required(input.name, 'place_name_required'),
175
+ [ClaimsPlaceSchemaorg.addressCountry]: required(input.address.addressCountry, 'place_country_required').toUpperCase(),
176
+ [ClaimsPlaceSchemaorg.postalCode]: required(input.address.postalCode, 'place_postal_code_required'),
177
+ [ClaimsPlaceSchemaorg.latitude]: input.position.latitude,
178
+ [ClaimsPlaceSchemaorg.longitude]: input.position.longitude,
179
+ [ClaimsPlaceSchemaorg.publicAccess]: input.publicAccess ?? true,
180
+ [ClaimsPlaceSchemaorg.userSelected]: input.userSelected ?? false,
181
+ };
182
+ const textValues = [
183
+ [ClaimsPlaceSchemaorg.additionalType, input.additionalType], [ClaimsPlaceSchemaorg.description, input.description],
184
+ [ClaimsPlaceSchemaorg.addressRegion, input.address.addressRegion], [ClaimsPlaceSchemaorg.addressLocality, input.address.addressLocality],
185
+ [ClaimsPlaceSchemaorg.extendedAddress, input.address.extendedAddress], [ClaimsPlaceSchemaorg.streetAddress, input.address.streetAddress],
186
+ ];
187
+ for (const [name, raw] of textValues) {
188
+ const value = optional(raw);
189
+ if (value)
190
+ claims[name] = value;
191
+ }
192
+ if (input.position.elevation !== undefined)
193
+ claims[ClaimsPlaceSchemaorg.elevation] = input.position.elevation;
194
+ if (photoUrl)
195
+ claims[ClaimsPlaceSchemaorg.photoContentUrl] = photoUrl;
196
+ if (accessibilityFeatures.length)
197
+ claims[ClaimsPlaceSchemaorg.amenityFeature] = accessibilityFeatures;
198
+ if (openingHours.length)
199
+ claims[ClaimsPlaceSchemaorg.openingHoursSpecification] = openingHours;
200
+ if (specialOpeningHours.length)
201
+ claims[ClaimsPlaceSchemaorg.specialOpeningHoursSpecification] = specialOpeningHours;
202
+ return Object.freeze({ resourceType: 'Place', id, meta: Object.freeze({ claims: Object.freeze(claims) }) });
203
+ }
204
+ /** Builds a claims-only Schema.org Service joined to its provider and public Places. */
205
+ export function buildServiceDirectoryResource(input) {
206
+ const id = required(input.id, 'service_id_required');
207
+ if (!RESOURCE_ID_PATTERN.test(id))
208
+ throw new TypeError('service_id_invalid');
209
+ const placeIdentifiers = unique(input.placeIdentifiers, 'service_place_required');
210
+ const serviceTypes = unique(input.serviceTypes, 'service_type_required');
211
+ if (!placeIdentifiers.length)
212
+ throw new TypeError('service_place_required');
213
+ if (!serviceTypes.length)
214
+ throw new TypeError('service_type_required');
215
+ const categories = unique(input.categories, 'service_category_invalid');
216
+ const areaServed = unique(input.areaServed, 'service_area_invalid');
217
+ const claims = {
218
+ [ClaimsDirectoryServiceSchemaorg.identifier]: id,
219
+ [ClaimsDirectoryServiceSchemaorg.providerIdentifier]: required(input.organizationIdentifier, 'service_organization_required'),
220
+ [ClaimsDirectoryServiceSchemaorg.placeIdentifier]: placeIdentifiers,
221
+ [ClaimsDirectoryServiceSchemaorg.name]: required(input.name, 'service_name_required'),
222
+ [ClaimsDirectoryServiceSchemaorg.serviceType]: serviceTypes,
223
+ [ClaimsDirectoryServiceSchemaorg.userSelected]: input.userSelected ?? false,
224
+ };
225
+ const description = optional(input.description);
226
+ if (description)
227
+ claims[ClaimsDirectoryServiceSchemaorg.description] = description;
228
+ if (categories.length)
229
+ claims[ClaimsDirectoryServiceSchemaorg.category] = categories;
230
+ if (areaServed.length)
231
+ claims[ClaimsDirectoryServiceSchemaorg.areaServed] = areaServed;
232
+ return Object.freeze({ resourceType: 'Service', id, meta: Object.freeze({ claims: Object.freeze(claims) }) });
233
+ }
234
+ /** Builds a claims-only Schema.org Product; insurance details remain a public plan definition, not personal Coverage. */
235
+ export function buildProductDirectoryResource(input) {
236
+ const id = required(input.id, 'product_id_required');
237
+ if (!RESOURCE_ID_PATTERN.test(id))
238
+ throw new TypeError('product_id_invalid');
239
+ const productType = required(input.productType, 'product_type_required');
240
+ const categories = unique(input.categories, 'product_category_invalid');
241
+ const areaServed = unique(input.areaServed, 'product_area_invalid');
242
+ const url = optionalHttpsUrl(input.url, 'product_url_invalid');
243
+ const imageUrl = optionalHttpsUrl(input.imageUrl, 'product_image_url_invalid');
244
+ const availableFrom = input.availableFrom ? assertDate(input.availableFrom, 'product_period_invalid') : undefined;
245
+ const availableThrough = input.availableThrough ? assertDate(input.availableThrough, 'product_period_invalid') : undefined;
246
+ if (Boolean(availableFrom) !== Boolean(availableThrough) || (availableFrom && availableThrough && availableFrom > availableThrough))
247
+ throw new TypeError('product_period_invalid');
248
+ if (input.price !== undefined && (!Number.isFinite(input.price) || input.price < 0))
249
+ throw new TypeError('product_price_invalid');
250
+ const priceCurrency = optional(input.priceCurrency)?.toUpperCase();
251
+ if ((input.price === undefined) !== (priceCurrency === undefined) || (priceCurrency && !/^[A-Z]{3}$/.test(priceCurrency)))
252
+ throw new TypeError('product_price_invalid');
253
+ if (productType === 'insurance-plan' && !input.insurance)
254
+ throw new TypeError('insurance_product_profile_required');
255
+ const coverageTypes = unique(input.insurance?.coverageTypes, 'insurance_coverage_type_invalid');
256
+ if (input.insurance && !coverageTypes.length)
257
+ throw new TypeError('insurance_coverage_type_invalid');
258
+ const networks = unique(input.insurance?.networks, 'insurance_network_invalid');
259
+ const claims = {
260
+ [ClaimsProductSchemaorg.identifier]: id,
261
+ [ClaimsProductSchemaorg.providerIdentifier]: required(input.organizationIdentifier, 'product_organization_required'),
262
+ [ClaimsProductSchemaorg.name]: required(input.name, 'product_name_required'),
263
+ [ClaimsProductSchemaorg.additionalType]: productType,
264
+ [ClaimsProductSchemaorg.userSelected]: input.userSelected ?? false,
265
+ };
266
+ const description = optional(input.description);
267
+ if (description)
268
+ claims[ClaimsProductSchemaorg.description] = description;
269
+ if (categories.length)
270
+ claims[ClaimsProductSchemaorg.category] = categories;
271
+ if (areaServed.length)
272
+ claims[ClaimsProductSchemaorg.areaServed] = areaServed;
273
+ if (url)
274
+ claims[ClaimsProductSchemaorg.url] = url;
275
+ if (imageUrl)
276
+ claims[ClaimsProductSchemaorg.imageContentUrl] = imageUrl;
277
+ if (availableFrom)
278
+ claims[ClaimsProductSchemaorg.availabilityStarts] = availableFrom;
279
+ if (availableThrough)
280
+ claims[ClaimsProductSchemaorg.availabilityEnds] = availableThrough;
281
+ if (input.price !== undefined)
282
+ claims[ClaimsProductSchemaorg.price] = input.price;
283
+ if (priceCurrency)
284
+ claims[ClaimsProductSchemaorg.priceCurrency] = priceCurrency;
285
+ if (input.insurance)
286
+ claims[ClaimsProductSchemaorg.insuranceStatus] = input.insurance.status;
287
+ if (coverageTypes.length)
288
+ claims[ClaimsProductSchemaorg.insuranceCoverageType] = coverageTypes;
289
+ if (networks.length)
290
+ claims[ClaimsProductSchemaorg.insuranceNetwork] = networks;
291
+ return Object.freeze({ resourceType: 'Product', id, meta: Object.freeze({ claims: Object.freeze(claims) }) });
292
+ }
293
+ /** Rejects native fields and converts a directory resource into deterministic array-valued claims. */
294
+ export function normalizeDirectoryFlatClaimsResource(input) {
295
+ if (!input || typeof input !== 'object' || Array.isArray(input))
296
+ throw new TypeError('directory_flat_resource_invalid');
297
+ const value = input;
298
+ if ((value.resourceType !== 'Place' && value.resourceType !== 'Service' && value.resourceType !== 'Product') || Object.keys(value).some(key => !['resourceType', 'id', 'meta'].includes(key)))
299
+ throw new TypeError('directory_flat_resource_invalid');
300
+ const resourceType = value.resourceType;
301
+ const id = required(value.id, 'directory_flat_resource_invalid');
302
+ if (!RESOURCE_ID_PATTERN.test(id))
303
+ throw new TypeError('directory_flat_resource_invalid');
304
+ const rawClaims = value.meta?.claims;
305
+ if (!rawClaims || typeof rawClaims !== 'object' || Array.isArray(rawClaims))
306
+ throw new TypeError('directory_flat_resource_invalid');
307
+ const allowed = new Set(DirectoryFlatClaimCatalog[resourceType]);
308
+ const claims = {};
309
+ for (const [name, raw] of Object.entries(rawClaims)) {
310
+ if (!allowed.has(name))
311
+ throw new TypeError('directory_flat_claim_invalid');
312
+ const values = (Array.isArray(raw) ? raw : [raw]).map(item => String(item).trim());
313
+ if (!values.length || values.some(item => !item))
314
+ throw new TypeError('directory_flat_claim_invalid');
315
+ claims[name] = Object.freeze([...new Set(values)]);
316
+ }
317
+ return Object.freeze({ resourceType, id, meta: Object.freeze({ claims: Object.freeze(claims) }) });
318
+ }
319
+ function values(resource, name) {
320
+ const raw = resource.meta.claims[name];
321
+ return (Array.isArray(raw) ? raw : raw === undefined ? [] : [raw]).map(String);
322
+ }
323
+ function first(resource, name) {
324
+ return values(resource, name)[0];
325
+ }
326
+ function parseJson(value, code) {
327
+ try {
328
+ return JSON.parse(value);
329
+ }
330
+ catch {
331
+ throw new TypeError(code);
332
+ }
333
+ }
334
+ /** Explicitly projects one neutral Place to native FHIR R5 Location for export or interop. */
335
+ export function projectPlaceToFhirR5Location(place) {
336
+ if (place.resourceType !== 'Place')
337
+ throw new TypeError('place_resource_required');
338
+ const latitude = Number(first(place, ClaimsPlaceSchemaorg.latitude));
339
+ const longitude = Number(first(place, ClaimsPlaceSchemaorg.longitude));
340
+ const postalCode = required(first(place, ClaimsPlaceSchemaorg.postalCode), 'place_postal_code_required');
341
+ const owner = required(first(place, ClaimsPlaceSchemaorg.ownerIdentifier), 'place_organization_required');
342
+ const opening = values(place, ClaimsPlaceSchemaorg.openingHoursSpecification).map(value => parseJson(value, 'place_opening_hours_invalid'));
343
+ const special = values(place, ClaimsPlaceSchemaorg.specialOpeningHoursSpecification).map(value => parseJson(value, 'place_special_opening_hours_invalid'));
344
+ const address = {
345
+ country: first(place, ClaimsPlaceSchemaorg.addressCountry), state: first(place, ClaimsPlaceSchemaorg.addressRegion),
346
+ city: first(place, ClaimsPlaceSchemaorg.addressLocality), postalCode,
347
+ ...(first(place, ClaimsPlaceSchemaorg.streetAddress) ? { line: [first(place, ClaimsPlaceSchemaorg.streetAddress)] } : {}),
348
+ };
349
+ const availableTime = opening.map(period => ({
350
+ daysOfWeek: period.daysOfWeek.map(day => day.slice(0, 3).toLowerCase()),
351
+ availableStartTime: period.opens, availableEndTime: period.closes,
352
+ }));
353
+ const notAvailableTime = special.filter(period => period.closed).map(period => ({
354
+ description: 'Special closure', during: { start: period.validFrom, end: period.validThrough },
355
+ }));
356
+ return Object.freeze({
357
+ resourceType: 'Location', id: place.id, status: 'active', mode: 'instance',
358
+ name: first(place, ClaimsPlaceSchemaorg.name), description: first(place, ClaimsPlaceSchemaorg.description),
359
+ address, position: { latitude, longitude, ...(first(place, ClaimsPlaceSchemaorg.elevation) ? { altitude: Number(first(place, ClaimsPlaceSchemaorg.elevation)) } : {}) },
360
+ managingOrganization: { reference: owner },
361
+ characteristic: values(place, ClaimsPlaceSchemaorg.amenityFeature).map(code => ({ coding: [{ code }] })),
362
+ hoursOfOperation: [{ availableTime, notAvailableTime }],
363
+ meta: { claims: {
364
+ 'Location.identifier': place.id, 'Location.status': 'active', 'Location.name': first(place, ClaimsPlaceSchemaorg.name),
365
+ 'Location.address-postalcode': postalCode, 'Location.near': `${latitude}|${longitude}`, 'Location.organization': owner,
366
+ ...(values(place, ClaimsPlaceSchemaorg.amenityFeature).length ? { 'Location.characteristic': values(place, ClaimsPlaceSchemaorg.amenityFeature) } : {}),
367
+ } },
368
+ });
369
+ }
370
+ /** Explicitly projects a neutral Service and its Places to native FHIR R5 HealthcareService. */
371
+ export function projectServiceToFhirR5HealthcareService(service, input) {
372
+ if (service.resourceType !== 'Service')
373
+ throw new TypeError('service_resource_required');
374
+ const placeIdentifiers = values(service, ClaimsDirectoryServiceSchemaorg.placeIdentifier);
375
+ const places = placeIdentifiers.map(identifier => {
376
+ const place = input.places.find(candidate => candidate.resourceType === 'Place' && candidate.id === identifier);
377
+ if (!place)
378
+ throw new TypeError('service_place_not_found');
379
+ return place;
380
+ });
381
+ const serviceTypes = values(service, ClaimsDirectoryServiceSchemaorg.serviceType);
382
+ const categories = values(service, ClaimsDirectoryServiceSchemaorg.category);
383
+ const organization = required(first(service, ClaimsDirectoryServiceSchemaorg.providerIdentifier), 'service_organization_required');
384
+ const photoUrl = places.map(place => first(place, ClaimsPlaceSchemaorg.photoContentUrl)).find(Boolean);
385
+ const concept = (code) => ({ coding: [{ code }] });
386
+ return Object.freeze({
387
+ resourceType: 'HealthcareService', id: service.id, active: true, providedBy: { reference: organization },
388
+ name: first(service, ClaimsDirectoryServiceSchemaorg.name), comment: first(service, ClaimsDirectoryServiceSchemaorg.description),
389
+ category: categories.map(concept), type: serviceTypes.map(concept),
390
+ location: places.map(place => ({ reference: `Location/${place.id}` })),
391
+ ...(photoUrl ? { photo: { url: photoUrl } } : {}),
392
+ meta: { claims: {
393
+ 'HealthcareService.identifier': service.id, 'HealthcareService.organization': organization,
394
+ 'HealthcareService.name': first(service, ClaimsDirectoryServiceSchemaorg.name),
395
+ 'HealthcareService.location': places.map(place => `Location/${place.id}`),
396
+ 'HealthcareService.service-type': serviceTypes,
397
+ ...(categories.length ? { 'HealthcareService.service-category': categories } : {}),
398
+ } },
399
+ });
400
+ }
401
+ /** Explicitly projects a public insurance Product definition to FHIR R5 InsurancePlan. */
402
+ export function projectProductToFhirR5InsurancePlan(product) {
403
+ if (product.resourceType !== 'Product')
404
+ throw new TypeError('product_resource_required');
405
+ if (first(product, ClaimsProductSchemaorg.additionalType) !== 'insurance-plan')
406
+ throw new TypeError('insurance_product_required');
407
+ const organization = required(first(product, ClaimsProductSchemaorg.providerIdentifier), 'product_organization_required');
408
+ const status = required(first(product, ClaimsProductSchemaorg.insuranceStatus), 'insurance_product_profile_required');
409
+ const coverageTypes = values(product, ClaimsProductSchemaorg.insuranceCoverageType);
410
+ if (!coverageTypes.length)
411
+ throw new TypeError('insurance_coverage_type_invalid');
412
+ const networks = values(product, ClaimsProductSchemaorg.insuranceNetwork);
413
+ const areaServed = values(product, ClaimsProductSchemaorg.areaServed);
414
+ const name = required(first(product, ClaimsProductSchemaorg.name), 'product_name_required');
415
+ const start = first(product, ClaimsProductSchemaorg.availabilityStarts);
416
+ const end = first(product, ClaimsProductSchemaorg.availabilityEnds);
417
+ const concept = (code) => ({ coding: [{ code }] });
418
+ return Object.freeze({
419
+ resourceType: 'InsurancePlan', id: product.id,
420
+ identifier: [{ value: product.id }], status, type: [concept('insurance-plan')], name,
421
+ ...(start && end ? { period: { start, end } } : {}),
422
+ ownedBy: { reference: organization },
423
+ coverageArea: areaServed.map(reference => ({ reference })),
424
+ network: networks.map(reference => ({ reference })),
425
+ coverage: coverageTypes.map(type => ({ type: concept(type), network: networks.map(reference => ({ reference })), benefit: [{ type: concept(type) }] })),
426
+ meta: { claims: {
427
+ 'InsurancePlan.identifier': product.id,
428
+ 'InsurancePlan.status': status,
429
+ 'InsurancePlan.name': name,
430
+ 'InsurancePlan.owned-by': organization,
431
+ 'InsurancePlan.type': 'insurance-plan',
432
+ } },
433
+ });
434
+ }
435
+ /** Builds the Schema.org JSON-LD OfferCatalog presented by one organization. */
436
+ export function buildOrganizationOfferCatalog(input) {
437
+ const expectedKind = OrganizationOfferingKinds[input.sector];
438
+ const organizationIdentifier = required(input.organizationIdentifier, 'organization_identifier_required');
439
+ const itemListElement = input.offerings.map(offering => {
440
+ if (offering.resourceType !== expectedKind)
441
+ throw new TypeError('organization_offering_kind_invalid');
442
+ const isService = offering.resourceType === 'Service';
443
+ const nameClaim = isService ? ClaimsDirectoryServiceSchemaorg.name : ClaimsProductSchemaorg.name;
444
+ const providerClaim = isService ? ClaimsDirectoryServiceSchemaorg.providerIdentifier : ClaimsProductSchemaorg.providerIdentifier;
445
+ if (first(offering, providerClaim) !== organizationIdentifier)
446
+ throw new TypeError('organization_offering_owner_invalid');
447
+ return Object.freeze({
448
+ '@type': 'Offer',
449
+ itemOffered: Object.freeze({
450
+ '@type': offering.resourceType,
451
+ identifier: offering.id,
452
+ name: required(first(offering, nameClaim), 'organization_offering_name_required'),
453
+ }),
454
+ });
455
+ });
456
+ return Object.freeze({
457
+ '@context': 'https://schema.org', '@type': 'OfferCatalog',
458
+ identifier: required(input.id, 'organization_catalog_id_required'),
459
+ name: required(input.name, 'organization_catalog_name_required'),
460
+ offeredBy: Object.freeze({ '@type': 'Organization', identifier: organizationIdentifier }),
461
+ numberOfItems: itemListElement.length,
462
+ itemListElement: Object.freeze(itemListElement),
463
+ });
464
+ }
465
+ /** Advertises a transferable catalog snapshot as a DSP Dataset with an ODRL use Offer. */
466
+ export function buildDataspaceCatalogAdvertisement(input) {
467
+ const endpointURL = optionalHttpsUrl(input.endpointUrl, 'dataspace_endpoint_url_invalid');
468
+ if (!endpointURL)
469
+ throw new TypeError('dataspace_endpoint_url_invalid');
470
+ const dataServiceId = required(input.dataServiceId, 'dataspace_data_service_id_required');
471
+ const datasetId = required(input.datasetId, 'dataspace_dataset_id_required');
472
+ const service = Object.freeze({ '@id': dataServiceId, '@type': 'DataService', endpointURL });
473
+ const dataset = Object.freeze({
474
+ '@id': datasetId,
475
+ '@type': 'Dataset',
476
+ hasPolicy: Object.freeze([Object.freeze({
477
+ '@id': required(input.policyId, 'dataspace_policy_id_required'),
478
+ '@type': 'Offer',
479
+ permission: Object.freeze([Object.freeze({ action: 'use' })]),
480
+ })]),
481
+ distribution: Object.freeze([Object.freeze({
482
+ '@type': 'Distribution',
483
+ format: required(input.distributionFormat, 'dataspace_distribution_format_required'),
484
+ accessService: dataServiceId,
485
+ })]),
486
+ });
487
+ return Object.freeze({
488
+ '@context': Object.freeze([DataspaceProtocol2025.context]),
489
+ '@id': required(input.catalogId, 'dataspace_catalog_id_required'),
490
+ '@type': 'Catalog',
491
+ participantId: required(input.participantId, 'dataspace_participant_id_required'),
492
+ service: Object.freeze([service]),
493
+ dataset: Object.freeze([dataset]),
494
+ });
495
+ }
496
+ /** Builds the DSP DataAddress exchanged only after agreement negotiation starts a transfer. */
497
+ export function buildDataspaceDataAddress(input) {
498
+ const endpointType = required(input.endpointType, 'dataspace_data_address_invalid');
499
+ if (!endpointType.startsWith('https://'))
500
+ throw new TypeError('dataspace_data_address_invalid');
501
+ const endpoint = optionalHttpsUrl(input.endpoint, 'dataspace_data_address_invalid');
502
+ const endpointProperties = (input.endpointProperties ?? []).map(property => Object.freeze({
503
+ '@type': 'EndpointProperty',
504
+ name: required(property.name, 'dataspace_data_address_invalid'),
505
+ value: required(property.value, 'dataspace_data_address_invalid'),
506
+ }));
507
+ return Object.freeze({
508
+ '@type': 'DataAddress', endpointType,
509
+ ...(endpoint ? { endpoint } : {}),
510
+ ...(endpointProperties.length ? { endpointProperties: Object.freeze(endpointProperties) } : {}),
511
+ });
512
+ }
513
+ function normalizedPostal(value) { return value.replace(/[^A-Za-z0-9]/g, '').toUpperCase(); }
514
+ function distanceKm(left, right) {
515
+ const radians = (value) => value * Math.PI / 180;
516
+ const lat = radians(right.latitude - left.latitude);
517
+ const lon = radians(right.longitude - left.longitude);
518
+ const a = Math.sin(lat / 2) ** 2 + Math.cos(radians(left.latitude)) * Math.cos(radians(right.latitude)) * Math.sin(lon / 2) ** 2;
519
+ return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
520
+ }
521
+ /** Joins and filters a public clinic directory by postal code, service and optional geographic radius. */
522
+ export function searchClinicMarketplace(input) {
523
+ const postalCode = input.postalCode ? normalizedPostal(required(input.postalCode, 'marketplace_postal_code_invalid')) : undefined;
524
+ const requestedTypes = new Set(unique(input.serviceTypes, 'marketplace_service_type_invalid'));
525
+ if (input.near) {
526
+ assertCoordinate(input.near.latitude, -90, 90, 'marketplace_latitude_invalid');
527
+ assertCoordinate(input.near.longitude, -180, 180, 'marketplace_longitude_invalid');
528
+ if (!Number.isFinite(input.near.radiusKm) || input.near.radiusKm <= 0)
529
+ throw new TypeError('marketplace_radius_invalid');
530
+ }
531
+ const results = [];
532
+ for (const place of input.places) {
533
+ if (place.resourceType !== 'Place')
534
+ throw new TypeError('place_resource_required');
535
+ if (postalCode && normalizedPostal(String(first(place, ClaimsPlaceSchemaorg.postalCode) || '')) !== postalCode)
536
+ continue;
537
+ const services = input.services.filter(service => service.resourceType === 'Service'
538
+ && values(service, ClaimsDirectoryServiceSchemaorg.placeIdentifier).includes(place.id)
539
+ && (!requestedTypes.size || values(service, ClaimsDirectoryServiceSchemaorg.serviceType).some(type => requestedTypes.has(type))));
540
+ if (!services.length)
541
+ continue;
542
+ const distance = input.near ? distanceKm(input.near, {
543
+ latitude: Number(first(place, ClaimsPlaceSchemaorg.latitude)), longitude: Number(first(place, ClaimsPlaceSchemaorg.longitude)),
544
+ }) : undefined;
545
+ if (input.near && (distance === undefined || distance > input.near.radiusKm))
546
+ continue;
547
+ results.push(Object.freeze({ place, services: Object.freeze(services), ...(distance === undefined ? {} : { distanceKm: distance }) }));
548
+ }
549
+ return Object.freeze(results.sort((left, right) => (left.distanceKm ?? 0) - (right.distanceKm ?? 0) || left.place.id.localeCompare(right.place.id)));
550
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-data-utils-ts",
3
- "version": "0.5.20",
3
+ "version": "0.5.22",
4
4
  "description": "Browser-safe governed VetChain data contracts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",
@@ -68,6 +68,10 @@
68
68
  "types": "./dist/payment.d.ts",
69
69
  "default": "./dist/payment.js"
70
70
  },
71
+ "./place-service-directory": {
72
+ "types": "./dist/place-service-directory.d.ts",
73
+ "default": "./dist/place-service-directory.js"
74
+ },
71
75
  "./research-study": {
72
76
  "types": "./dist/research-study.d.ts",
73
77
  "default": "./dist/research-study.js"