vet-data-utils-ts 0.5.14 → 0.5.16
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/dist/organization-application.d.ts +6 -2
- package/dist/organization-application.js +14 -3
- package/dist/scheduling.d.ts +65 -29
- package/dist/scheduling.js +150 -58
- package/package.json +1 -1
|
@@ -50,5 +50,9 @@ export declare function organizationServiceTypeForParticipationRoles(roles: read
|
|
|
50
50
|
* object and remain inside the BFF/high-level Node runtime.
|
|
51
51
|
*/
|
|
52
52
|
export declare function parseReusableOrganizationApplication(input: unknown): ReusableOrganizationApplication;
|
|
53
|
-
/**
|
|
54
|
-
export declare function
|
|
53
|
+
/** Requires an organization creator to provide an optional official telephone already formatted as E.164. */
|
|
54
|
+
export declare function parseOptionalOrganizationOfficialPhone(value: unknown): string | undefined;
|
|
55
|
+
/** Helps an individual normalize a telephone used to look up an organization. */
|
|
56
|
+
export declare function normalizePhoneForLookup(value: unknown, defaultCallingCode: unknown): string | undefined;
|
|
57
|
+
/** @deprecated Use {@link normalizePhoneForLookup}; organization applications use {@link parseOptionalOrganizationOfficialPhone}. */
|
|
58
|
+
export declare const normalizeOptionalOrganizationOfficialPhone: typeof normalizePhoneForLookup;
|
|
@@ -91,7 +91,7 @@ export function parseReusableOrganizationApplication(input) {
|
|
|
91
91
|
regionalId,
|
|
92
92
|
...(subdivisionCode ? { subdivisionCode } : {}),
|
|
93
93
|
...(optional(value.officialLicense) ? { officialLicense: optional(value.officialLicense) } : {}),
|
|
94
|
-
...(optional(value.officialPhone) ? { officialPhone:
|
|
94
|
+
...(optional(value.officialPhone) ? { officialPhone: parseOptionalOrganizationOfficialPhone(value.officialPhone) } : {}),
|
|
95
95
|
legalRepresentative: Object.freeze({
|
|
96
96
|
name: required(legalRepresentative.name, 'organization_legal_representative_name_required'),
|
|
97
97
|
email: legalEmail,
|
|
@@ -100,8 +100,17 @@ export function parseReusableOrganizationApplication(input) {
|
|
|
100
100
|
participationRoles: Object.freeze([...new Set(selectedRoles)]),
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
-
/**
|
|
104
|
-
export function
|
|
103
|
+
/** Requires an organization creator to provide an optional official telephone already formatted as E.164. */
|
|
104
|
+
export function parseOptionalOrganizationOfficialPhone(value) {
|
|
105
|
+
const raw = optional(value);
|
|
106
|
+
if (!raw)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (!/^\+[1-9]\d{7,14}$/.test(raw))
|
|
109
|
+
throw new TypeError('organization_official_phone_international_format_required');
|
|
110
|
+
return raw;
|
|
111
|
+
}
|
|
112
|
+
/** Helps an individual normalize a telephone used to look up an organization. */
|
|
113
|
+
export function normalizePhoneForLookup(value, defaultCallingCode) {
|
|
105
114
|
const raw = optional(value);
|
|
106
115
|
if (!raw)
|
|
107
116
|
return undefined;
|
|
@@ -118,6 +127,8 @@ export function normalizeOptionalOrganizationOfficialPhone(value, defaultCalling
|
|
|
118
127
|
throw new TypeError('organization_official_phone_invalid');
|
|
119
128
|
return compact;
|
|
120
129
|
}
|
|
130
|
+
/** @deprecated Use {@link normalizePhoneForLookup}; organization applications use {@link parseOptionalOrganizationOfficialPhone}. */
|
|
131
|
+
export const normalizeOptionalOrganizationOfficialPhone = normalizePhoneForLookup;
|
|
121
132
|
function record(value, error) {
|
|
122
133
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
123
134
|
throw new TypeError(error);
|
package/dist/scheduling.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** FHIR R5 Slot statuses accepted by
|
|
1
|
+
/** FHIR R5 Slot statuses accepted by reusable scheduling. */
|
|
2
2
|
export declare const SlotStatuses: Readonly<{
|
|
3
3
|
readonly Busy: "busy";
|
|
4
4
|
readonly Free: "free";
|
|
@@ -27,35 +27,37 @@ export declare const AppointmentStatuses: Readonly<{
|
|
|
27
27
|
readonly CheckedIn: "checked-in";
|
|
28
28
|
readonly Waitlist: "waitlist";
|
|
29
29
|
}>;
|
|
30
|
-
/** Official FHIR R5 Slot resource-specific SearchParameter codes
|
|
31
|
-
export declare const
|
|
32
|
-
/**
|
|
33
|
-
export declare const
|
|
30
|
+
/** Official FHIR R5 Slot resource-specific SearchParameter codes. */
|
|
31
|
+
export declare const SlotSearchParameters: readonly ["appointment-type", "identifier", "schedule", "service-category", "service-type", "service-type-reference", "specialty", "start", "status"];
|
|
32
|
+
/** Official FHIR R5 SearchParameter codes used by reusable scheduling. */
|
|
33
|
+
export declare const SchedulingSearchParameterCatalog: Readonly<{
|
|
34
34
|
readonly Location: readonly string[];
|
|
35
35
|
readonly Schedule: readonly string[];
|
|
36
|
-
readonly Slot: readonly
|
|
36
|
+
readonly Slot: readonly ["appointment-type", "identifier", "schedule", "service-category", "service-type", "service-type-reference", "specialty", "start", "status"];
|
|
37
37
|
readonly Appointment: readonly string[];
|
|
38
38
|
readonly AppointmentResponse: readonly string[];
|
|
39
39
|
}>;
|
|
40
|
-
|
|
41
|
-
export
|
|
42
|
-
|
|
40
|
+
/** Indexed scheduling claims: FHIR SearchParameters plus the governed generic user-selected extension. */
|
|
41
|
+
export declare const SchedulingFlatClaimCatalog: Readonly<Record<"Location" | "Schedule" | "Appointment" | "AppointmentResponse" | "Slot", readonly string[]>>;
|
|
42
|
+
export type SchedulingResourceType = keyof typeof SchedulingFlatClaimCatalog;
|
|
43
|
+
export type SchedulingFlatClaimsResource = Readonly<Record<string, unknown> & {
|
|
44
|
+
resourceType: SchedulingResourceType;
|
|
43
45
|
id: string;
|
|
44
|
-
meta: Readonly<{
|
|
46
|
+
meta: Readonly<Record<string, unknown> & {
|
|
45
47
|
claims: Readonly<Record<string, readonly string[]>>;
|
|
46
48
|
}>;
|
|
47
49
|
}>;
|
|
48
|
-
/** Validates
|
|
49
|
-
export declare function
|
|
50
|
-
export type ClaimsFirstResource = Readonly<{
|
|
50
|
+
/** Validates native FHIR scheduling data and normalizes indexed SearchParameter claims to strings. */
|
|
51
|
+
export declare function normalizeSchedulingFlatClaimsResource(input: unknown): SchedulingFlatClaimsResource;
|
|
52
|
+
export type ClaimsFirstResource = Readonly<Record<string, unknown> & {
|
|
51
53
|
resourceType: string;
|
|
52
54
|
id: string;
|
|
53
|
-
meta: Readonly<{
|
|
55
|
+
meta: Readonly<Record<string, unknown> & {
|
|
54
56
|
claims: Readonly<Record<string, unknown>>;
|
|
55
57
|
}>;
|
|
56
58
|
}>;
|
|
57
|
-
/** Builds
|
|
58
|
-
export declare function
|
|
59
|
+
/** Builds one physical FHIR R5 Location with SearchParameter claims. */
|
|
60
|
+
export declare function buildLocationResource(input: Readonly<{
|
|
59
61
|
id: string;
|
|
60
62
|
organizationReference: string;
|
|
61
63
|
name: string;
|
|
@@ -67,7 +69,7 @@ export declare function buildVeterinaryLocationResource(input: Readonly<{
|
|
|
67
69
|
photoUrl?: string;
|
|
68
70
|
}>): ClaimsFirstResource;
|
|
69
71
|
/** Builds the claims-first FHIR R5 Schedule joining one practitioner and consultation Location. */
|
|
70
|
-
export declare function
|
|
72
|
+
export declare function buildScheduleResource(input: Readonly<{
|
|
71
73
|
id: string;
|
|
72
74
|
name: string;
|
|
73
75
|
practitionerReference: string;
|
|
@@ -75,13 +77,13 @@ export declare function buildVeterinaryScheduleResource(input: Readonly<{
|
|
|
75
77
|
planningHorizonStart: string;
|
|
76
78
|
planningHorizonEnd: string;
|
|
77
79
|
}>): ClaimsFirstResource;
|
|
78
|
-
export type
|
|
80
|
+
export type AvailabilityPeriod = Readonly<{
|
|
79
81
|
daysOfWeek: readonly number[];
|
|
80
82
|
startTime: string;
|
|
81
83
|
endTime: string;
|
|
82
84
|
locationReferences: readonly string[];
|
|
83
85
|
}>;
|
|
84
|
-
export type
|
|
86
|
+
export type AvailabilityException = Readonly<{
|
|
85
87
|
effect: 'restrict' | 'expand';
|
|
86
88
|
recurrence: Readonly<{
|
|
87
89
|
frequency: 'weekly' | 'monthly';
|
|
@@ -92,25 +94,27 @@ export type VeterinaryAvailabilityException = Readonly<{
|
|
|
92
94
|
endTime: string;
|
|
93
95
|
locationReferences: readonly string[];
|
|
94
96
|
}>;
|
|
95
|
-
export type
|
|
97
|
+
export type ExpandedSlot = Readonly<{
|
|
96
98
|
localDate: string;
|
|
97
99
|
localStart: string;
|
|
98
100
|
localEnd: string;
|
|
99
101
|
locationReference: string;
|
|
100
102
|
resource: ClaimsFirstResource;
|
|
101
103
|
}>;
|
|
104
|
+
/** Produces the stable Schedule id for one practitioner/location availability stream. */
|
|
105
|
+
export declare function scheduleIdForLocation(baseScheduleId: string, locationReference: string): string;
|
|
102
106
|
/** Expands bounded weekly availability into claims-first Slots, applying recurring restrictions or expansions. */
|
|
103
|
-
export declare function
|
|
107
|
+
export declare function expandAvailability(input: Readonly<{
|
|
104
108
|
scheduleId: string;
|
|
105
109
|
startDate: string;
|
|
106
110
|
endDate: string;
|
|
107
111
|
timeZone: string;
|
|
108
112
|
slotMinutes: number;
|
|
109
|
-
periods: readonly
|
|
110
|
-
exceptions: readonly
|
|
111
|
-
}>): readonly
|
|
113
|
+
periods: readonly AvailabilityPeriod[];
|
|
114
|
+
exceptions: readonly AvailabilityException[];
|
|
115
|
+
}>): readonly ExpandedSlot[];
|
|
112
116
|
/** Builds a claims-first response that confirms, declines or proposes a new appointment time. */
|
|
113
|
-
export declare function
|
|
117
|
+
export declare function buildAppointmentResponseResource(input: Readonly<{
|
|
114
118
|
id: string;
|
|
115
119
|
appointmentReference: string;
|
|
116
120
|
actorReference: string;
|
|
@@ -123,7 +127,7 @@ export declare function buildVeterinaryAppointmentResponseResource(input: Readon
|
|
|
123
127
|
notificationRecipientEmail?: string;
|
|
124
128
|
}>): ClaimsFirstResource;
|
|
125
129
|
/** Builds the FHIR-like Communication that an adapter may render as the additional English clinic email. */
|
|
126
|
-
export declare function
|
|
130
|
+
export declare function buildAppointmentResponseNotificationResource(input: Readonly<{
|
|
127
131
|
id: string;
|
|
128
132
|
responseReference: string;
|
|
129
133
|
appointmentReference: string;
|
|
@@ -133,8 +137,8 @@ export declare function buildVeterinaryAppointmentResponseNotificationResource(i
|
|
|
133
137
|
sentAt: string;
|
|
134
138
|
language: string;
|
|
135
139
|
}>): ClaimsFirstResource;
|
|
136
|
-
/** Builds a
|
|
137
|
-
export declare function
|
|
140
|
+
/** Builds a FHIR R5 Appointment whose indexed claims are SearchParameters. */
|
|
141
|
+
export declare function buildAppointmentResource(input: Readonly<{
|
|
138
142
|
id: string;
|
|
139
143
|
status: string;
|
|
140
144
|
start: string;
|
|
@@ -144,13 +148,45 @@ export declare function buildVeterinaryAppointmentResource(input: Readonly<{
|
|
|
144
148
|
locationReference?: string;
|
|
145
149
|
confirmationDueAt?: string;
|
|
146
150
|
userSelected?: boolean;
|
|
151
|
+
organizationReference?: string;
|
|
152
|
+
notificationEmail?: string;
|
|
147
153
|
clinicRecipientReference?: string;
|
|
148
154
|
clinicRecipientEmail?: string;
|
|
149
155
|
}>): ClaimsFirstResource;
|
|
150
156
|
/** Builds the durable web inbox notification when a schedule change displaces an appointment. */
|
|
151
|
-
export declare function
|
|
157
|
+
export declare function buildAppointmentNotificationResource(input: Readonly<{
|
|
152
158
|
id: string;
|
|
153
159
|
appointmentReference: string;
|
|
154
160
|
subjectReference: string;
|
|
155
161
|
reason: string;
|
|
156
162
|
}>): ClaimsFirstResource;
|
|
163
|
+
/** @deprecated Use {@link SlotSearchParameters}. */
|
|
164
|
+
export declare const VeterinarySlotSearchParameters: readonly ["appointment-type", "identifier", "schedule", "service-category", "service-type", "service-type-reference", "specialty", "start", "status"];
|
|
165
|
+
/** @deprecated Use {@link SchedulingFlatClaimCatalog}. */
|
|
166
|
+
export declare const VeterinarySchedulingFlatClaimCatalog: Readonly<Record<"Location" | "Schedule" | "Appointment" | "AppointmentResponse" | "Slot", readonly string[]>>;
|
|
167
|
+
/** @deprecated Use {@link SchedulingResourceType}. */
|
|
168
|
+
export type VeterinarySchedulingResourceType = SchedulingResourceType;
|
|
169
|
+
/** @deprecated Use {@link SchedulingFlatClaimsResource}. */
|
|
170
|
+
export type VeterinarySchedulingFlatClaimsResource = SchedulingFlatClaimsResource;
|
|
171
|
+
/** @deprecated Use {@link normalizeSchedulingFlatClaimsResource}. */
|
|
172
|
+
export declare const normalizeVeterinarySchedulingFlatClaimsResource: typeof normalizeSchedulingFlatClaimsResource;
|
|
173
|
+
/** @deprecated Use {@link buildLocationResource}. */
|
|
174
|
+
export declare const buildVeterinaryLocationResource: typeof buildLocationResource;
|
|
175
|
+
/** @deprecated Use {@link buildScheduleResource}. */
|
|
176
|
+
export declare const buildVeterinaryScheduleResource: typeof buildScheduleResource;
|
|
177
|
+
/** @deprecated Use {@link AvailabilityPeriod}. */
|
|
178
|
+
export type VeterinaryAvailabilityPeriod = AvailabilityPeriod;
|
|
179
|
+
/** @deprecated Use {@link AvailabilityException}. */
|
|
180
|
+
export type VeterinaryAvailabilityException = AvailabilityException;
|
|
181
|
+
/** @deprecated Use {@link ExpandedSlot}. */
|
|
182
|
+
export type ExpandedVeterinarySlot = ExpandedSlot;
|
|
183
|
+
/** @deprecated Use {@link expandAvailability}. */
|
|
184
|
+
export declare const expandVeterinaryAvailability: typeof expandAvailability;
|
|
185
|
+
/** @deprecated Use {@link buildAppointmentResponseResource}. */
|
|
186
|
+
export declare const buildVeterinaryAppointmentResponseResource: typeof buildAppointmentResponseResource;
|
|
187
|
+
/** @deprecated Use {@link buildAppointmentResponseNotificationResource}. */
|
|
188
|
+
export declare const buildVeterinaryAppointmentResponseNotificationResource: typeof buildAppointmentResponseNotificationResource;
|
|
189
|
+
/** @deprecated Use {@link buildAppointmentResource}. */
|
|
190
|
+
export declare const buildVeterinaryAppointmentResource: typeof buildAppointmentResource;
|
|
191
|
+
/** @deprecated Use {@link buildAppointmentNotificationResource}. */
|
|
192
|
+
export declare const buildVeterinaryAppointmentNotificationResource: typeof buildAppointmentNotificationResource;
|
package/dist/scheduling.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** FHIR R5 Slot statuses accepted by
|
|
1
|
+
/** FHIR R5 Slot statuses accepted by reusable scheduling. */
|
|
2
2
|
export const SlotStatuses = Object.freeze({ Busy: 'busy', Free: 'free', BusyUnavailable: 'busy-unavailable', BusyTentative: 'busy-tentative', EnteredInError: 'entered-in-error' });
|
|
3
3
|
/** FHIR R5 AppointmentResponse participant statuses. */
|
|
4
4
|
export const AppointmentResponseStatuses = Object.freeze({ Accepted: 'accepted', Declined: 'declined', Tentative: 'tentative', NeedsAction: 'needs-action', EnteredInError: 'entered-in-error' });
|
|
@@ -7,40 +7,58 @@ export const AppointmentStatuses = Object.freeze({
|
|
|
7
7
|
Proposed: 'proposed', Pending: 'pending', Booked: 'booked', Arrived: 'arrived', Fulfilled: 'fulfilled',
|
|
8
8
|
Cancelled: 'cancelled', NoShow: 'noshow', EnteredInError: 'entered-in-error', CheckedIn: 'checked-in', Waitlist: 'waitlist',
|
|
9
9
|
});
|
|
10
|
-
/** Official FHIR R5 Slot resource-specific SearchParameter codes
|
|
11
|
-
export const
|
|
12
|
-
/**
|
|
13
|
-
export const
|
|
14
|
-
Location: Object.freeze(['
|
|
15
|
-
Schedule: Object.freeze(['
|
|
16
|
-
Slot:
|
|
17
|
-
Appointment: Object.freeze(['
|
|
18
|
-
AppointmentResponse: Object.freeze(['
|
|
10
|
+
/** Official FHIR R5 Slot resource-specific SearchParameter codes. */
|
|
11
|
+
export const SlotSearchParameters = Object.freeze(['appointment-type', 'identifier', 'schedule', 'service-category', 'service-type', 'service-type-reference', 'specialty', 'start', 'status']);
|
|
12
|
+
/** Official FHIR R5 SearchParameter codes used by reusable scheduling. */
|
|
13
|
+
export const SchedulingSearchParameterCatalog = Object.freeze({
|
|
14
|
+
Location: Object.freeze(['characteristic', 'identifier', 'name', 'near', 'organization', 'status', 'type']),
|
|
15
|
+
Schedule: Object.freeze(['active', 'actor', 'date', 'identifier', 'name', 'service-category', 'service-type', 'specialty']),
|
|
16
|
+
Slot: SlotSearchParameters,
|
|
17
|
+
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
|
+
AppointmentResponse: Object.freeze(['_lastUpdated', 'actor', 'appointment', 'group', 'identifier', 'location', 'part-status', 'patient', 'practitioner']),
|
|
19
19
|
});
|
|
20
|
-
/**
|
|
21
|
-
export
|
|
20
|
+
/** Indexed scheduling claims: FHIR SearchParameters plus the governed generic user-selected extension. */
|
|
21
|
+
export const SchedulingFlatClaimCatalog = Object.freeze(Object.fromEntries(Object.entries(SchedulingSearchParameterCatalog).map(([resourceType, parameters]) => [
|
|
22
|
+
resourceType,
|
|
23
|
+
Object.freeze([
|
|
24
|
+
...parameters.map(parameter => `${resourceType}.${parameter}`),
|
|
25
|
+
...(resourceType === 'Appointment' ? ['Appointment.user-selected'] : []),
|
|
26
|
+
]),
|
|
27
|
+
])));
|
|
28
|
+
const nativeFieldsByResource = Object.freeze({
|
|
29
|
+
Location: new Set(['identifier', 'status', 'mode', 'name', 'description', 'type', 'telecom', 'address', 'physicalType', 'position', 'managingOrganization', 'characteristic', 'contained']),
|
|
30
|
+
Schedule: new Set(['identifier', 'active', 'serviceCategory', 'serviceType', 'specialty', 'name', 'actor', 'planningHorizon', 'comment']),
|
|
31
|
+
Slot: new Set(['identifier', 'serviceCategory', 'serviceType', 'specialty', 'appointmentType', 'schedule', 'status', 'start', 'end', 'overbooked', 'comment']),
|
|
32
|
+
Appointment: new Set(['identifier', 'status', 'serviceCategory', 'serviceType', 'specialty', 'appointmentType', 'reason', 'description', 'start', 'end', 'slot', 'participant', 'subject', 'note', 'supportingInformation', 'contained']),
|
|
33
|
+
AppointmentResponse: new Set(['identifier', 'appointment', 'proposedNewTime', 'start', 'end', 'participantType', 'actor', 'participantStatus', 'comment', 'recurring', 'occurrenceDate', 'recurrenceId', 'contained']),
|
|
34
|
+
});
|
|
35
|
+
/** Validates native FHIR scheduling data and normalizes indexed SearchParameter claims to strings. */
|
|
36
|
+
export function normalizeSchedulingFlatClaimsResource(input) {
|
|
22
37
|
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
23
|
-
throw new TypeError('
|
|
38
|
+
throw new TypeError('scheduling_flat_resource_invalid');
|
|
24
39
|
const value = input;
|
|
25
|
-
if (typeof value.resourceType !== 'string' || !(value.resourceType in
|
|
26
|
-
throw new TypeError('
|
|
27
|
-
const
|
|
40
|
+
if (typeof value.resourceType !== 'string' || !(value.resourceType in SchedulingFlatClaimCatalog))
|
|
41
|
+
throw new TypeError('scheduling_flat_resource_invalid');
|
|
42
|
+
const resourceType = value.resourceType;
|
|
43
|
+
if (Object.keys(value).some(key => !['resourceType', 'id', 'meta'].includes(key) && !nativeFieldsByResource[resourceType].has(key)))
|
|
44
|
+
throw new TypeError('scheduling_flat_resource_invalid');
|
|
45
|
+
const id = required(String(value.id || ''), 'scheduling_flat_resource_invalid');
|
|
28
46
|
if (!/^[A-Za-z0-9\-.]{1,64}$/.test(id))
|
|
29
|
-
throw new TypeError('
|
|
47
|
+
throw new TypeError('scheduling_flat_resource_invalid');
|
|
30
48
|
const meta = value.meta;
|
|
31
|
-
if (!meta || typeof meta !== 'object' || !meta.claims || typeof meta.claims !== 'object' || Array.isArray(meta.claims)
|
|
32
|
-
throw new TypeError('
|
|
33
|
-
const allowed = new Set(
|
|
49
|
+
if (!meta || typeof meta !== 'object' || !meta.claims || typeof meta.claims !== 'object' || Array.isArray(meta.claims))
|
|
50
|
+
throw new TypeError('scheduling_flat_resource_invalid');
|
|
51
|
+
const allowed = new Set(SchedulingFlatClaimCatalog[resourceType]);
|
|
34
52
|
const claims = {};
|
|
35
53
|
for (const [name, raw] of Object.entries(meta.claims)) {
|
|
36
54
|
if (!allowed.has(name))
|
|
37
|
-
throw new TypeError('
|
|
55
|
+
throw new TypeError('scheduling_flat_claim_invalid');
|
|
38
56
|
const values = (Array.isArray(raw) ? raw : [raw]).map(item => String(item).trim());
|
|
39
57
|
if (!values.length || values.some(item => !item))
|
|
40
|
-
throw new TypeError('
|
|
41
|
-
claims[name] = Object.freeze([...new Set(values)]);
|
|
58
|
+
throw new TypeError('scheduling_flat_claim_invalid');
|
|
59
|
+
claims[name] = Object.freeze([...new Set([...(claims[name] || []), ...values])]);
|
|
42
60
|
}
|
|
43
|
-
return Object.freeze({
|
|
61
|
+
return Object.freeze({ ...value, resourceType, id, meta: Object.freeze({ ...meta, claims: Object.freeze(claims) }) });
|
|
44
62
|
}
|
|
45
63
|
const required = (value, code) => { const normalized = value.trim(); if (!normalized)
|
|
46
64
|
throw new Error(code); return normalized; };
|
|
@@ -50,9 +68,9 @@ const assertDate = (value) => { if (!datePattern.test(value) || Number.isNaN(Dat
|
|
|
50
68
|
throw new Error('date_invalid'); return value; };
|
|
51
69
|
const assertTime = (value) => { if (!timePattern.test(value))
|
|
52
70
|
throw new Error('time_invalid'); return value; };
|
|
53
|
-
const claimsResource = (resourceType, id, claims) => Object.freeze({ resourceType, id: required(id, 'resource_id_required'), meta: Object.freeze({ claims: Object.freeze(claims) }) });
|
|
54
|
-
/** Builds
|
|
55
|
-
export function
|
|
71
|
+
const claimsResource = (resourceType, id, claims, native = {}, meta = {}) => Object.freeze({ resourceType, id: required(id, 'resource_id_required'), ...native, meta: Object.freeze({ ...meta, claims: Object.freeze(claims) }) });
|
|
72
|
+
/** Builds one physical FHIR R5 Location with SearchParameter claims. */
|
|
73
|
+
export function buildLocationResource(input) {
|
|
56
74
|
if (!Number.isFinite(input.latitude) || input.latitude < -90 || input.latitude > 90)
|
|
57
75
|
throw new Error('location_latitude_invalid');
|
|
58
76
|
if (!Number.isFinite(input.longitude) || input.longitude < -180 || input.longitude > 180)
|
|
@@ -62,26 +80,39 @@ export function buildVeterinaryLocationResource(input) {
|
|
|
62
80
|
if (parsed.protocol !== 'https:')
|
|
63
81
|
throw new Error('location_photo_url_invalid');
|
|
64
82
|
}
|
|
83
|
+
const organizationReference = required(input.organizationReference, 'location_organization_required');
|
|
84
|
+
const name = required(input.name, 'location_name_required');
|
|
85
|
+
const characteristicCodes = Object.freeze([...(input.characteristicCodes || [])]);
|
|
65
86
|
return claimsResource('Location', input.id, {
|
|
66
|
-
'Location.
|
|
67
|
-
|
|
68
|
-
'Location.
|
|
69
|
-
|
|
70
|
-
'Location.
|
|
71
|
-
...(
|
|
72
|
-
|
|
87
|
+
'Location.identifier': input.id,
|
|
88
|
+
'Location.status': 'active',
|
|
89
|
+
'Location.name': name,
|
|
90
|
+
'Location.near': `${input.latitude}|${input.longitude}`,
|
|
91
|
+
'Location.organization': organizationReference,
|
|
92
|
+
...(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 } }] }] } : {}),
|
|
73
100
|
});
|
|
74
101
|
}
|
|
75
102
|
/** Builds the claims-first FHIR R5 Schedule joining one practitioner and consultation Location. */
|
|
76
|
-
export function
|
|
103
|
+
export function buildScheduleResource(input) {
|
|
77
104
|
const start = assertDate(input.planningHorizonStart);
|
|
78
105
|
const end = assertDate(input.planningHorizonEnd);
|
|
79
106
|
if (start > end)
|
|
80
107
|
throw new Error('schedule_horizon_invalid');
|
|
108
|
+
const name = required(input.name, 'schedule_name_required');
|
|
109
|
+
const actors = Object.freeze([required(input.practitionerReference, 'schedule_practitioner_required'), required(input.locationReference, 'schedule_location_required')]);
|
|
81
110
|
return claimsResource('Schedule', input.id, {
|
|
82
|
-
'Schedule.active': true, 'Schedule.name':
|
|
83
|
-
'Schedule.actor':
|
|
84
|
-
|
|
111
|
+
'Schedule.identifier': input.id, 'Schedule.active': true, 'Schedule.name': name,
|
|
112
|
+
'Schedule.actor': actors, 'Schedule.date': Object.freeze([start, end]),
|
|
113
|
+
}, {
|
|
114
|
+
identifier: [{ value: input.id }], active: true, name,
|
|
115
|
+
actor: actors.map(reference => ({ reference })), planningHorizon: { start, end },
|
|
85
116
|
});
|
|
86
117
|
}
|
|
87
118
|
const minutes = (value) => { const [hour, minute] = assertTime(value).split(':').map(Number); return hour * 60 + minute; };
|
|
@@ -109,8 +140,14 @@ const matchesExceptionDate = (date, recurrence) => { const [, , day] = dateParts
|
|
|
109
140
|
const validateRange = (startTime, endTime, code) => { const start = minutes(startTime); const end = minutes(endTime); if (start >= end)
|
|
110
141
|
throw new Error(code); return [start, end]; };
|
|
111
142
|
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
|
+
}
|
|
112
149
|
/** Expands bounded weekly availability into claims-first Slots, applying recurring restrictions or expansions. */
|
|
113
|
-
export function
|
|
150
|
+
export function expandAvailability(input) {
|
|
114
151
|
const startDate = assertDate(input.startDate);
|
|
115
152
|
const endDate = assertDate(input.endDate);
|
|
116
153
|
if (startDate > endDate)
|
|
@@ -161,16 +198,18 @@ export function expandVeterinaryAvailability(input) {
|
|
|
161
198
|
}
|
|
162
199
|
}
|
|
163
200
|
}
|
|
164
|
-
const scheduleReference = `Schedule/${required(input.scheduleId, 'schedule_id_required')}`;
|
|
165
201
|
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 => {
|
|
166
202
|
const start = zonedInstant(slot.localDate, slot.localStart, input.timeZone);
|
|
167
203
|
const end = zonedInstant(slot.localDate, slot.localEnd, input.timeZone);
|
|
168
204
|
const id = `${input.scheduleId}-${slot.localDate}-${slot.localStart.replace(':', '')}-${slot.locationReference.split('/').at(-1)}`;
|
|
169
|
-
|
|
205
|
+
const scheduleReference = `Schedule/${scheduleIdForLocation(input.scheduleId, slot.locationReference)}`;
|
|
206
|
+
return Object.freeze({ ...slot, resource: claimsResource('Slot', id, {
|
|
207
|
+
'Slot.identifier': id, 'Slot.schedule': scheduleReference, 'Slot.status': SlotStatuses.Free, 'Slot.start': start,
|
|
208
|
+
}, { identifier: [{ value: id }], schedule: { reference: scheduleReference }, status: SlotStatuses.Free, start, end }) });
|
|
170
209
|
}));
|
|
171
210
|
}
|
|
172
211
|
/** Builds a claims-first response that confirms, declines or proposes a new appointment time. */
|
|
173
|
-
export function
|
|
212
|
+
export function buildAppointmentResponseResource(input) {
|
|
174
213
|
if (!Object.values(AppointmentResponseStatuses).includes(input.participantStatus))
|
|
175
214
|
throw new Error('appointment_response_status_invalid');
|
|
176
215
|
if ((input.start && !input.end) || (!input.start && input.end))
|
|
@@ -186,10 +225,30 @@ export function buildVeterinaryAppointmentResponseResource(input) {
|
|
|
186
225
|
throw new Error('appointment_response_notification_recipient_incomplete');
|
|
187
226
|
if (hasRecipientEmail && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(input.notificationRecipientEmail.trim()))
|
|
188
227
|
throw new Error('appointment_response_notification_email_invalid');
|
|
189
|
-
|
|
228
|
+
const appointmentReference = required(input.appointmentReference, 'appointment_reference_required');
|
|
229
|
+
const actorReference = required(input.actorReference, 'appointment_response_actor_required');
|
|
230
|
+
const lastUpdated = new Date(respondedAt).toISOString();
|
|
231
|
+
const contained = hasRecipientReference ? [buildAppointmentResponseNotificationResource({
|
|
232
|
+
id: `${input.id}-clinic-email`, responseReference: `AppointmentResponse/${input.id}`, appointmentReference,
|
|
233
|
+
senderReference: actorReference, recipientReference: input.notificationRecipientReference.trim(),
|
|
234
|
+
participantStatus: input.participantStatus, sentAt: lastUpdated, language: 'en',
|
|
235
|
+
}), { resourceType: 'Endpoint', id: `${input.id}-clinic-email-endpoint`, status: 'active', connectionType: [{ coding: [{ code: 'email' }] }], address: `mailto:${input.notificationRecipientEmail.trim().toLowerCase()}` }] : undefined;
|
|
236
|
+
return claimsResource('AppointmentResponse', input.id, {
|
|
237
|
+
'AppointmentResponse.identifier': input.id,
|
|
238
|
+
'AppointmentResponse.appointment': appointmentReference,
|
|
239
|
+
'AppointmentResponse.actor': actorReference,
|
|
240
|
+
'AppointmentResponse.part-status': input.participantStatus,
|
|
241
|
+
'AppointmentResponse._lastUpdated': lastUpdated,
|
|
242
|
+
}, {
|
|
243
|
+
identifier: [{ value: input.id }], appointment: { reference: appointmentReference }, actor: { reference: actorReference },
|
|
244
|
+
participantStatus: input.participantStatus,
|
|
245
|
+
...(input.proposedNewTime === undefined ? {} : { proposedNewTime: input.proposedNewTime }),
|
|
246
|
+
...(input.start ? { start: input.start, end: input.end } : {}),
|
|
247
|
+
...(contained ? { contained } : {}),
|
|
248
|
+
}, { lastUpdated });
|
|
190
249
|
}
|
|
191
250
|
/** Builds the FHIR-like Communication that an adapter may render as the additional English clinic email. */
|
|
192
|
-
export function
|
|
251
|
+
export function buildAppointmentResponseNotificationResource(input) {
|
|
193
252
|
if (input.language !== 'en')
|
|
194
253
|
throw new Error('appointment_response_notification_language_unsupported');
|
|
195
254
|
if (!Object.values(AppointmentResponseStatuses).includes(input.participantStatus))
|
|
@@ -209,8 +268,8 @@ export function buildVeterinaryAppointmentResponseNotificationResource(input) {
|
|
|
209
268
|
'Communication.text': `The appointment was ${statusText[input.participantStatus]}.`,
|
|
210
269
|
});
|
|
211
270
|
}
|
|
212
|
-
/** Builds a
|
|
213
|
-
export function
|
|
271
|
+
/** Builds a FHIR R5 Appointment whose indexed claims are SearchParameters. */
|
|
272
|
+
export function buildAppointmentResource(input) {
|
|
214
273
|
if (!Object.values(AppointmentStatuses).includes(input.status))
|
|
215
274
|
throw new Error('appointment_status_invalid');
|
|
216
275
|
const start = Date.parse(input.start);
|
|
@@ -225,26 +284,39 @@ export function buildVeterinaryAppointmentResource(input) {
|
|
|
225
284
|
const confirmationDueAt = input.confirmationDueAt || new Date(start - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
226
285
|
if (!Number.isFinite(Date.parse(confirmationDueAt)) || Date.parse(confirmationDueAt) >= start)
|
|
227
286
|
throw new Error('appointment_confirmation_due_invalid');
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
throw new Error('
|
|
287
|
+
const organizationReference = input.organizationReference || input.clinicRecipientReference;
|
|
288
|
+
const notificationEmail = input.notificationEmail || input.clinicRecipientEmail;
|
|
289
|
+
const hasOrganization = Boolean(organizationReference?.trim());
|
|
290
|
+
const hasNotificationEmail = Boolean(notificationEmail?.trim());
|
|
291
|
+
if (hasOrganization !== hasNotificationEmail)
|
|
292
|
+
throw new Error('appointment_notification_recipient_incomplete');
|
|
293
|
+
if (hasNotificationEmail && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(notificationEmail.trim()))
|
|
294
|
+
throw new Error('appointment_notification_email_invalid');
|
|
295
|
+
const startInstant = new Date(start).toISOString();
|
|
296
|
+
const endInstant = new Date(end).toISOString();
|
|
297
|
+
const organization = hasOrganization ? organizationReference.trim() : undefined;
|
|
298
|
+
const emailEndpoint = hasNotificationEmail ? {
|
|
299
|
+
resourceType: 'Endpoint', id: 'clinic-notification-email', status: 'active',
|
|
300
|
+
connectionType: [{ coding: [{ code: 'email' }] }],
|
|
301
|
+
address: `mailto:${notificationEmail.trim().toLowerCase()}`,
|
|
302
|
+
} : undefined;
|
|
234
303
|
return claimsResource('Appointment', input.id, {
|
|
235
|
-
'Appointment.status': input.status,
|
|
236
|
-
'Appointment.start': new Date(start).toISOString(),
|
|
237
|
-
'Appointment.end': new Date(end).toISOString(),
|
|
304
|
+
'Appointment.identifier': input.id, 'Appointment.status': input.status, 'Appointment.date': startInstant,
|
|
238
305
|
...(input.slotReferences.length ? { 'Appointment.slot': Object.freeze([...input.slotReferences]) } : {}),
|
|
239
306
|
'Appointment.actor': Object.freeze([...input.actorReferences]),
|
|
240
307
|
'Appointment.user-selected': userSelected,
|
|
241
|
-
...(
|
|
308
|
+
...(organization ? { 'Appointment.supporting-info': organization } : {}),
|
|
242
309
|
...(input.locationReference ? { 'Appointment.location': required(input.locationReference, 'appointment_location_required') } : {}),
|
|
243
|
-
|
|
310
|
+
}, {
|
|
311
|
+
identifier: [{ value: input.id }], status: input.status, start: startInstant, end: endInstant,
|
|
312
|
+
...(input.slotReferences.length ? { slot: input.slotReferences.map(reference => ({ reference })) } : {}),
|
|
313
|
+
participant: input.actorReferences.map(reference => ({ actor: { reference }, status: 'needs-action' })),
|
|
314
|
+
...(organization ? { supportingInformation: [{ reference: organization }, { reference: '#clinic-notification-email' }] } : {}),
|
|
315
|
+
...(emailEndpoint ? { contained: [emailEndpoint] } : {}),
|
|
244
316
|
});
|
|
245
317
|
}
|
|
246
318
|
/** Builds the durable web inbox notification when a schedule change displaces an appointment. */
|
|
247
|
-
export function
|
|
319
|
+
export function buildAppointmentNotificationResource(input) {
|
|
248
320
|
const id = required(input.id, 'communication_id_required');
|
|
249
321
|
return claimsResource('Communication', id, {
|
|
250
322
|
'Communication.identifier': id,
|
|
@@ -257,3 +329,23 @@ export function buildVeterinaryAppointmentNotificationResource(input) {
|
|
|
257
329
|
'Communication.content-code': 'https://vetchain.app/fhir/CodeSystem/appointment-action|reschedule',
|
|
258
330
|
});
|
|
259
331
|
}
|
|
332
|
+
/** @deprecated Use {@link SlotSearchParameters}. */
|
|
333
|
+
export const VeterinarySlotSearchParameters = SlotSearchParameters;
|
|
334
|
+
/** @deprecated Use {@link SchedulingFlatClaimCatalog}. */
|
|
335
|
+
export const VeterinarySchedulingFlatClaimCatalog = SchedulingFlatClaimCatalog;
|
|
336
|
+
/** @deprecated Use {@link normalizeSchedulingFlatClaimsResource}. */
|
|
337
|
+
export const normalizeVeterinarySchedulingFlatClaimsResource = normalizeSchedulingFlatClaimsResource;
|
|
338
|
+
/** @deprecated Use {@link buildLocationResource}. */
|
|
339
|
+
export const buildVeterinaryLocationResource = buildLocationResource;
|
|
340
|
+
/** @deprecated Use {@link buildScheduleResource}. */
|
|
341
|
+
export const buildVeterinaryScheduleResource = buildScheduleResource;
|
|
342
|
+
/** @deprecated Use {@link expandAvailability}. */
|
|
343
|
+
export const expandVeterinaryAvailability = expandAvailability;
|
|
344
|
+
/** @deprecated Use {@link buildAppointmentResponseResource}. */
|
|
345
|
+
export const buildVeterinaryAppointmentResponseResource = buildAppointmentResponseResource;
|
|
346
|
+
/** @deprecated Use {@link buildAppointmentResponseNotificationResource}. */
|
|
347
|
+
export const buildVeterinaryAppointmentResponseNotificationResource = buildAppointmentResponseNotificationResource;
|
|
348
|
+
/** @deprecated Use {@link buildAppointmentResource}. */
|
|
349
|
+
export const buildVeterinaryAppointmentResource = buildAppointmentResource;
|
|
350
|
+
/** @deprecated Use {@link buildAppointmentNotificationResource}. */
|
|
351
|
+
export const buildVeterinaryAppointmentNotificationResource = buildAppointmentNotificationResource;
|