vet-data-utils-ts 0.5.13 → 0.5.15

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.
@@ -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
- /** Normalizes an optional official organization telephone to E.164 using the selected jurisdiction calling code. */
54
- export declare function normalizeOptionalOrganizationOfficialPhone(value: unknown, defaultCallingCode: unknown): string | undefined;
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: normalizeOptionalOrganizationOfficialPhone(value.officialPhone, value.defaultPhoneCallingCode) } : {}),
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
- /** Normalizes an optional official organization telephone to E.164 using the selected jurisdiction calling code. */
104
- export function normalizeOptionalOrganizationOfficialPhone(value, defaultCallingCode) {
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);
@@ -1,4 +1,4 @@
1
- /** FHIR R5 Slot statuses accepted by veterinary scheduling. */
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 supported by flat claims. */
31
- export declare const VeterinarySlotSearchParameters: readonly ["identifier", "schedule", "service-category", "service-type", "specialty", "start", "status"];
32
- /** Governed operational flat claims accepted by GW VET scheduling managers. */
33
- export declare const VeterinarySchedulingFlatClaimCatalog: Readonly<{
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 string[];
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
- export type VeterinarySchedulingResourceType = keyof typeof VeterinarySchedulingFlatClaimCatalog;
41
- export type VeterinarySchedulingFlatClaimsResource = Readonly<{
42
- resourceType: VeterinarySchedulingResourceType;
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 the claims-only wire shape and normalizes scalar builder values to indexed strings. */
49
- export declare function normalizeVeterinarySchedulingFlatClaimsResource(input: unknown): VeterinarySchedulingFlatClaimsResource;
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 the claims-first veterinary projection of one physical FHIR R5 Location. */
58
- export declare function buildVeterinaryLocationResource(input: Readonly<{
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 buildVeterinaryScheduleResource(input: Readonly<{
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 VeterinaryAvailabilityPeriod = Readonly<{
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 VeterinaryAvailabilityException = Readonly<{
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 ExpandedVeterinarySlot = Readonly<{
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 expandVeterinaryAvailability(input: Readonly<{
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 VeterinaryAvailabilityPeriod[];
110
- exceptions: readonly VeterinaryAvailabilityException[];
111
- }>): readonly ExpandedVeterinarySlot[];
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 buildVeterinaryAppointmentResponseResource(input: Readonly<{
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 buildVeterinaryAppointmentResponseNotificationResource(input: Readonly<{
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 searchable claims-first FHIR R5 Appointment with a seven-day confirmation due time. */
137
- export declare function buildVeterinaryAppointmentResource(input: Readonly<{
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,11 +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;
153
+ clinicRecipientReference?: string;
154
+ clinicRecipientEmail?: string;
147
155
  }>): ClaimsFirstResource;
148
156
  /** Builds the durable web inbox notification when a schedule change displaces an appointment. */
149
- export declare function buildVeterinaryAppointmentNotificationResource(input: Readonly<{
157
+ export declare function buildAppointmentNotificationResource(input: Readonly<{
150
158
  id: string;
151
159
  appointmentReference: string;
152
160
  subjectReference: string;
153
161
  reason: string;
154
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;
@@ -1,4 +1,4 @@
1
- /** FHIR R5 Slot statuses accepted by veterinary scheduling. */
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 supported by flat claims. */
11
- export const VeterinarySlotSearchParameters = Object.freeze(['identifier', 'schedule', 'service-category', 'service-type', 'specialty', 'start', 'status']);
12
- /** Governed operational flat claims accepted by GW VET scheduling managers. */
13
- export const VeterinarySchedulingFlatClaimCatalog = Object.freeze({
14
- Location: Object.freeze(['Location.identifier', 'Location.status', 'Location.mode', 'Location.name', 'Location.description', 'Location.position.latitude', 'Location.position.longitude', 'Location.position.altitude', 'Location.managingOrganization', 'Location.characteristic', 'VeterinaryLocation.photoUrl']),
15
- Schedule: Object.freeze(['Schedule.identifier', 'Schedule.active', 'Schedule.name', 'Schedule.actor', 'Schedule.planningHorizon.start', 'Schedule.planningHorizon.end']),
16
- Slot: Object.freeze(['Slot.identifier', 'Slot.schedule', 'Slot.status', 'Slot.start', 'Slot.end', 'Slot.service-category', 'Slot.service-type', 'Slot.specialty', 'VeterinarySlot.location', 'VeterinarySlot.timeZone']),
17
- Appointment: Object.freeze(['Appointment.identifier', 'Appointment.status', 'Appointment.description', 'Appointment.note', 'Appointment.start', 'Appointment.end', 'Appointment.slot', 'Appointment.actor', 'Appointment.location', 'Appointment.user-selected', 'VeterinaryAppointment.confirmationDueAt', 'VeterinaryAppointment.reschedulingReason']),
18
- AppointmentResponse: Object.freeze(['AppointmentResponse.identifier', 'AppointmentResponse.appointment', 'AppointmentResponse.actor', 'AppointmentResponse.participantStatus', 'AppointmentResponse.proposedNewTime', 'AppointmentResponse.start', 'AppointmentResponse.end', 'VeterinaryAppointmentResponse.respondedAt', 'VeterinaryAppointmentResponse.recipient', 'VeterinaryAppointmentResponse.recipientEmail']),
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
- /** Validates the claims-only wire shape and normalizes scalar builder values to indexed strings. */
21
- export function normalizeVeterinarySchedulingFlatClaimsResource(input) {
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']),
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('veterinary_scheduling_flat_resource_invalid');
38
+ throw new TypeError('scheduling_flat_resource_invalid');
24
39
  const value = input;
25
- if (typeof value.resourceType !== 'string' || !(value.resourceType in VeterinarySchedulingFlatClaimCatalog) || Object.keys(value).some(key => !['resourceType', 'id', 'meta'].includes(key)))
26
- throw new TypeError('veterinary_scheduling_flat_resource_invalid');
27
- const id = required(String(value.id || ''), 'veterinary_scheduling_flat_resource_invalid');
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('veterinary_scheduling_flat_resource_invalid');
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) || Object.keys(meta).some(key => key !== 'claims'))
32
- throw new TypeError('veterinary_scheduling_flat_resource_invalid');
33
- const allowed = new Set(VeterinarySchedulingFlatClaimCatalog[value.resourceType]);
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('veterinary_scheduling_flat_claim_invalid');
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('veterinary_scheduling_flat_claim_invalid');
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({ resourceType: value.resourceType, id, meta: Object.freeze({ claims: Object.freeze(claims) }) });
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 the claims-first veterinary projection of one physical FHIR R5 Location. */
55
- export function buildVeterinaryLocationResource(input) {
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.status': 'active', 'Location.mode': 'instance', 'Location.name': required(input.name, 'location_name_required'),
67
- ...(input.description ? { 'Location.description': input.description.trim() } : {}),
68
- 'Location.position.latitude': input.latitude, 'Location.position.longitude': input.longitude,
69
- ...(input.altitude === undefined ? {} : { 'Location.position.altitude': input.altitude }),
70
- 'Location.managingOrganization': required(input.organizationReference, 'location_organization_required'),
71
- ...(input.characteristicCodes?.length ? { 'Location.characteristic': Object.freeze([...input.characteristicCodes]) } : {}),
72
- ...(input.photoUrl ? { 'VeterinaryLocation.photoUrl': input.photoUrl } : {}),
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 buildVeterinaryScheduleResource(input) {
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': required(input.name, 'schedule_name_required'),
83
- 'Schedule.actor': Object.freeze([required(input.practitionerReference, 'schedule_practitioner_required'), required(input.locationReference, 'schedule_location_required')]),
84
- 'Schedule.planningHorizon.start': start, 'Schedule.planningHorizon.end': end,
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 expandVeterinaryAvailability(input) {
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
- return Object.freeze({ ...slot, resource: claimsResource('Slot', id, { 'Slot.identifier': id, 'Slot.schedule': scheduleReference, 'Slot.status': SlotStatuses.Free, 'Slot.start': start, 'Slot.end': end, 'VeterinarySlot.location': slot.locationReference, 'VeterinarySlot.timeZone': input.timeZone }) });
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 buildVeterinaryAppointmentResponseResource(input) {
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
- return claimsResource('AppointmentResponse', input.id, { 'AppointmentResponse.appointment': required(input.appointmentReference, 'appointment_reference_required'), 'AppointmentResponse.actor': required(input.actorReference, 'appointment_response_actor_required'), 'AppointmentResponse.participantStatus': input.participantStatus, 'VeterinaryAppointmentResponse.respondedAt': new Date(respondedAt).toISOString(), ...(hasRecipientReference ? { 'VeterinaryAppointmentResponse.recipient': input.notificationRecipientReference.trim(), 'VeterinaryAppointmentResponse.recipientEmail': input.notificationRecipientEmail.trim().toLowerCase() } : {}), ...(input.proposedNewTime === undefined ? {} : { 'AppointmentResponse.proposedNewTime': input.proposedNewTime }), ...(input.start ? { 'AppointmentResponse.start': input.start, 'AppointmentResponse.end': input.end } : {}) });
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 buildVeterinaryAppointmentResponseNotificationResource(input) {
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 searchable claims-first FHIR R5 Appointment with a seven-day confirmation due time. */
213
- export function buildVeterinaryAppointmentResource(input) {
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,19 +284,33 @@ 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');
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;
228
298
  return claimsResource('Appointment', input.id, {
229
- 'Appointment.status': input.status,
230
- 'Appointment.start': new Date(start).toISOString(),
231
- 'Appointment.end': new Date(end).toISOString(),
299
+ 'Appointment.identifier': input.id, 'Appointment.status': input.status, 'Appointment.date': startInstant,
232
300
  ...(input.slotReferences.length ? { 'Appointment.slot': Object.freeze([...input.slotReferences]) } : {}),
233
301
  'Appointment.actor': Object.freeze([...input.actorReferences]),
234
302
  'Appointment.user-selected': userSelected,
303
+ ...(organization ? { 'Appointment.supporting-info': organization } : {}),
235
304
  ...(input.locationReference ? { 'Appointment.location': required(input.locationReference, 'appointment_location_required') } : {}),
236
- 'VeterinaryAppointment.confirmationDueAt': new Date(confirmationDueAt).toISOString(),
305
+ }, {
306
+ identifier: [{ value: input.id }], status: input.status, start: startInstant, end: endInstant,
307
+ ...(input.slotReferences.length ? { slot: input.slotReferences.map(reference => ({ reference })) } : {}),
308
+ participant: input.actorReferences.map(reference => ({ actor: { reference }, status: 'needs-action' })),
309
+ ...(organization ? { supportingInformation: [{ reference: organization }] } : {}),
237
310
  });
238
311
  }
239
312
  /** Builds the durable web inbox notification when a schedule change displaces an appointment. */
240
- export function buildVeterinaryAppointmentNotificationResource(input) {
313
+ export function buildAppointmentNotificationResource(input) {
241
314
  const id = required(input.id, 'communication_id_required');
242
315
  return claimsResource('Communication', id, {
243
316
  'Communication.identifier': id,
@@ -250,3 +323,23 @@ export function buildVeterinaryAppointmentNotificationResource(input) {
250
323
  'Communication.content-code': 'https://vetchain.app/fhir/CodeSystem/appointment-action|reschedule',
251
324
  });
252
325
  }
326
+ /** @deprecated Use {@link SlotSearchParameters}. */
327
+ export const VeterinarySlotSearchParameters = SlotSearchParameters;
328
+ /** @deprecated Use {@link SchedulingFlatClaimCatalog}. */
329
+ export const VeterinarySchedulingFlatClaimCatalog = SchedulingFlatClaimCatalog;
330
+ /** @deprecated Use {@link normalizeSchedulingFlatClaimsResource}. */
331
+ export const normalizeVeterinarySchedulingFlatClaimsResource = normalizeSchedulingFlatClaimsResource;
332
+ /** @deprecated Use {@link buildLocationResource}. */
333
+ export const buildVeterinaryLocationResource = buildLocationResource;
334
+ /** @deprecated Use {@link buildScheduleResource}. */
335
+ export const buildVeterinaryScheduleResource = buildScheduleResource;
336
+ /** @deprecated Use {@link expandAvailability}. */
337
+ export const expandVeterinaryAvailability = expandAvailability;
338
+ /** @deprecated Use {@link buildAppointmentResponseResource}. */
339
+ export const buildVeterinaryAppointmentResponseResource = buildAppointmentResponseResource;
340
+ /** @deprecated Use {@link buildAppointmentResponseNotificationResource}. */
341
+ export const buildVeterinaryAppointmentResponseNotificationResource = buildAppointmentResponseNotificationResource;
342
+ /** @deprecated Use {@link buildAppointmentResource}. */
343
+ export const buildVeterinaryAppointmentResource = buildAppointmentResource;
344
+ /** @deprecated Use {@link buildAppointmentNotificationResource}. */
345
+ export const buildVeterinaryAppointmentNotificationResource = buildAppointmentNotificationResource;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-data-utils-ts",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "Browser-safe governed VetChain data contracts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",