vet-data-utils-ts 0.6.0 → 0.7.1
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 +23 -3
- package/dist/animal-identity-assurance.d.ts +182 -0
- package/dist/animal-identity-assurance.js +231 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/place-service-directory.d.ts +7 -42
- package/dist/place-service-directory.js +11 -50
- package/dist/scheduling.d.ts +17 -11
- package/dist/scheduling.js +49 -42
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -26,9 +26,10 @@ The product-local `place-service-directory` export models persistent public
|
|
|
26
26
|
departments as claims-only Schema.org `Organization` resources, their sites as
|
|
27
27
|
`Place`, and their offerings as `Service` or `Product`. A department has one
|
|
28
28
|
parent legal organization, one or more Places and several offered Services.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
32
33
|
publishes services; an insurance organization publishes products in a
|
|
33
34
|
Schema.org `OfferCatalog`. Logos, images, accessibility, coordinates, address
|
|
34
35
|
and opening-hours specifications remain neutral indexed claims. Native FHIR R5
|
|
@@ -126,6 +127,25 @@ and applies weekly or monthly-ordinal exceptions that restrict or extend one or
|
|
|
126
127
|
more consultation Locations. It converts clinic-local times through an IANA
|
|
127
128
|
time zone, including daylight-saving changes.
|
|
128
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
|
+
|
|
129
149
|
The official searchable facts remain resource-qualified flat claims. The
|
|
130
150
|
exported `VeterinarySlotSearchParameters` catalogue identifies the supported
|
|
131
151
|
FHIR R5 Slot query codes; generated resources include `Slot.identifier`,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export declare const VeterinaryAnimalIdentityCredentialTypes: Readonly<{
|
|
2
|
+
Assurance: "VeterinaryAnimalIdentityAssuranceCredential";
|
|
3
|
+
}>;
|
|
4
|
+
export type VeterinaryAnimalControllerRelationship = 'owner' | 'caregiver';
|
|
5
|
+
export type VeterinaryIdentityDocumentDigest = Readonly<{
|
|
6
|
+
contentType: 'application/pdf' | 'image/jpeg' | 'image/png';
|
|
7
|
+
/** SHA3-384 multihash encoded as base58btc. Raw document bytes never belong in the credential. */
|
|
8
|
+
sha3_384: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type VeterinaryAnimalIdentityAssuranceCredential = Readonly<{
|
|
11
|
+
'@context': readonly ['https://www.w3.org/ns/credentials/v2', 'https://schema.org'];
|
|
12
|
+
id: string;
|
|
13
|
+
type: readonly ['VerifiableCredential', 'VeterinaryAnimalIdentityAssuranceCredential'];
|
|
14
|
+
issuer: string;
|
|
15
|
+
validFrom: string;
|
|
16
|
+
validUntil: string;
|
|
17
|
+
credentialSubject: Readonly<{
|
|
18
|
+
id: string;
|
|
19
|
+
'@type': 'Person';
|
|
20
|
+
holdsAnimalIdentity: Readonly<{
|
|
21
|
+
id: string;
|
|
22
|
+
'@type': 'Animal';
|
|
23
|
+
name: string;
|
|
24
|
+
species: Readonly<{
|
|
25
|
+
'@type': 'Taxon';
|
|
26
|
+
taxonRank: 'species';
|
|
27
|
+
identifier: string;
|
|
28
|
+
}>;
|
|
29
|
+
identifier: Readonly<{
|
|
30
|
+
'@type': 'PropertyValue';
|
|
31
|
+
propertyID: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}>;
|
|
34
|
+
}>;
|
|
35
|
+
relationshipToAnimal: VeterinaryAnimalControllerRelationship;
|
|
36
|
+
verified_claims: Readonly<{
|
|
37
|
+
verification: Readonly<{
|
|
38
|
+
trust_framework: 'veterinarian_in_person_document_check';
|
|
39
|
+
time: string;
|
|
40
|
+
verification_process: string;
|
|
41
|
+
evidence: readonly [
|
|
42
|
+
Readonly<{
|
|
43
|
+
type: 'document';
|
|
44
|
+
method: 'physical_in_person_check';
|
|
45
|
+
verifier: Readonly<{
|
|
46
|
+
organization: string;
|
|
47
|
+
professional: string;
|
|
48
|
+
role: 'ISCO-08|2250';
|
|
49
|
+
}>;
|
|
50
|
+
time: string;
|
|
51
|
+
check_details: readonly [Readonly<{
|
|
52
|
+
check_method: 'physical_in_person_check';
|
|
53
|
+
time: string;
|
|
54
|
+
}>];
|
|
55
|
+
attachments: Readonly<{
|
|
56
|
+
content_type: string;
|
|
57
|
+
digest: Readonly<{
|
|
58
|
+
alg: 'sha3-384';
|
|
59
|
+
value: string;
|
|
60
|
+
}>;
|
|
61
|
+
}>;
|
|
62
|
+
}>
|
|
63
|
+
];
|
|
64
|
+
}>;
|
|
65
|
+
claims: Readonly<{
|
|
66
|
+
name: string;
|
|
67
|
+
identifier: Readonly<{
|
|
68
|
+
type: string;
|
|
69
|
+
value: string;
|
|
70
|
+
country: string;
|
|
71
|
+
}>;
|
|
72
|
+
}>;
|
|
73
|
+
}>;
|
|
74
|
+
}>;
|
|
75
|
+
evidence: readonly [
|
|
76
|
+
Readonly<{
|
|
77
|
+
type: 'VeterinaryAnimalIdentityEvidence';
|
|
78
|
+
verifier: string;
|
|
79
|
+
time: string;
|
|
80
|
+
content_type: string;
|
|
81
|
+
digest: Readonly<{
|
|
82
|
+
alg: 'sha3-384';
|
|
83
|
+
value: string;
|
|
84
|
+
}>;
|
|
85
|
+
}>,
|
|
86
|
+
Readonly<{
|
|
87
|
+
type: 'AnimalControllerRelationshipEvidence';
|
|
88
|
+
relationship: VeterinaryAnimalControllerRelationship;
|
|
89
|
+
verifier: string;
|
|
90
|
+
time: string;
|
|
91
|
+
content_type: string;
|
|
92
|
+
digest: Readonly<{
|
|
93
|
+
alg: 'sha3-384';
|
|
94
|
+
value: string;
|
|
95
|
+
}>;
|
|
96
|
+
}>
|
|
97
|
+
];
|
|
98
|
+
credentialStatus: Readonly<{
|
|
99
|
+
id: string;
|
|
100
|
+
type: 'SimpleCredentialStatus2026';
|
|
101
|
+
}>;
|
|
102
|
+
proof?: unknown;
|
|
103
|
+
}>;
|
|
104
|
+
export type VeterinaryAnimalIdentityAssuranceInput = Readonly<{
|
|
105
|
+
credentialId: string;
|
|
106
|
+
issuerDid: string;
|
|
107
|
+
veterinarianOrganizationId: string;
|
|
108
|
+
veterinarianRole: 'ISCO-08|2250';
|
|
109
|
+
holderDid: string;
|
|
110
|
+
validFrom: string;
|
|
111
|
+
validUntil: string;
|
|
112
|
+
animal: Readonly<{
|
|
113
|
+
id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
speciesTaxonomyId: string;
|
|
116
|
+
identifier: Readonly<{
|
|
117
|
+
system: string;
|
|
118
|
+
value: string;
|
|
119
|
+
}>;
|
|
120
|
+
}>;
|
|
121
|
+
controller: Readonly<{
|
|
122
|
+
id: string;
|
|
123
|
+
legalName: string;
|
|
124
|
+
identifier: Readonly<{
|
|
125
|
+
type: string;
|
|
126
|
+
value: string;
|
|
127
|
+
country: string;
|
|
128
|
+
}>;
|
|
129
|
+
}>;
|
|
130
|
+
relationship: VeterinaryAnimalControllerRelationship;
|
|
131
|
+
evidence: Readonly<{
|
|
132
|
+
animalDocument: VeterinaryIdentityDocumentDigest;
|
|
133
|
+
controllerDocument: VeterinaryIdentityDocumentDigest;
|
|
134
|
+
relationshipDocument: VeterinaryIdentityDocumentDigest;
|
|
135
|
+
checkedAt: string;
|
|
136
|
+
}>;
|
|
137
|
+
}>;
|
|
138
|
+
/**
|
|
139
|
+
* Builds the unsigned W3C VC 2.0 payload attested by a veterinarian.
|
|
140
|
+
*
|
|
141
|
+
* OpenID Identity Assurance `verified_claims` is deliberately scoped to the
|
|
142
|
+
* natural person. Animal identity and the owner/caregiver relationship use
|
|
143
|
+
* separate veterinary evidence entries. All physical files are hash-only.
|
|
144
|
+
*/
|
|
145
|
+
export declare function buildVeterinaryAnimalIdentityAssuranceCredential(input: VeterinaryAnimalIdentityAssuranceInput): VeterinaryAnimalIdentityAssuranceCredential;
|
|
146
|
+
/** Validates the governed credential shape before wallet storage or presentation. */
|
|
147
|
+
export declare function parseVeterinaryAnimalIdentityAssuranceCredential(value: unknown): VeterinaryAnimalIdentityAssuranceCredential;
|
|
148
|
+
/** Builds the bounded OpenID4VP 1.0 request used by airline, police and insurer verifiers. */
|
|
149
|
+
export declare function buildVeterinaryAnimalIdentityOpenId4VpRequest(input: Readonly<{
|
|
150
|
+
clientId: string;
|
|
151
|
+
responseUri: string;
|
|
152
|
+
nonce: string;
|
|
153
|
+
state: string;
|
|
154
|
+
animalId: string;
|
|
155
|
+
}>): Readonly<{
|
|
156
|
+
client_id: string;
|
|
157
|
+
response_uri: string;
|
|
158
|
+
response_type: "vp_token";
|
|
159
|
+
response_mode: "direct_post";
|
|
160
|
+
nonce: string;
|
|
161
|
+
state: string;
|
|
162
|
+
presentation_definition: Readonly<{
|
|
163
|
+
id: `vetchain-animal-identity-${string}`;
|
|
164
|
+
input_descriptors: readonly Readonly<{
|
|
165
|
+
id: "veterinary-animal-identity-assurance";
|
|
166
|
+
format: Readonly<{
|
|
167
|
+
jwt_vc_json: Readonly<{
|
|
168
|
+
alg: readonly string[];
|
|
169
|
+
}>;
|
|
170
|
+
}>;
|
|
171
|
+
constraints: Readonly<{
|
|
172
|
+
fields: readonly Readonly<{
|
|
173
|
+
path: readonly string[];
|
|
174
|
+
filter: Readonly<{
|
|
175
|
+
type: "string";
|
|
176
|
+
const: string;
|
|
177
|
+
}>;
|
|
178
|
+
}>[];
|
|
179
|
+
}>;
|
|
180
|
+
}>[];
|
|
181
|
+
}>;
|
|
182
|
+
}>;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
export const VeterinaryAnimalIdentityCredentialTypes = Object.freeze({
|
|
2
|
+
Assurance: 'VeterinaryAnimalIdentityAssuranceCredential',
|
|
3
|
+
});
|
|
4
|
+
function record(value) {
|
|
5
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
6
|
+
}
|
|
7
|
+
function firstRecord(value) {
|
|
8
|
+
return Array.isArray(value) ? value.map(record).find(Boolean) : undefined;
|
|
9
|
+
}
|
|
10
|
+
function required(value, error) {
|
|
11
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
12
|
+
if (!text)
|
|
13
|
+
throw new TypeError(error);
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
function did(value, error) {
|
|
17
|
+
const text = required(value, error);
|
|
18
|
+
if (!text.startsWith('did:'))
|
|
19
|
+
throw new TypeError(error);
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
function instant(value, error) {
|
|
23
|
+
const text = required(value, error);
|
|
24
|
+
if (!Number.isFinite(Date.parse(text)))
|
|
25
|
+
throw new TypeError(error);
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
function digest(value, error) {
|
|
29
|
+
const contentType = required(value?.contentType, error);
|
|
30
|
+
if (!['application/pdf', 'image/jpeg', 'image/png'].includes(contentType))
|
|
31
|
+
throw new TypeError(error);
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
contentType: contentType,
|
|
34
|
+
sha3_384: required(value?.sha3_384, error),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Builds the unsigned W3C VC 2.0 payload attested by a veterinarian.
|
|
39
|
+
*
|
|
40
|
+
* OpenID Identity Assurance `verified_claims` is deliberately scoped to the
|
|
41
|
+
* natural person. Animal identity and the owner/caregiver relationship use
|
|
42
|
+
* separate veterinary evidence entries. All physical files are hash-only.
|
|
43
|
+
*/
|
|
44
|
+
export function buildVeterinaryAnimalIdentityAssuranceCredential(input) {
|
|
45
|
+
const credentialId = required(input.credentialId, 'animal_identity_credential_id_required');
|
|
46
|
+
const issuerDid = did(input.issuerDid, 'animal_identity_credential_issuer_invalid');
|
|
47
|
+
const holderDid = did(input.holderDid, 'animal_identity_credential_holder_invalid');
|
|
48
|
+
const controllerId = did(input.controller?.id, 'animal_identity_controller_id_invalid');
|
|
49
|
+
if (holderDid !== controllerId)
|
|
50
|
+
throw new TypeError('holder_controller_identity_mismatch');
|
|
51
|
+
const animalId = did(input.animal?.id, 'animal_identity_subject_invalid');
|
|
52
|
+
const validFrom = instant(input.validFrom, 'animal_identity_credential_valid_from_invalid');
|
|
53
|
+
const validUntil = instant(input.validUntil, 'animal_identity_credential_valid_until_invalid');
|
|
54
|
+
if (Date.parse(validFrom) > Date.parse(validUntil))
|
|
55
|
+
throw new TypeError('animal_identity_credential_validity_invalid');
|
|
56
|
+
if (input.relationship !== 'owner' && input.relationship !== 'caregiver') {
|
|
57
|
+
throw new TypeError('animal_controller_relationship_invalid');
|
|
58
|
+
}
|
|
59
|
+
if (input.veterinarianRole !== 'ISCO-08|2250')
|
|
60
|
+
throw new TypeError('veterinarian_role_invalid');
|
|
61
|
+
const organization = required(input.veterinarianOrganizationId, 'veterinarian_organization_required');
|
|
62
|
+
const checkedAt = instant(input.evidence?.checkedAt, 'animal_identity_evidence_time_invalid');
|
|
63
|
+
const animalDocument = digest(input.evidence?.animalDocument, 'animal_identity_evidence_digest_required');
|
|
64
|
+
const controllerDocument = digest(input.evidence?.controllerDocument, 'controller_identity_evidence_digest_required');
|
|
65
|
+
const relationshipDocument = digest(input.evidence?.relationshipDocument, 'relationship_evidence_digest_required');
|
|
66
|
+
const statusId = `${credentialId}#status`;
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
'@context': Object.freeze(['https://www.w3.org/ns/credentials/v2', 'https://schema.org']),
|
|
69
|
+
id: credentialId,
|
|
70
|
+
type: Object.freeze(['VerifiableCredential', VeterinaryAnimalIdentityCredentialTypes.Assurance]),
|
|
71
|
+
issuer: issuerDid,
|
|
72
|
+
validFrom,
|
|
73
|
+
validUntil,
|
|
74
|
+
credentialSubject: Object.freeze({
|
|
75
|
+
id: holderDid,
|
|
76
|
+
'@type': 'Person',
|
|
77
|
+
holdsAnimalIdentity: Object.freeze({
|
|
78
|
+
id: animalId,
|
|
79
|
+
'@type': 'Animal',
|
|
80
|
+
name: required(input.animal?.name, 'animal_identity_name_required'),
|
|
81
|
+
species: Object.freeze({
|
|
82
|
+
'@type': 'Taxon',
|
|
83
|
+
taxonRank: 'species',
|
|
84
|
+
identifier: required(input.animal?.speciesTaxonomyId, 'animal_identity_species_required'),
|
|
85
|
+
}),
|
|
86
|
+
identifier: Object.freeze({
|
|
87
|
+
'@type': 'PropertyValue',
|
|
88
|
+
propertyID: required(input.animal?.identifier?.system, 'animal_identity_identifier_system_required'),
|
|
89
|
+
value: required(input.animal?.identifier?.value, 'animal_identity_identifier_value_required'),
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
relationshipToAnimal: input.relationship,
|
|
93
|
+
verified_claims: Object.freeze({
|
|
94
|
+
verification: Object.freeze({
|
|
95
|
+
trust_framework: 'veterinarian_in_person_document_check',
|
|
96
|
+
time: checkedAt,
|
|
97
|
+
verification_process: credentialId,
|
|
98
|
+
evidence: Object.freeze([Object.freeze({
|
|
99
|
+
type: 'document',
|
|
100
|
+
method: 'physical_in_person_check',
|
|
101
|
+
verifier: Object.freeze({ organization, professional: issuerDid, role: input.veterinarianRole }),
|
|
102
|
+
time: checkedAt,
|
|
103
|
+
check_details: Object.freeze([Object.freeze({ check_method: 'physical_in_person_check', time: checkedAt })]),
|
|
104
|
+
attachments: Object.freeze({
|
|
105
|
+
content_type: controllerDocument.contentType,
|
|
106
|
+
digest: Object.freeze({ alg: 'sha3-384', value: controllerDocument.sha3_384 }),
|
|
107
|
+
}),
|
|
108
|
+
})]),
|
|
109
|
+
}),
|
|
110
|
+
claims: Object.freeze({
|
|
111
|
+
name: required(input.controller?.legalName, 'controller_identity_name_required'),
|
|
112
|
+
identifier: Object.freeze({
|
|
113
|
+
type: required(input.controller?.identifier?.type, 'controller_identity_identifier_type_required'),
|
|
114
|
+
value: required(input.controller?.identifier?.value, 'controller_identity_identifier_value_required'),
|
|
115
|
+
country: required(input.controller?.identifier?.country, 'controller_identity_identifier_country_required').toUpperCase(),
|
|
116
|
+
}),
|
|
117
|
+
}),
|
|
118
|
+
}),
|
|
119
|
+
}),
|
|
120
|
+
evidence: Object.freeze([
|
|
121
|
+
Object.freeze({
|
|
122
|
+
type: 'VeterinaryAnimalIdentityEvidence',
|
|
123
|
+
verifier: issuerDid,
|
|
124
|
+
time: checkedAt,
|
|
125
|
+
content_type: animalDocument.contentType,
|
|
126
|
+
digest: Object.freeze({ alg: 'sha3-384', value: animalDocument.sha3_384 }),
|
|
127
|
+
}),
|
|
128
|
+
Object.freeze({
|
|
129
|
+
type: 'AnimalControllerRelationshipEvidence',
|
|
130
|
+
relationship: input.relationship,
|
|
131
|
+
verifier: issuerDid,
|
|
132
|
+
time: checkedAt,
|
|
133
|
+
content_type: relationshipDocument.contentType,
|
|
134
|
+
digest: Object.freeze({ alg: 'sha3-384', value: relationshipDocument.sha3_384 }),
|
|
135
|
+
}),
|
|
136
|
+
]),
|
|
137
|
+
credentialStatus: Object.freeze({ id: statusId, type: 'SimpleCredentialStatus2026' }),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Validates the governed credential shape before wallet storage or presentation. */
|
|
141
|
+
export function parseVeterinaryAnimalIdentityAssuranceCredential(value) {
|
|
142
|
+
const credential = record(value);
|
|
143
|
+
const subject = record(credential?.credentialSubject);
|
|
144
|
+
const animal = record(subject?.holdsAnimalIdentity);
|
|
145
|
+
const humanEvidence = firstRecord(record(record(subject?.verified_claims)?.verification)?.evidence);
|
|
146
|
+
const humanVerifier = record(humanEvidence?.verifier);
|
|
147
|
+
const types = Array.isArray(credential?.type) ? credential.type : [];
|
|
148
|
+
if (!types.includes('VerifiableCredential') || !types.includes(VeterinaryAnimalIdentityCredentialTypes.Assurance)) {
|
|
149
|
+
throw new TypeError('animal_identity_credential_type_invalid');
|
|
150
|
+
}
|
|
151
|
+
const rebuilt = buildVeterinaryAnimalIdentityAssuranceCredential({
|
|
152
|
+
credentialId: required(credential?.id, 'animal_identity_credential_id_required'),
|
|
153
|
+
issuerDid: required(credential?.issuer, 'animal_identity_credential_issuer_invalid'),
|
|
154
|
+
veterinarianOrganizationId: required(humanVerifier?.organization, 'veterinarian_organization_required'),
|
|
155
|
+
veterinarianRole: 'ISCO-08|2250',
|
|
156
|
+
holderDid: required(subject?.id, 'animal_identity_credential_holder_invalid'),
|
|
157
|
+
validFrom: required(credential?.validFrom, 'animal_identity_credential_valid_from_invalid'),
|
|
158
|
+
validUntil: required(credential?.validUntil, 'animal_identity_credential_valid_until_invalid'),
|
|
159
|
+
animal: {
|
|
160
|
+
id: required(animal?.id, 'animal_identity_subject_invalid'),
|
|
161
|
+
name: required(animal?.name, 'animal_identity_name_required'),
|
|
162
|
+
speciesTaxonomyId: required(record(animal?.species)?.identifier, 'animal_identity_species_required'),
|
|
163
|
+
identifier: {
|
|
164
|
+
system: required(record(animal?.identifier)?.propertyID, 'animal_identity_identifier_system_required'),
|
|
165
|
+
value: required(record(animal?.identifier)?.value, 'animal_identity_identifier_value_required'),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
controller: {
|
|
169
|
+
id: required(subject?.id, 'animal_identity_controller_id_invalid'),
|
|
170
|
+
legalName: required(record(record(subject?.verified_claims)?.claims)?.name, 'controller_identity_name_required'),
|
|
171
|
+
identifier: record(record(record(subject?.verified_claims)?.claims)?.identifier),
|
|
172
|
+
},
|
|
173
|
+
relationship: subject?.relationshipToAnimal,
|
|
174
|
+
evidence: parseCredentialEvidence(credential, subject),
|
|
175
|
+
});
|
|
176
|
+
return Object.freeze({ ...rebuilt, ...(credential?.proof !== undefined ? { proof: credential.proof } : {}) });
|
|
177
|
+
}
|
|
178
|
+
function parseCredentialEvidence(credential, subject) {
|
|
179
|
+
const evidence = Array.isArray(credential.evidence) ? credential.evidence.map(record).filter(Boolean) : [];
|
|
180
|
+
const animal = evidence.find(item => item.type === 'VeterinaryAnimalIdentityEvidence');
|
|
181
|
+
const relationship = evidence.find(item => item.type === 'AnimalControllerRelationshipEvidence');
|
|
182
|
+
const human = firstRecord(record(record(subject.verified_claims)?.verification)?.evidence);
|
|
183
|
+
const humanAttachment = record(human?.attachments);
|
|
184
|
+
const humanDigest = record(humanAttachment?.digest);
|
|
185
|
+
const animalDigest = record(animal?.digest);
|
|
186
|
+
const relationshipDigest = record(relationship?.digest);
|
|
187
|
+
return {
|
|
188
|
+
animalDocument: {
|
|
189
|
+
contentType: animal?.content_type,
|
|
190
|
+
sha3_384: required(animalDigest?.value, 'animal_identity_evidence_digest_required'),
|
|
191
|
+
},
|
|
192
|
+
controllerDocument: {
|
|
193
|
+
contentType: humanAttachment?.content_type,
|
|
194
|
+
sha3_384: required(humanDigest?.value, 'controller_identity_evidence_digest_required'),
|
|
195
|
+
},
|
|
196
|
+
relationshipDocument: {
|
|
197
|
+
contentType: relationship?.content_type,
|
|
198
|
+
sha3_384: required(relationshipDigest?.value, 'relationship_evidence_digest_required'),
|
|
199
|
+
},
|
|
200
|
+
checkedAt: required(animal?.time, 'animal_identity_evidence_time_invalid'),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Builds the bounded OpenID4VP 1.0 request used by airline, police and insurer verifiers. */
|
|
204
|
+
export function buildVeterinaryAnimalIdentityOpenId4VpRequest(input) {
|
|
205
|
+
const clientId = required(input.clientId, 'animal_identity_verifier_client_id_required');
|
|
206
|
+
const responseUri = required(input.responseUri, 'animal_identity_verifier_response_uri_required');
|
|
207
|
+
const nonce = required(input.nonce, 'animal_identity_presentation_nonce_required');
|
|
208
|
+
const state = required(input.state, 'animal_identity_presentation_state_required');
|
|
209
|
+
const animalId = did(input.animalId, 'animal_identity_subject_invalid');
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
client_id: clientId,
|
|
212
|
+
response_uri: responseUri,
|
|
213
|
+
response_type: 'vp_token',
|
|
214
|
+
response_mode: 'direct_post',
|
|
215
|
+
nonce,
|
|
216
|
+
state,
|
|
217
|
+
presentation_definition: Object.freeze({
|
|
218
|
+
id: `vetchain-animal-identity-${state}`,
|
|
219
|
+
input_descriptors: Object.freeze([Object.freeze({
|
|
220
|
+
id: 'veterinary-animal-identity-assurance',
|
|
221
|
+
format: Object.freeze({ jwt_vc_json: Object.freeze({ alg: Object.freeze(['ES256', 'ML-DSA-65']) }) }),
|
|
222
|
+
constraints: Object.freeze({
|
|
223
|
+
fields: Object.freeze([
|
|
224
|
+
Object.freeze({ path: Object.freeze(['$.type']), filter: Object.freeze({ type: 'string', const: VeterinaryAnimalIdentityCredentialTypes.Assurance }) }),
|
|
225
|
+
Object.freeze({ path: Object.freeze(['$.credentialSubject.holdsAnimalIdentity.id']), filter: Object.freeze({ type: 'string', const: animalId }) }),
|
|
226
|
+
]),
|
|
227
|
+
}),
|
|
228
|
+
})]),
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -197,9 +197,9 @@ export declare const DataspaceProtocol2025: Readonly<{
|
|
|
197
197
|
/**
|
|
198
198
|
* Builds the claims-only public department Organization.
|
|
199
199
|
*
|
|
200
|
-
* One department is one Schema.org/FHIR Organization
|
|
201
|
-
*
|
|
202
|
-
*
|
|
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
203
|
*/
|
|
204
204
|
export declare function buildDepartmentDirectoryResource(input: DepartmentDirectoryInput): DirectoryResource;
|
|
205
205
|
/** Builds the claims-only public Place authored by an organization. */
|
|
@@ -239,49 +239,14 @@ export declare function projectDepartmentToFhirR5Organization(department: Direct
|
|
|
239
239
|
name: string;
|
|
240
240
|
}>;
|
|
241
241
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
242
|
+
* @deprecated Aggregate department HealthcareService projection is forbidden.
|
|
243
|
+
* Project each offered Service independently with
|
|
244
|
+
* projectServiceToFhirR5HealthcareService(...).
|
|
245
245
|
*/
|
|
246
246
|
export declare function projectDepartmentToFhirR5HealthcareService(department: DirectoryResource | DirectoryFlatClaimsResource, input: Readonly<{
|
|
247
247
|
services: readonly (DirectoryResource | DirectoryFlatClaimsResource)[];
|
|
248
248
|
places: readonly (DirectoryResource | DirectoryFlatClaimsResource)[];
|
|
249
|
-
}>):
|
|
250
|
-
meta: {
|
|
251
|
-
claims: {
|
|
252
|
-
'HealthcareService.service-category'?: string[] | undefined;
|
|
253
|
-
'HealthcareService.identifier': string;
|
|
254
|
-
'HealthcareService.organization': string;
|
|
255
|
-
'HealthcareService.name': string | undefined;
|
|
256
|
-
'HealthcareService.location': string[];
|
|
257
|
-
'HealthcareService.service-type': string[];
|
|
258
|
-
};
|
|
259
|
-
};
|
|
260
|
-
photo?: {
|
|
261
|
-
url: string;
|
|
262
|
-
} | undefined;
|
|
263
|
-
resourceType: "HealthcareService";
|
|
264
|
-
id: string;
|
|
265
|
-
active: true;
|
|
266
|
-
providedBy: {
|
|
267
|
-
reference: string;
|
|
268
|
-
};
|
|
269
|
-
name: string | undefined;
|
|
270
|
-
comment: string | undefined;
|
|
271
|
-
category: {
|
|
272
|
-
coding: {
|
|
273
|
-
code: string;
|
|
274
|
-
}[];
|
|
275
|
-
}[];
|
|
276
|
-
type: {
|
|
277
|
-
coding: {
|
|
278
|
-
code: string;
|
|
279
|
-
}[];
|
|
280
|
-
}[];
|
|
281
|
-
location: {
|
|
282
|
-
reference: string;
|
|
283
|
-
}[];
|
|
284
|
-
}>;
|
|
249
|
+
}>): void;
|
|
285
250
|
/** Explicitly projects one neutral Place to native FHIR R5 Location for export or interop. */
|
|
286
251
|
export declare function projectPlaceToFhirR5Location(place: DirectoryResource | DirectoryFlatClaimsResource): Readonly<{
|
|
287
252
|
resourceType: "Location";
|
|
@@ -169,9 +169,9 @@ function encodeSpecialOpeningHours(value) {
|
|
|
169
169
|
/**
|
|
170
170
|
* Builds the claims-only public department Organization.
|
|
171
171
|
*
|
|
172
|
-
* One department is one Schema.org/FHIR Organization
|
|
173
|
-
*
|
|
174
|
-
*
|
|
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
175
|
*/
|
|
176
176
|
export function buildDepartmentDirectoryResource(input) {
|
|
177
177
|
const id = required(input.id, 'department_id_required');
|
|
@@ -421,54 +421,14 @@ export function projectDepartmentToFhirR5Organization(department) {
|
|
|
421
421
|
});
|
|
422
422
|
}
|
|
423
423
|
/**
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
424
|
+
* @deprecated Aggregate department HealthcareService projection is forbidden.
|
|
425
|
+
* Project each offered Service independently with
|
|
426
|
+
* projectServiceToFhirR5HealthcareService(...).
|
|
427
427
|
*/
|
|
428
428
|
export function projectDepartmentToFhirR5HealthcareService(department, input) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const places = departmentLocations.map(identifier => {
|
|
433
|
-
const place = input.places.find(candidate => candidate.resourceType === 'Place' && candidate.id === identifier);
|
|
434
|
-
if (!place)
|
|
435
|
-
throw new TypeError('department_place_not_found');
|
|
436
|
-
return place;
|
|
437
|
-
});
|
|
438
|
-
const services = input.services.filter(service => {
|
|
439
|
-
if (service.resourceType !== 'Service')
|
|
440
|
-
throw new TypeError('service_resource_required');
|
|
441
|
-
return first(service, ClaimsDirectoryServiceSchemaorg.providerIdentifier) === department.id;
|
|
442
|
-
});
|
|
443
|
-
if (!services.length)
|
|
444
|
-
throw new TypeError('department_service_required');
|
|
445
|
-
for (const service of services) {
|
|
446
|
-
if (values(service, ClaimsDirectoryServiceSchemaorg.placeIdentifier).some(identifier => !departmentLocations.includes(identifier))) {
|
|
447
|
-
throw new TypeError('department_service_place_invalid');
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
const serviceTypes = [...new Set(services.flatMap(service => values(service, ClaimsDirectoryServiceSchemaorg.serviceType)))];
|
|
451
|
-
const categories = [...new Set(services.flatMap(service => values(service, ClaimsDirectoryServiceSchemaorg.category)))];
|
|
452
|
-
const concept = (code) => ({ coding: [{ code }] });
|
|
453
|
-
const photoUrl = first(department, ClaimsDirectoryOrganizationSchemaorg.logoContentUrl)
|
|
454
|
-
|| places.map(place => first(place, ClaimsPlaceSchemaorg.photoContentUrl)).find(Boolean);
|
|
455
|
-
return Object.freeze({
|
|
456
|
-
resourceType: 'HealthcareService', id: department.id, active: true,
|
|
457
|
-
providedBy: { reference: `Organization/${department.id}` },
|
|
458
|
-
name: first(department, ClaimsDirectoryOrganizationSchemaorg.name),
|
|
459
|
-
comment: first(department, ClaimsDirectoryOrganizationSchemaorg.description),
|
|
460
|
-
category: categories.map(concept), type: serviceTypes.map(concept),
|
|
461
|
-
location: places.map(place => ({ reference: `Location/${place.id}` })),
|
|
462
|
-
...(photoUrl ? { photo: { url: photoUrl } } : {}),
|
|
463
|
-
meta: { claims: {
|
|
464
|
-
'HealthcareService.identifier': department.id,
|
|
465
|
-
'HealthcareService.organization': `Organization/${department.id}`,
|
|
466
|
-
'HealthcareService.name': first(department, ClaimsDirectoryOrganizationSchemaorg.name),
|
|
467
|
-
'HealthcareService.location': places.map(place => `Location/${place.id}`),
|
|
468
|
-
'HealthcareService.service-type': serviceTypes,
|
|
469
|
-
...(categories.length ? { 'HealthcareService.service-category': categories } : {}),
|
|
470
|
-
} },
|
|
471
|
-
});
|
|
429
|
+
void department;
|
|
430
|
+
void input;
|
|
431
|
+
throw new TypeError('department_aggregate_healthcare_service_forbidden');
|
|
472
432
|
}
|
|
473
433
|
/** Explicitly projects one neutral Place to native FHIR R5 Location for export or interop. */
|
|
474
434
|
export function projectPlaceToFhirR5Location(place) {
|
|
@@ -519,7 +479,8 @@ export function projectServiceToFhirR5HealthcareService(service, input) {
|
|
|
519
479
|
});
|
|
520
480
|
const serviceTypes = values(service, ClaimsDirectoryServiceSchemaorg.serviceType);
|
|
521
481
|
const categories = values(service, ClaimsDirectoryServiceSchemaorg.category);
|
|
522
|
-
const
|
|
482
|
+
const rawOrganization = required(first(service, ClaimsDirectoryServiceSchemaorg.providerIdentifier), 'service_organization_required');
|
|
483
|
+
const organization = rawOrganization.startsWith('Organization/') ? rawOrganization : `Organization/${rawOrganization}`;
|
|
523
484
|
const photoUrl = places.map(place => first(place, ClaimsPlaceSchemaorg.photoContentUrl)).find(Boolean);
|
|
524
485
|
const concept = (code) => ({ coding: [{ code }] });
|
|
525
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.1",
|
|
4
4
|
"description": "Browser-safe governed VetChain data contracts",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Connecting Solution & Applications Ltd",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./dist/animal-card.d.ts",
|
|
17
17
|
"default": "./dist/animal-card.js"
|
|
18
18
|
},
|
|
19
|
+
"./animal-identity-assurance": {
|
|
20
|
+
"types": "./dist/animal-identity-assurance.d.ts",
|
|
21
|
+
"default": "./dist/animal-identity-assurance.js"
|
|
22
|
+
},
|
|
19
23
|
"./animal-behavior": {
|
|
20
24
|
"types": "./dist/animal-behavior.d.ts",
|
|
21
25
|
"default": "./dist/animal-behavior.js"
|
|
@@ -157,10 +161,10 @@
|
|
|
157
161
|
"typescript": "^5.5.4"
|
|
158
162
|
},
|
|
159
163
|
"dependencies": {
|
|
160
|
-
"fhir-data-utils-ts": "0.
|
|
164
|
+
"fhir-data-utils-ts": "0.6.0",
|
|
161
165
|
"gdc-common-utils-ts": "2.9.12",
|
|
162
166
|
"pako": "^2.2.0",
|
|
163
|
-
"sos-data-utils-ts": "0.
|
|
167
|
+
"sos-data-utils-ts": "0.5.1"
|
|
164
168
|
},
|
|
165
169
|
"engines": {
|
|
166
170
|
"node": ">=20"
|