vet-data-utils-ts 0.5.10 → 0.5.14

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.
@@ -23,6 +23,7 @@ export type ReusableOrganizationApplication = Readonly<{
23
23
  regionalId: boolean;
24
24
  subdivisionCode?: string;
25
25
  officialLicense?: string;
26
+ officialPhone?: string;
26
27
  legalRepresentative: Readonly<{
27
28
  name: string;
28
29
  email: string;
@@ -49,3 +50,5 @@ export declare function organizationServiceTypeForParticipationRoles(roles: read
49
50
  * object and remain inside the BFF/high-level Node runtime.
50
51
  */
51
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;
@@ -91,6 +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
95
  legalRepresentative: Object.freeze({
95
96
  name: required(legalRepresentative.name, 'organization_legal_representative_name_required'),
96
97
  email: legalEmail,
@@ -99,6 +100,24 @@ export function parseReusableOrganizationApplication(input) {
99
100
  participationRoles: Object.freeze([...new Set(selectedRoles)]),
100
101
  });
101
102
  }
103
+ /** Normalizes an optional official organization telephone to E.164 using the selected jurisdiction calling code. */
104
+ export function normalizeOptionalOrganizationOfficialPhone(value, defaultCallingCode) {
105
+ const raw = optional(value);
106
+ if (!raw)
107
+ return undefined;
108
+ let compact = raw.replace(/^tel:/i, '').replace(/[\s().-]/g, '');
109
+ if (compact.startsWith('00'))
110
+ compact = `+${compact.slice(2)}`;
111
+ if (!compact.startsWith('+')) {
112
+ const callingCode = String(defaultCallingCode ?? '').trim().replace(/^00/, '+');
113
+ if (!/^\+[1-9]\d{0,2}$/.test(callingCode))
114
+ throw new TypeError('organization_official_phone_calling_code_required');
115
+ compact = `${callingCode}${compact.replace(/^0/, '')}`;
116
+ }
117
+ if (!/^\+[1-9]\d{7,14}$/.test(compact))
118
+ throw new TypeError('organization_official_phone_invalid');
119
+ return compact;
120
+ }
102
121
  function record(value, error) {
103
122
  if (!value || typeof value !== 'object' || Array.isArray(value))
104
123
  throw new TypeError(error);
@@ -115,9 +115,23 @@ export declare function buildVeterinaryAppointmentResponseResource(input: Readon
115
115
  appointmentReference: string;
116
116
  actorReference: string;
117
117
  participantStatus: string;
118
+ respondedAt?: string;
118
119
  proposedNewTime?: boolean;
119
120
  start?: string;
120
121
  end?: string;
122
+ notificationRecipientReference?: string;
123
+ notificationRecipientEmail?: string;
124
+ }>): ClaimsFirstResource;
125
+ /** Builds the FHIR-like Communication that an adapter may render as the additional English clinic email. */
126
+ export declare function buildVeterinaryAppointmentResponseNotificationResource(input: Readonly<{
127
+ id: string;
128
+ responseReference: string;
129
+ appointmentReference: string;
130
+ senderReference: string;
131
+ recipientReference: string;
132
+ participantStatus: string;
133
+ sentAt: string;
134
+ language: string;
121
135
  }>): ClaimsFirstResource;
122
136
  /** Builds a searchable claims-first FHIR R5 Appointment with a seven-day confirmation due time. */
123
137
  export declare function buildVeterinaryAppointmentResource(input: Readonly<{
@@ -129,6 +143,9 @@ export declare function buildVeterinaryAppointmentResource(input: Readonly<{
129
143
  actorReferences: readonly string[];
130
144
  locationReference?: string;
131
145
  confirmationDueAt?: string;
146
+ userSelected?: boolean;
147
+ clinicRecipientReference?: string;
148
+ clinicRecipientEmail?: string;
132
149
  }>): ClaimsFirstResource;
133
150
  /** Builds the durable web inbox notification when a schedule change displaces an appointment. */
134
151
  export declare function buildVeterinaryAppointmentNotificationResource(input: Readonly<{
@@ -14,8 +14,8 @@ export const VeterinarySchedulingFlatClaimCatalog = Object.freeze({
14
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
15
  Schedule: Object.freeze(['Schedule.identifier', 'Schedule.active', 'Schedule.name', 'Schedule.actor', 'Schedule.planningHorizon.start', 'Schedule.planningHorizon.end']),
16
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', 'VeterinaryAppointment.confirmationDueAt', 'VeterinaryAppointment.reschedulingReason']),
18
- AppointmentResponse: Object.freeze(['AppointmentResponse.identifier', 'AppointmentResponse.appointment', 'AppointmentResponse.actor', 'AppointmentResponse.participantStatus', 'AppointmentResponse.proposedNewTime', 'AppointmentResponse.start', 'AppointmentResponse.end']),
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', 'VeterinaryAppointment.clinicRecipient', 'VeterinaryAppointment.clinicEmail']),
18
+ AppointmentResponse: Object.freeze(['AppointmentResponse.identifier', 'AppointmentResponse.appointment', 'AppointmentResponse.actor', 'AppointmentResponse.participantStatus', 'AppointmentResponse.proposedNewTime', 'AppointmentResponse.start', 'AppointmentResponse.end', 'VeterinaryAppointmentResponse.respondedAt', 'VeterinaryAppointmentResponse.recipient', 'VeterinaryAppointmentResponse.recipientEmail']),
19
19
  });
20
20
  /** Validates the claims-only wire shape and normalizes scalar builder values to indexed strings. */
21
21
  export function normalizeVeterinarySchedulingFlatClaimsResource(input) {
@@ -177,7 +177,37 @@ export function buildVeterinaryAppointmentResponseResource(input) {
177
177
  throw new Error('appointment_response_time_incomplete');
178
178
  if (input.start && input.end && Date.parse(input.start) >= Date.parse(input.end))
179
179
  throw new Error('appointment_response_time_invalid');
180
- 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, ...(input.proposedNewTime === undefined ? {} : { 'AppointmentResponse.proposedNewTime': input.proposedNewTime }), ...(input.start ? { 'AppointmentResponse.start': input.start, 'AppointmentResponse.end': input.end } : {}) });
180
+ const respondedAt = input.respondedAt || new Date().toISOString();
181
+ if (!Number.isFinite(Date.parse(respondedAt)))
182
+ throw new Error('appointment_response_responded_at_invalid');
183
+ const hasRecipientReference = Boolean(input.notificationRecipientReference?.trim());
184
+ const hasRecipientEmail = Boolean(input.notificationRecipientEmail?.trim());
185
+ if (hasRecipientReference !== hasRecipientEmail)
186
+ throw new Error('appointment_response_notification_recipient_incomplete');
187
+ if (hasRecipientEmail && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(input.notificationRecipientEmail.trim()))
188
+ 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 } : {}) });
190
+ }
191
+ /** Builds the FHIR-like Communication that an adapter may render as the additional English clinic email. */
192
+ export function buildVeterinaryAppointmentResponseNotificationResource(input) {
193
+ if (input.language !== 'en')
194
+ throw new Error('appointment_response_notification_language_unsupported');
195
+ if (!Object.values(AppointmentResponseStatuses).includes(input.participantStatus))
196
+ throw new Error('appointment_response_status_invalid');
197
+ if (!Number.isFinite(Date.parse(input.sentAt)))
198
+ throw new Error('appointment_response_notification_sent_at_invalid');
199
+ const statusText = Object.freeze({ accepted: 'accepted', declined: 'declined', tentative: 'tentatively changed', 'needs-action': 'left awaiting action', 'entered-in-error': 'marked as entered in error' });
200
+ return claimsResource('Communication', input.id, {
201
+ 'Communication.identifier': input.id,
202
+ 'Communication.status': 'completed',
203
+ 'Communication.category': 'http://terminology.hl7.org/CodeSystem/communication-category|notification',
204
+ 'Communication.recipient': required(input.recipientReference, 'appointment_response_notification_recipient_required'),
205
+ 'Communication.sender': required(input.senderReference, 'appointment_response_notification_sender_required'),
206
+ 'Communication.sent': new Date(input.sentAt).toISOString(),
207
+ 'Communication.topic': 'https://vetchain.app/fhir/CodeSystem/communication-topic|appointment-response',
208
+ 'Communication.content-reference': Object.freeze([required(input.responseReference, 'appointment_response_reference_required'), required(input.appointmentReference, 'appointment_reference_required')]),
209
+ 'Communication.text': `The appointment was ${statusText[input.participantStatus]}.`,
210
+ });
181
211
  }
182
212
  /** Builds a searchable claims-first FHIR R5 Appointment with a seven-day confirmation due time. */
183
213
  export function buildVeterinaryAppointmentResource(input) {
@@ -187,19 +217,28 @@ export function buildVeterinaryAppointmentResource(input) {
187
217
  const end = Date.parse(input.end);
188
218
  if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end)
189
219
  throw new Error('appointment_time_invalid');
190
- if (!input.slotReferences.length)
220
+ const userSelected = input.userSelected ?? false;
221
+ if (!input.slotReferences.length && !userSelected)
191
222
  throw new Error('appointment_slot_required');
192
223
  if (!input.actorReferences.length)
193
224
  throw new Error('appointment_actor_required');
194
225
  const confirmationDueAt = input.confirmationDueAt || new Date(start - 7 * 24 * 60 * 60 * 1000).toISOString();
195
226
  if (!Number.isFinite(Date.parse(confirmationDueAt)) || Date.parse(confirmationDueAt) >= start)
196
227
  throw new Error('appointment_confirmation_due_invalid');
228
+ const hasClinicRecipient = Boolean(input.clinicRecipientReference?.trim());
229
+ const hasClinicEmail = Boolean(input.clinicRecipientEmail?.trim());
230
+ if (hasClinicRecipient !== hasClinicEmail)
231
+ throw new Error('appointment_clinic_notification_recipient_incomplete');
232
+ if (hasClinicEmail && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(input.clinicRecipientEmail.trim()))
233
+ throw new Error('appointment_clinic_email_invalid');
197
234
  return claimsResource('Appointment', input.id, {
198
235
  'Appointment.status': input.status,
199
236
  'Appointment.start': new Date(start).toISOString(),
200
237
  'Appointment.end': new Date(end).toISOString(),
201
- 'Appointment.slot': Object.freeze([...input.slotReferences]),
238
+ ...(input.slotReferences.length ? { 'Appointment.slot': Object.freeze([...input.slotReferences]) } : {}),
202
239
  'Appointment.actor': Object.freeze([...input.actorReferences]),
240
+ 'Appointment.user-selected': userSelected,
241
+ ...(hasClinicRecipient ? { 'VeterinaryAppointment.clinicRecipient': input.clinicRecipientReference.trim(), 'VeterinaryAppointment.clinicEmail': input.clinicRecipientEmail.trim().toLowerCase() } : {}),
203
242
  ...(input.locationReference ? { 'Appointment.location': required(input.locationReference, 'appointment_location_required') } : {}),
204
243
  'VeterinaryAppointment.confirmationDueAt': new Date(confirmationDueAt).toISOString(),
205
244
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-data-utils-ts",
3
- "version": "0.5.10",
3
+ "version": "0.5.14",
4
4
  "description": "Browser-safe governed VetChain data contracts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",