@veladent/booking-core 0.0.0-stage → 1.0.4-dev.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/index.js +499 -0
- package/package.json +36 -4
- package/types/booking/core/src/client.d.ts +21 -0
- package/types/booking/core/src/index.d.ts +5 -0
- package/types/booking/core/src/locale.d.ts +113 -0
- package/types/booking/core/src/timezone.d.ts +10 -0
- package/types/booking/core/src/types.d.ts +135 -0
- package/types/booking/core/src/validation.d.ts +9 -0
package/README.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Veladent booking core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework-independent booking API client, types, locale helpers, and validation.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @veladent/booking-core
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { createBookingClient } from "@veladent/booking-core";
|
|
11
|
+
|
|
12
|
+
const client = createBookingClient({
|
|
13
|
+
baseUrl: "YOUR_API_BASE_URL",
|
|
14
|
+
publishableKey: "YOUR_SITE_PUBLISHABLE_KEY",
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Use `@dev` or `@sandbox` to install a public prerelease. The default channel is
|
|
19
|
+
stable (`latest`). Supply your API URL and site publishable key at runtime.
|
|
20
|
+
Never supply a server secret or npm token to a browser application.
|
|
21
|
+
|
|
22
|
+
For the React UI, install `@veladent/booking-widget`.
|
package/index.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
function normalizeEmail(value) {
|
|
2
|
+
return value.trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
function normalizePhone(value) {
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
if (trimmed.startsWith("00")) {
|
|
7
|
+
return `+${trimmed.slice(2).replace(/\D/g, "")}`;
|
|
8
|
+
}
|
|
9
|
+
if (trimmed.startsWith("+")) {
|
|
10
|
+
return `+${trimmed.slice(1).replace(/\D/g, "")}`;
|
|
11
|
+
}
|
|
12
|
+
return trimmed.replace(/\D/g, "");
|
|
13
|
+
}
|
|
14
|
+
function isLikelyE164(value) {
|
|
15
|
+
return /^\+[1-9]\d{7,14}$/.test(value);
|
|
16
|
+
}
|
|
17
|
+
function validateContact(contact) {
|
|
18
|
+
if (!normalizePhone(contact.phone)) {
|
|
19
|
+
return "phone";
|
|
20
|
+
}
|
|
21
|
+
if (!contact.fullName.trim()) {
|
|
22
|
+
return "fullName";
|
|
23
|
+
}
|
|
24
|
+
if (!/^\S+@\S+\.\S+$/.test(normalizeEmail(contact.email))) {
|
|
25
|
+
return "email";
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function validateExistingPhone(phone) {
|
|
30
|
+
return isLikelyE164(normalizePhone(phone)) ? null : "phone";
|
|
31
|
+
}
|
|
32
|
+
function validateVerificationCode(code) {
|
|
33
|
+
return /^\d{6}$/.test(code.trim()) ? null : "verificationCode";
|
|
34
|
+
}
|
|
35
|
+
function isValidPastDateOfBirth(value) {
|
|
36
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
40
|
+
if (Number.isNaN(date.getTime())) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const now = /* @__PURE__ */ new Date();
|
|
44
|
+
const todayUtc = Date.UTC(
|
|
45
|
+
now.getUTCFullYear(),
|
|
46
|
+
now.getUTCMonth(),
|
|
47
|
+
now.getUTCDate()
|
|
48
|
+
);
|
|
49
|
+
return date.getTime() <= todayUtc && date.getUTCFullYear() >= 1900;
|
|
50
|
+
}
|
|
51
|
+
function validatePatientDetails(details) {
|
|
52
|
+
if (!details) {
|
|
53
|
+
return "patientDetails";
|
|
54
|
+
}
|
|
55
|
+
if (!isValidPastDateOfBirth(details.dateOfBirth)) {
|
|
56
|
+
return "dateOfBirth";
|
|
57
|
+
}
|
|
58
|
+
if (!details.gender.trim()) {
|
|
59
|
+
return "gender";
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
class BookingApiError extends Error {
|
|
64
|
+
constructor(message, status, code) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "BookingApiError";
|
|
67
|
+
this.status = status;
|
|
68
|
+
this.code = code;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function joinUrl(baseUrl, path) {
|
|
72
|
+
return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
73
|
+
}
|
|
74
|
+
function normalizeConfig(config) {
|
|
75
|
+
const candidate = config;
|
|
76
|
+
return {
|
|
77
|
+
...config,
|
|
78
|
+
doctors: Array.isArray(config.doctors) ? config.doctors : [],
|
|
79
|
+
locale: config.locale || "en",
|
|
80
|
+
services: Array.isArray(config.services) ? config.services : [],
|
|
81
|
+
siteAddress: config.siteAddress ?? null,
|
|
82
|
+
siteLogoUrl: config.siteLogoUrl ?? null,
|
|
83
|
+
siteName: config.siteName ?? null,
|
|
84
|
+
sitePhone: config.sitePhone ?? null,
|
|
85
|
+
theme: config.theme ?? candidate.themeConfig ?? null,
|
|
86
|
+
timeZone: config.timeZone || candidate.timezone || "UTC",
|
|
87
|
+
treatments: Array.isArray(config.treatments) ? config.treatments : []
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function createBookingClient(options) {
|
|
91
|
+
const fetcher = options.fetcher ?? globalThis.fetch.bind(globalThis);
|
|
92
|
+
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
93
|
+
const headers = {
|
|
94
|
+
Accept: "application/json",
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
"X-Veladent-Publishable-Key": options.publishableKey
|
|
97
|
+
};
|
|
98
|
+
async function request(path, init = {}) {
|
|
99
|
+
const response = await fetcher(joinUrl(baseUrl, path), {
|
|
100
|
+
...init,
|
|
101
|
+
cache: "no-store",
|
|
102
|
+
headers: {
|
|
103
|
+
...headers,
|
|
104
|
+
...init.headers
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const body = await response.json().catch(() => null);
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const errorBody = body;
|
|
110
|
+
throw new BookingApiError(
|
|
111
|
+
errorBody?.message || "The booking request failed",
|
|
112
|
+
response.status,
|
|
113
|
+
errorBody?.code
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return body;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
cancelPayment(sessionId) {
|
|
120
|
+
return request(
|
|
121
|
+
`/api/v1/public/booking/payments/${encodeURIComponent(sessionId)}/cancel`,
|
|
122
|
+
{ method: "POST" }
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
createRequest(input) {
|
|
126
|
+
return request("/api/v1/public/booking/requests", {
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
bookingMode: input.bookingMode,
|
|
129
|
+
checkoutRequestId: input.checkoutRequestId,
|
|
130
|
+
contact: {
|
|
131
|
+
...input.contact,
|
|
132
|
+
email: normalizeEmail(input.contact.email),
|
|
133
|
+
phone: normalizePhone(input.contact.phone)
|
|
134
|
+
},
|
|
135
|
+
doctorId: input.doctorId,
|
|
136
|
+
end: input.end,
|
|
137
|
+
identity: input.bookingMode === "existing" && input.contact.firstName && input.contact.lastName && input.patientDetails.dateOfBirth ? {
|
|
138
|
+
dateOfBirth: input.patientDetails.dateOfBirth,
|
|
139
|
+
firstName: input.contact.firstName,
|
|
140
|
+
lastName: input.contact.lastName
|
|
141
|
+
} : void 0,
|
|
142
|
+
patientDetails: input.bookingMode === "new" ? input.patientDetails : void 0,
|
|
143
|
+
start: input.start,
|
|
144
|
+
treatmentId: input.treatmentId,
|
|
145
|
+
verificationCode: input.verificationCode
|
|
146
|
+
}),
|
|
147
|
+
method: "POST"
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
getConfig(signal) {
|
|
151
|
+
return request("/api/v1/public/booking/config", {
|
|
152
|
+
signal
|
|
153
|
+
}).then(normalizeConfig);
|
|
154
|
+
},
|
|
155
|
+
getPaymentStatus(sessionId) {
|
|
156
|
+
return request(
|
|
157
|
+
`/api/v1/public/booking/payments/${encodeURIComponent(sessionId)}`
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
getSlots(input) {
|
|
161
|
+
const params = new URLSearchParams({ treatmentId: input.treatmentId });
|
|
162
|
+
if (input.fromDate && input.toDate) {
|
|
163
|
+
params.set("fromDate", input.fromDate);
|
|
164
|
+
params.set("toDate", input.toDate);
|
|
165
|
+
} else {
|
|
166
|
+
params.set("date", input.date);
|
|
167
|
+
}
|
|
168
|
+
if (input.doctorId) {
|
|
169
|
+
params.set("doctorId", input.doctorId);
|
|
170
|
+
}
|
|
171
|
+
return request(
|
|
172
|
+
`/api/v1/public/booking/slots?${params}`,
|
|
173
|
+
{ signal: input.signal }
|
|
174
|
+
).then((body) => Array.isArray(body) ? body : body.slots);
|
|
175
|
+
},
|
|
176
|
+
requestVerification(input) {
|
|
177
|
+
return request(
|
|
178
|
+
"/api/v1/public/booking/verification/request",
|
|
179
|
+
{
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
bookingMode: input.bookingMode,
|
|
182
|
+
identity: input.identity,
|
|
183
|
+
phone: normalizePhone(input.phone)
|
|
184
|
+
}),
|
|
185
|
+
method: "POST"
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
verificationStorageKey: `veladent-booking:verification:${baseUrl}:${options.publishableKey}`,
|
|
190
|
+
verifyCode(input) {
|
|
191
|
+
return request(
|
|
192
|
+
"/api/v1/public/booking/verification/check",
|
|
193
|
+
{
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
bookingMode: input.bookingMode,
|
|
196
|
+
code: input.code.trim(),
|
|
197
|
+
identity: input.identity,
|
|
198
|
+
phone: normalizePhone(input.phone)
|
|
199
|
+
}),
|
|
200
|
+
method: "POST"
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const englishCopy = {
|
|
207
|
+
addressLine1: "Address line 1",
|
|
208
|
+
anyPractitioner: "Any practitioner",
|
|
209
|
+
anyPractitionerDescription: "Let the clinic choose the best available practitioner",
|
|
210
|
+
appointmentSummary: "Appointment summary",
|
|
211
|
+
appointmentTitle: "Book an appointment",
|
|
212
|
+
back: "Back",
|
|
213
|
+
balance: "Balance due after appointment",
|
|
214
|
+
bookAgain: "Book another appointment",
|
|
215
|
+
bookingDisabled: "This practice isn’t accepting online bookings right now.",
|
|
216
|
+
bookingFailed: "We could not complete the booking. Please try again.",
|
|
217
|
+
bookingUnavailableDescription: "We couldn’t load online booking right now. Please try again in a moment, or contact the practice for help.",
|
|
218
|
+
bookingUnavailableTitle: "Online booking is unavailable",
|
|
219
|
+
callPractice: "Call us",
|
|
220
|
+
cancelPayment: "Cancel payment and edit booking",
|
|
221
|
+
checkingPayment: "Checking payment status.",
|
|
222
|
+
checkPayment: "Check payment status",
|
|
223
|
+
chooseAnotherDate: "Please choose another date or contact the practice.",
|
|
224
|
+
chooseDate: "Choose a date",
|
|
225
|
+
chooseDateTime: "Choose a date & time",
|
|
226
|
+
chooseDoctor: "Choose practitioner",
|
|
227
|
+
chooseService: "What do you need?",
|
|
228
|
+
chooseTime: "Available times",
|
|
229
|
+
chooseTreatment: "Choose a treatment",
|
|
230
|
+
clinic: "Clinic",
|
|
231
|
+
close: "Close",
|
|
232
|
+
codeExpired: "This code has expired. Request a new code.",
|
|
233
|
+
codeExpiresIn: "Code expires in {time}",
|
|
234
|
+
confirmationDescription: "Your booking is complete. We look forward to seeing you.",
|
|
235
|
+
confirmCode: "Verify code",
|
|
236
|
+
confirmed: "Your appointment is confirmed.",
|
|
237
|
+
contactPractice: "Please contact the practice for help.",
|
|
238
|
+
contactSite: "Can’t see the appointment you need? Call {phone} to discuss your options.",
|
|
239
|
+
continue: "Continue",
|
|
240
|
+
dateOfBirth: "Date of birth",
|
|
241
|
+
deposit: "Deposit",
|
|
242
|
+
email: "Email address",
|
|
243
|
+
existingPatient: "Yes",
|
|
244
|
+
existingPatientDescription: "I'm an existing patient",
|
|
245
|
+
findPatientRecord: "Find your patient record",
|
|
246
|
+
firstName: "First name",
|
|
247
|
+
fullName: "Full name",
|
|
248
|
+
gender: "Gender",
|
|
249
|
+
genderOptions: {
|
|
250
|
+
female: "Female",
|
|
251
|
+
male: "Male",
|
|
252
|
+
other: "Other",
|
|
253
|
+
preferNotToSay: "Prefer not to say"
|
|
254
|
+
},
|
|
255
|
+
genderPlaceholder: "Select gender",
|
|
256
|
+
goBackAndEdit: "Go back and edit",
|
|
257
|
+
identityUnavailable: "We didn't find your patient record",
|
|
258
|
+
identityUnavailableDescription: "Your details may have changed, or there may be a mistake on your patient record. Please call the practice to verify your identity and update your contact details.",
|
|
259
|
+
invalidInput: "Please check the booking details and try again.",
|
|
260
|
+
lastName: "Last name",
|
|
261
|
+
loadingSlots: "Loading available times…",
|
|
262
|
+
loginTitle: "Log in to your account",
|
|
263
|
+
marketingOptIn: "Yes, I’d like to hear about news and special offers",
|
|
264
|
+
newPatient: "No",
|
|
265
|
+
newPatientDescription: "I'm a new patient",
|
|
266
|
+
noDoctors: "No practitioners are available for online booking.",
|
|
267
|
+
noMatchingServices: "No services match your search.",
|
|
268
|
+
noServices: "There are no services available for online booking.",
|
|
269
|
+
noSlots: "There are no available times on this date.",
|
|
270
|
+
noTreatments: "There are no treatments available for this service.",
|
|
271
|
+
otpCode: "6-digit code",
|
|
272
|
+
otpDescription: "Enter the 6-digit code sent to your mobile number.",
|
|
273
|
+
otpSent: "We sent a verification code to",
|
|
274
|
+
patientDetails: "Patient details",
|
|
275
|
+
patientDetailsRequired: "Please add the patient details so we can create the patient record.",
|
|
276
|
+
patientNoMatch: "We didn't find a match",
|
|
277
|
+
patientNoMatchDescription: "We searched for records matching these details.",
|
|
278
|
+
paymentAtPractice: "Pay the full treatment price securely with Stripe to confirm your appointment.",
|
|
279
|
+
paymentExpired: "The payment session has expired. Choose an appointment time and try again.",
|
|
280
|
+
paymentPending: "Complete payment in the Stripe tab. Your appointment will be confirmed automatically after payment.",
|
|
281
|
+
paymentReceived: "Payment received",
|
|
282
|
+
paymentStatusUnavailable: "We couldn’t check payment status. Please try checking again. Your payment may still be processing.",
|
|
283
|
+
payWithStripe: "Pay securely with Stripe",
|
|
284
|
+
pendingConfirmation: "Your appointment request is waiting for clinic confirmation.",
|
|
285
|
+
phone: "Mobile number",
|
|
286
|
+
phoneCountry: "Phone country and calling code",
|
|
287
|
+
phoneCountryCodeHint: "Include your country code, for example +91 8799657887 or +44 7700900123.",
|
|
288
|
+
postcode: "Postcode",
|
|
289
|
+
practitionerDescription: "Choose a practitioner or let us find the next available appointment",
|
|
290
|
+
preparingPayment: "Preparing your secure payment…",
|
|
291
|
+
price: "Price",
|
|
292
|
+
registeredQuestion: "Are you registered as a patient at this practice?",
|
|
293
|
+
requestCode: "Send verification code",
|
|
294
|
+
resendCode: "Resend code",
|
|
295
|
+
resendCodeIn: "Resend code in {seconds}s",
|
|
296
|
+
retry: "Try again",
|
|
297
|
+
reviewPayment: "Complete your payment",
|
|
298
|
+
searchServices: "Search services",
|
|
299
|
+
selectDateFirst: "Choose a date to see available times.",
|
|
300
|
+
serviceDescription: "Choose a service to continue",
|
|
301
|
+
slotsLoadFailed: "We couldn’t load available times. Please try again or choose another date.",
|
|
302
|
+
somethingElse: "Something else",
|
|
303
|
+
submit: "Book appointment",
|
|
304
|
+
submitting: "Booking appointment...",
|
|
305
|
+
title: "Book online",
|
|
306
|
+
titleLabel: "Title",
|
|
307
|
+
titlePlaceholder: "Select title",
|
|
308
|
+
total: "Total",
|
|
309
|
+
treatmentDescription: "Choose an appointment",
|
|
310
|
+
verificationReminders: "We use your mobile number for account verification and appointment reminders.",
|
|
311
|
+
verifyPhone: "Verify your mobile number",
|
|
312
|
+
yourDetails: "Contact details"
|
|
313
|
+
};
|
|
314
|
+
const arabicCopy = {
|
|
315
|
+
...englishCopy,
|
|
316
|
+
addressLine1: "العنوان",
|
|
317
|
+
anyPractitioner: "أي طبيب",
|
|
318
|
+
anyPractitionerDescription: "دع العيادة تختار الطبيب المتاح الأنسب",
|
|
319
|
+
appointmentSummary: "ملخص الموعد",
|
|
320
|
+
appointmentTitle: "حجز موعد",
|
|
321
|
+
back: "رجوع",
|
|
322
|
+
balance: "المبلغ المتبقي بعد الموعد",
|
|
323
|
+
bookAgain: "حجز موعد آخر",
|
|
324
|
+
bookingDisabled: "لا تقبل هذه العيادة الحجز عبر الإنترنت حاليًا.",
|
|
325
|
+
bookingFailed: "تعذر إكمال الحجز. يرجى المحاولة مرة أخرى.",
|
|
326
|
+
bookingUnavailableDescription: "تعذر تحميل الحجز الآن. حاول مرة أخرى بعد قليل أو تواصل مع العيادة للمساعدة.",
|
|
327
|
+
bookingUnavailableTitle: "الحجز عبر الإنترنت غير متاح",
|
|
328
|
+
callPractice: "اتصل بنا",
|
|
329
|
+
cancelPayment: "إلغاء الدفع وتعديل الحجز",
|
|
330
|
+
checkingPayment: "جارٍ التحقق من حالة الدفع.",
|
|
331
|
+
checkPayment: "التحقق من حالة الدفع",
|
|
332
|
+
chooseAnotherDate: "يرجى اختيار تاريخ آخر أو الاتصال بالعيادة.",
|
|
333
|
+
chooseDate: "اختر التاريخ",
|
|
334
|
+
chooseDateTime: "اختر التاريخ والوقت",
|
|
335
|
+
chooseDoctor: "اختر الطبيب",
|
|
336
|
+
chooseService: "ما الذي تحتاجه؟",
|
|
337
|
+
chooseTime: "الأوقات المتاحة",
|
|
338
|
+
chooseTreatment: "اختر العلاج",
|
|
339
|
+
clinic: "العيادة",
|
|
340
|
+
close: "إغلاق",
|
|
341
|
+
codeExpired: "انتهت صلاحية الرمز. اطلب رمزًا جديدًا.",
|
|
342
|
+
codeExpiresIn: "تنتهي صلاحية الرمز خلال {time}",
|
|
343
|
+
confirmationDescription: "اكتمل حجزك. نتطلع إلى رؤيتك.",
|
|
344
|
+
confirmCode: "تحقق من الرمز",
|
|
345
|
+
confirmed: "تم تأكيد موعدك.",
|
|
346
|
+
contactPractice: "يرجى الاتصال بالعيادة للمساعدة.",
|
|
347
|
+
contactSite: "لا ترى الموعد الذي تحتاجه؟ اتصل بـ {phone} لمناقشة الخيارات.",
|
|
348
|
+
continue: "متابعة",
|
|
349
|
+
dateOfBirth: "تاريخ الميلاد",
|
|
350
|
+
deposit: "العربون",
|
|
351
|
+
email: "البريد الإلكتروني",
|
|
352
|
+
existingPatient: "نعم",
|
|
353
|
+
existingPatientDescription: "أنا مريض حالي",
|
|
354
|
+
findPatientRecord: "البحث عن سجل المريض",
|
|
355
|
+
firstName: "الاسم الأول",
|
|
356
|
+
fullName: "الاسم الكامل",
|
|
357
|
+
gender: "الجنس",
|
|
358
|
+
genderOptions: {
|
|
359
|
+
female: "أنثى",
|
|
360
|
+
male: "ذكر",
|
|
361
|
+
other: "آخر",
|
|
362
|
+
preferNotToSay: "أفضل عدم الإفصاح"
|
|
363
|
+
},
|
|
364
|
+
genderPlaceholder: "اختر الجنس",
|
|
365
|
+
goBackAndEdit: "العودة وتعديل البيانات",
|
|
366
|
+
identityUnavailable: "لا يمكننا التحقق من هويتك عبر الإنترنت",
|
|
367
|
+
identityUnavailableDescription: "لم نتمكن من مطابقة بياناتك مع سجل مريض. يرجى الاتصال بالعيادة للتحقق من هويتك وبيانات الاتصال الخاصة بك.",
|
|
368
|
+
lastName: "اسم العائلة",
|
|
369
|
+
loadingSlots: "جارٍ تحميل الأوقات المتاحة…",
|
|
370
|
+
loginTitle: "تسجيل الدخول إلى حسابك",
|
|
371
|
+
marketingOptIn: "نعم، أود معرفة الأخبار والعروض الخاصة",
|
|
372
|
+
newPatient: "لا",
|
|
373
|
+
newPatientDescription: "أنا مريض جديد",
|
|
374
|
+
noDoctors: "لا يوجد أطباء متاحون للحجز عبر الإنترنت.",
|
|
375
|
+
noServices: "لا توجد خدمات متاحة للحجز عبر الإنترنت.",
|
|
376
|
+
noSlots: "لا توجد أوقات متاحة في هذا التاريخ.",
|
|
377
|
+
noTreatments: "لا توجد علاجات متاحة لهذه الخدمة.",
|
|
378
|
+
otpCode: "رمز مكون من 6 أرقام",
|
|
379
|
+
otpDescription: "أدخل الرمز المكون من 6 أرقام المرسل إلى هاتفك المحمول.",
|
|
380
|
+
otpSent: "أرسلنا رمز التحقق إلى",
|
|
381
|
+
patientDetails: "بيانات المريض",
|
|
382
|
+
patientDetailsRequired: "أضف بيانات المريض لإنشاء سجل المريض.",
|
|
383
|
+
patientNoMatch: "لم نعثر على سجل مطابق",
|
|
384
|
+
patientNoMatchDescription: "بحثنا عن سجلات تطابق هذه البيانات.",
|
|
385
|
+
paymentAtPractice: "ادفع سعر العلاج بالكامل بأمان عبر Stripe لتأكيد موعدك.",
|
|
386
|
+
paymentExpired: "انتهت جلسة الدفع. اختر وقتًا للموعد وحاول مرة أخرى.",
|
|
387
|
+
paymentPending: "أكمل الدفع في علامة تبويب Stripe. سيتم تأكيد موعدك تلقائيًا بعد الدفع.",
|
|
388
|
+
paymentReceived: "تم استلام الدفع",
|
|
389
|
+
paymentStatusUnavailable: "تعذر التحقق من حالة الدفع. يرجى المحاولة مرة أخرى. قد يكون الدفع قيد المعالجة.",
|
|
390
|
+
payWithStripe: "الدفع بأمان عبر Stripe",
|
|
391
|
+
pendingConfirmation: "طلب موعدك بانتظار تأكيد العيادة.",
|
|
392
|
+
phone: "رقم الجوال",
|
|
393
|
+
phoneCountry: "الدولة ورمز الاتصال",
|
|
394
|
+
phoneCountryCodeHint: "أدخل رمز الدولة، مثل +91 8799657887 أو +44 7700900123.",
|
|
395
|
+
postcode: "الرمز البريدي",
|
|
396
|
+
practitionerDescription: "اختر طبيبًا أو دعنا نجد أقرب موعد متاح",
|
|
397
|
+
preparingPayment: "جارٍ تجهيز الدفع الآمن…",
|
|
398
|
+
price: "السعر",
|
|
399
|
+
registeredQuestion: "هل أنت مسجل كمريض في هذه العيادة؟",
|
|
400
|
+
requestCode: "إرسال رمز التحقق",
|
|
401
|
+
resendCode: "إعادة إرسال الرمز",
|
|
402
|
+
resendCodeIn: "إعادة إرسال الرمز خلال {seconds} ثانية",
|
|
403
|
+
retry: "حاول مرة أخرى",
|
|
404
|
+
reviewPayment: "أكمل الدفع",
|
|
405
|
+
selectDateFirst: "اختر تاريخًا لعرض الأوقات المتاحة.",
|
|
406
|
+
serviceDescription: "اختر خدمة للمتابعة",
|
|
407
|
+
slotsLoadFailed: "تعذر تحميل الأوقات المتاحة. يرجى المحاولة مرة أخرى أو اختيار تاريخ آخر.",
|
|
408
|
+
somethingElse: "شيء آخر",
|
|
409
|
+
submit: "حجز الموعد",
|
|
410
|
+
submitting: "جارٍ حجز الموعد...",
|
|
411
|
+
title: "احجز عبر الإنترنت",
|
|
412
|
+
titleLabel: "اللقب",
|
|
413
|
+
titlePlaceholder: "اختر اللقب",
|
|
414
|
+
total: "الإجمالي",
|
|
415
|
+
treatmentDescription: "اختر موعدًا",
|
|
416
|
+
verificationReminders: "نستخدم رقم هاتفك للتحقق من الحساب وتذكيرات المواعيد.",
|
|
417
|
+
verifyPhone: "تحقق من رقم جوالك",
|
|
418
|
+
yourDetails: "بيانات التواصل"
|
|
419
|
+
};
|
|
420
|
+
function resolveLocale(locale) {
|
|
421
|
+
const candidate = locale?.trim() || "en";
|
|
422
|
+
try {
|
|
423
|
+
return Intl.getCanonicalLocales(candidate)[0] ?? "en";
|
|
424
|
+
} catch {
|
|
425
|
+
return "en";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function getLocaleDirection(locale) {
|
|
429
|
+
return /^((ar|fa|he|ur)(-|$))/i.test(resolveLocale(locale)) ? "rtl" : "ltr";
|
|
430
|
+
}
|
|
431
|
+
function getBookingCopy(locale) {
|
|
432
|
+
return resolveLocale(locale).toLowerCase().startsWith("ar") ? arabicCopy : englishCopy;
|
|
433
|
+
}
|
|
434
|
+
function getDateTimeParts(value, timeZone) {
|
|
435
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
436
|
+
if (Number.isNaN(date.getTime())) {
|
|
437
|
+
throw new Error("Invalid booking date");
|
|
438
|
+
}
|
|
439
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
440
|
+
day: "2-digit",
|
|
441
|
+
month: "2-digit",
|
|
442
|
+
timeZone,
|
|
443
|
+
year: "numeric"
|
|
444
|
+
}).formatToParts(date);
|
|
445
|
+
}
|
|
446
|
+
function formatBookingDateTime(value, options) {
|
|
447
|
+
return new Intl.DateTimeFormat(resolveLocale(options.locale), {
|
|
448
|
+
dateStyle: "medium",
|
|
449
|
+
timeStyle: "short",
|
|
450
|
+
timeZone: options.timeZone
|
|
451
|
+
}).format(typeof value === "string" ? new Date(value) : value);
|
|
452
|
+
}
|
|
453
|
+
function formatBookingDate(value, options) {
|
|
454
|
+
return new Intl.DateTimeFormat(resolveLocale(options.locale), {
|
|
455
|
+
dateStyle: "full",
|
|
456
|
+
timeZone: options.timeZone
|
|
457
|
+
}).format(typeof value === "string" ? new Date(value) : value);
|
|
458
|
+
}
|
|
459
|
+
function formatBookingTime(value, options) {
|
|
460
|
+
return new Intl.DateTimeFormat(resolveLocale(options.locale), {
|
|
461
|
+
timeStyle: "short",
|
|
462
|
+
timeZone: options.timeZone
|
|
463
|
+
}).format(typeof value === "string" ? new Date(value) : value);
|
|
464
|
+
}
|
|
465
|
+
function getDateKeyInTimeZone(value, timeZone) {
|
|
466
|
+
const parts = getDateTimeParts(value, timeZone);
|
|
467
|
+
const values = Object.fromEntries(
|
|
468
|
+
parts.map((part) => [part.type, part.value])
|
|
469
|
+
);
|
|
470
|
+
return `${values.year}-${values.month}-${values.day}`;
|
|
471
|
+
}
|
|
472
|
+
function addCalendarDays(dateKey, days) {
|
|
473
|
+
const date = /* @__PURE__ */ new Date(`${dateKey}T12:00:00.000Z`);
|
|
474
|
+
if (Number.isNaN(date.getTime())) {
|
|
475
|
+
throw new Error("Invalid booking date key");
|
|
476
|
+
}
|
|
477
|
+
date.setUTCDate(date.getUTCDate() + days);
|
|
478
|
+
return date.toISOString().slice(0, 10);
|
|
479
|
+
}
|
|
480
|
+
export {
|
|
481
|
+
BookingApiError,
|
|
482
|
+
addCalendarDays,
|
|
483
|
+
createBookingClient,
|
|
484
|
+
formatBookingDate,
|
|
485
|
+
formatBookingDateTime,
|
|
486
|
+
formatBookingTime,
|
|
487
|
+
getBookingCopy,
|
|
488
|
+
getDateKeyInTimeZone,
|
|
489
|
+
getLocaleDirection,
|
|
490
|
+
isLikelyE164,
|
|
491
|
+
isValidPastDateOfBirth,
|
|
492
|
+
normalizeEmail,
|
|
493
|
+
normalizePhone,
|
|
494
|
+
resolveLocale,
|
|
495
|
+
validateContact,
|
|
496
|
+
validateExistingPhone,
|
|
497
|
+
validatePatientDetails,
|
|
498
|
+
validateVerificationCode
|
|
499
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
|
+
"dependencies": {},
|
|
3
|
+
"description": "Veladent booking API client and types",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"types": "./types/booking/core/src/index.d.ts",
|
|
7
|
+
"import": "./index.js"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"styles.css",
|
|
13
|
+
"types",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"license": "UNLICENSED",
|
|
17
|
+
"main": "./index.js",
|
|
2
18
|
"name": "@veladent/booking-core",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
19
|
+
"repository": {
|
|
20
|
+
"directory": "packages/booking/core",
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/veladent-tech/veladent.git"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"**/*.css"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"types": "./types/booking/core/src/index.d.ts",
|
|
29
|
+
"version": "1.0.4-dev.9",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"registry": "https://registry.npmjs.org/"
|
|
33
|
+
},
|
|
34
|
+
"veladentRelease": {
|
|
35
|
+
"environment": "dev",
|
|
36
|
+
"source": "da9f660f9c547eea900329e92dc3cc282f15e6d3a333643282198e2bf105ef9d"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BookingClientOptions, BookingConfig, BookingResult, BookingSlot, CreateBookingInput, GetSlotsInput, RequestBookingVerificationInput, RequestBookingVerificationResult, VerifyBookingCodeInput, VerifyBookingCodeResult } from "./types.js";
|
|
2
|
+
export declare class BookingApiError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly code?: string;
|
|
5
|
+
constructor(message: string, status: number, code?: string);
|
|
6
|
+
}
|
|
7
|
+
export interface BookingClient {
|
|
8
|
+
cancelPayment?(sessionId: string): Promise<Omit<BookingResult, "status"> & {
|
|
9
|
+
status: "pending" | "confirmed" | "expired";
|
|
10
|
+
}>;
|
|
11
|
+
createRequest(input: CreateBookingInput): Promise<BookingResult>;
|
|
12
|
+
getConfig(signal?: AbortSignal): Promise<BookingConfig>;
|
|
13
|
+
getPaymentStatus?(sessionId: string): Promise<Omit<BookingResult, "status"> & {
|
|
14
|
+
status: "pending" | "confirmed" | "expired";
|
|
15
|
+
}>;
|
|
16
|
+
getSlots(input: GetSlotsInput): Promise<BookingSlot[]>;
|
|
17
|
+
requestVerification(input: RequestBookingVerificationInput): Promise<RequestBookingVerificationResult>;
|
|
18
|
+
verificationStorageKey?: string;
|
|
19
|
+
verifyCode(input: VerifyBookingCodeInput): Promise<VerifyBookingCodeResult>;
|
|
20
|
+
}
|
|
21
|
+
export declare function createBookingClient(options: BookingClientOptions): BookingClient;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { BookingLocale } from "./types.js";
|
|
2
|
+
export type BookingDirection = "ltr" | "rtl";
|
|
3
|
+
export interface BookingCopy {
|
|
4
|
+
addressLine1: string;
|
|
5
|
+
anyPractitioner: string;
|
|
6
|
+
anyPractitionerDescription: string;
|
|
7
|
+
appointmentSummary: string;
|
|
8
|
+
appointmentTitle: string;
|
|
9
|
+
back: string;
|
|
10
|
+
balance: string;
|
|
11
|
+
bookAgain: string;
|
|
12
|
+
bookingDisabled: string;
|
|
13
|
+
bookingFailed: string;
|
|
14
|
+
bookingUnavailableDescription: string;
|
|
15
|
+
bookingUnavailableTitle: string;
|
|
16
|
+
callPractice: string;
|
|
17
|
+
cancelPayment: string;
|
|
18
|
+
checkingPayment: string;
|
|
19
|
+
checkPayment: string;
|
|
20
|
+
chooseAnotherDate: string;
|
|
21
|
+
chooseDate: string;
|
|
22
|
+
chooseDateTime: string;
|
|
23
|
+
chooseDoctor: string;
|
|
24
|
+
chooseService: string;
|
|
25
|
+
chooseTime: string;
|
|
26
|
+
chooseTreatment: string;
|
|
27
|
+
clinic: string;
|
|
28
|
+
close: string;
|
|
29
|
+
codeExpired: string;
|
|
30
|
+
codeExpiresIn: string;
|
|
31
|
+
confirmationDescription: string;
|
|
32
|
+
confirmCode: string;
|
|
33
|
+
confirmed: string;
|
|
34
|
+
contactPractice: string;
|
|
35
|
+
contactSite: string;
|
|
36
|
+
continue: string;
|
|
37
|
+
dateOfBirth: string;
|
|
38
|
+
deposit: string;
|
|
39
|
+
email: string;
|
|
40
|
+
existingPatient: string;
|
|
41
|
+
existingPatientDescription: string;
|
|
42
|
+
findPatientRecord: string;
|
|
43
|
+
firstName: string;
|
|
44
|
+
fullName: string;
|
|
45
|
+
gender: string;
|
|
46
|
+
genderOptions: {
|
|
47
|
+
female: string;
|
|
48
|
+
male: string;
|
|
49
|
+
other: string;
|
|
50
|
+
preferNotToSay: string;
|
|
51
|
+
};
|
|
52
|
+
genderPlaceholder: string;
|
|
53
|
+
goBackAndEdit: string;
|
|
54
|
+
identityUnavailable: string;
|
|
55
|
+
identityUnavailableDescription: string;
|
|
56
|
+
invalidInput: string;
|
|
57
|
+
lastName: string;
|
|
58
|
+
loadingSlots: string;
|
|
59
|
+
loginTitle: string;
|
|
60
|
+
marketingOptIn: string;
|
|
61
|
+
newPatient: string;
|
|
62
|
+
newPatientDescription: string;
|
|
63
|
+
noDoctors: string;
|
|
64
|
+
noMatchingServices: string;
|
|
65
|
+
noServices: string;
|
|
66
|
+
noSlots: string;
|
|
67
|
+
noTreatments: string;
|
|
68
|
+
otpCode: string;
|
|
69
|
+
otpDescription: string;
|
|
70
|
+
otpSent: string;
|
|
71
|
+
patientDetails: string;
|
|
72
|
+
patientDetailsRequired: string;
|
|
73
|
+
patientNoMatch: string;
|
|
74
|
+
patientNoMatchDescription: string;
|
|
75
|
+
paymentAtPractice: string;
|
|
76
|
+
paymentExpired: string;
|
|
77
|
+
paymentPending: string;
|
|
78
|
+
paymentReceived: string;
|
|
79
|
+
paymentStatusUnavailable: string;
|
|
80
|
+
payWithStripe: string;
|
|
81
|
+
pendingConfirmation: string;
|
|
82
|
+
phone: string;
|
|
83
|
+
phoneCountry: string;
|
|
84
|
+
phoneCountryCodeHint: string;
|
|
85
|
+
postcode: string;
|
|
86
|
+
practitionerDescription: string;
|
|
87
|
+
preparingPayment: string;
|
|
88
|
+
price: string;
|
|
89
|
+
registeredQuestion: string;
|
|
90
|
+
requestCode: string;
|
|
91
|
+
resendCode: string;
|
|
92
|
+
resendCodeIn: string;
|
|
93
|
+
retry: string;
|
|
94
|
+
reviewPayment: string;
|
|
95
|
+
searchServices: string;
|
|
96
|
+
selectDateFirst: string;
|
|
97
|
+
serviceDescription: string;
|
|
98
|
+
slotsLoadFailed: string;
|
|
99
|
+
somethingElse: string;
|
|
100
|
+
submit: string;
|
|
101
|
+
submitting: string;
|
|
102
|
+
title: string;
|
|
103
|
+
titleLabel: string;
|
|
104
|
+
titlePlaceholder: string;
|
|
105
|
+
total: string;
|
|
106
|
+
treatmentDescription: string;
|
|
107
|
+
verificationReminders: string;
|
|
108
|
+
verifyPhone: string;
|
|
109
|
+
yourDetails: string;
|
|
110
|
+
}
|
|
111
|
+
export declare function resolveLocale(locale?: BookingLocale): string;
|
|
112
|
+
export declare function getLocaleDirection(locale?: BookingLocale): BookingDirection;
|
|
113
|
+
export declare function getBookingCopy(locale?: BookingLocale): BookingCopy;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BookingLocale } from "./types.js";
|
|
2
|
+
export interface FormatDateTimeOptions {
|
|
3
|
+
locale?: BookingLocale;
|
|
4
|
+
timeZone: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function formatBookingDateTime(value: Date | string, options: FormatDateTimeOptions): string;
|
|
7
|
+
export declare function formatBookingDate(value: Date | string, options: FormatDateTimeOptions): string;
|
|
8
|
+
export declare function formatBookingTime(value: Date | string, options: FormatDateTimeOptions): string;
|
|
9
|
+
export declare function getDateKeyInTimeZone(value: Date | string, timeZone: string): string;
|
|
10
|
+
export declare function addCalendarDays(dateKey: string, days: number): string;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export type BookingLocale = string;
|
|
2
|
+
export interface BookingTheme {
|
|
3
|
+
accentColor?: string;
|
|
4
|
+
colorMode?: "dark" | "light";
|
|
5
|
+
radius?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BookingDoctor {
|
|
8
|
+
avatarUrl?: string | null;
|
|
9
|
+
displayName: string;
|
|
10
|
+
id: string;
|
|
11
|
+
specialty?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface BookingService {
|
|
14
|
+
description?: string | null;
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
export interface BookingTreatment {
|
|
19
|
+
currencyCode: string;
|
|
20
|
+
currencySymbol?: string | null;
|
|
21
|
+
deposit?: number | null;
|
|
22
|
+
description?: string | null;
|
|
23
|
+
durationMinutes: number;
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
price: number;
|
|
27
|
+
serviceId: string;
|
|
28
|
+
}
|
|
29
|
+
export interface BookingConfig {
|
|
30
|
+
doctors: BookingDoctor[];
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
locale: BookingLocale;
|
|
33
|
+
services: BookingService[];
|
|
34
|
+
siteAddress?: string | null;
|
|
35
|
+
siteLogoUrl?: string | null;
|
|
36
|
+
siteName?: string | null;
|
|
37
|
+
sitePhone?: string | null;
|
|
38
|
+
theme?: BookingTheme | null;
|
|
39
|
+
timeZone: string;
|
|
40
|
+
treatments: BookingTreatment[];
|
|
41
|
+
}
|
|
42
|
+
export interface BookingSlot {
|
|
43
|
+
available: boolean;
|
|
44
|
+
doctorId: string;
|
|
45
|
+
doctorName?: string | null;
|
|
46
|
+
end: string;
|
|
47
|
+
id: string;
|
|
48
|
+
start: string;
|
|
49
|
+
}
|
|
50
|
+
export interface BookingContact {
|
|
51
|
+
address?: string;
|
|
52
|
+
email: string;
|
|
53
|
+
firstName?: string;
|
|
54
|
+
fullName: string;
|
|
55
|
+
lastName?: string;
|
|
56
|
+
marketingOptIn?: boolean;
|
|
57
|
+
phone: string;
|
|
58
|
+
postalCode?: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface PatientDetails {
|
|
62
|
+
dateOfBirth: string;
|
|
63
|
+
gender: string;
|
|
64
|
+
}
|
|
65
|
+
export interface CreateBookingInput {
|
|
66
|
+
bookingMode: "existing" | "new";
|
|
67
|
+
checkoutRequestId?: string;
|
|
68
|
+
contact: BookingContact;
|
|
69
|
+
doctorId: string;
|
|
70
|
+
end: string;
|
|
71
|
+
patientDetails: PatientDetails;
|
|
72
|
+
start: string;
|
|
73
|
+
treatmentId: string;
|
|
74
|
+
verificationCode: string;
|
|
75
|
+
}
|
|
76
|
+
export interface RequestBookingVerificationInput {
|
|
77
|
+
bookingMode?: "existing" | "new";
|
|
78
|
+
identity?: {
|
|
79
|
+
firstName: string;
|
|
80
|
+
lastName: string;
|
|
81
|
+
dateOfBirth: string;
|
|
82
|
+
};
|
|
83
|
+
phone: string;
|
|
84
|
+
}
|
|
85
|
+
export interface RequestBookingVerificationResult {
|
|
86
|
+
cooldownSeconds: number;
|
|
87
|
+
expiresInSeconds: number;
|
|
88
|
+
is_patient_found?: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface VerifyBookingCodeInput {
|
|
91
|
+
bookingMode?: "existing" | "new";
|
|
92
|
+
code: string;
|
|
93
|
+
identity?: {
|
|
94
|
+
firstName: string;
|
|
95
|
+
lastName: string;
|
|
96
|
+
dateOfBirth: string;
|
|
97
|
+
};
|
|
98
|
+
phone: string;
|
|
99
|
+
}
|
|
100
|
+
export interface VerifyBookingCodeResult {
|
|
101
|
+
patientMatched?: boolean;
|
|
102
|
+
sessionExpiresInSeconds?: number;
|
|
103
|
+
sessionToken?: string;
|
|
104
|
+
verified: boolean;
|
|
105
|
+
}
|
|
106
|
+
export interface BookingResult {
|
|
107
|
+
checkout?: {
|
|
108
|
+
sessionId: string;
|
|
109
|
+
url: string;
|
|
110
|
+
amount: number;
|
|
111
|
+
price: number;
|
|
112
|
+
currency: string;
|
|
113
|
+
};
|
|
114
|
+
payment?: {
|
|
115
|
+
price: number;
|
|
116
|
+
currency: string;
|
|
117
|
+
};
|
|
118
|
+
scheduledEnd: string;
|
|
119
|
+
scheduledStart: string;
|
|
120
|
+
status: "pending" | "confirmed";
|
|
121
|
+
treatmentName?: string;
|
|
122
|
+
}
|
|
123
|
+
export interface GetSlotsInput {
|
|
124
|
+
date: string;
|
|
125
|
+
doctorId?: string;
|
|
126
|
+
fromDate?: string;
|
|
127
|
+
signal?: AbortSignal;
|
|
128
|
+
toDate?: string;
|
|
129
|
+
treatmentId: string;
|
|
130
|
+
}
|
|
131
|
+
export interface BookingClientOptions {
|
|
132
|
+
baseUrl: string;
|
|
133
|
+
fetcher?: typeof fetch;
|
|
134
|
+
publishableKey: string;
|
|
135
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BookingContact, PatientDetails } from "./types.js";
|
|
2
|
+
export declare function normalizeEmail(value: string): string;
|
|
3
|
+
export declare function normalizePhone(value: string): string;
|
|
4
|
+
export declare function isLikelyE164(value: string): boolean;
|
|
5
|
+
export declare function validateContact(contact: BookingContact): string | null;
|
|
6
|
+
export declare function validateExistingPhone(phone: string): string | null;
|
|
7
|
+
export declare function validateVerificationCode(code: string): string | null;
|
|
8
|
+
export declare function isValidPastDateOfBirth(value: string): boolean;
|
|
9
|
+
export declare function validatePatientDetails(details?: PatientDetails): string | null;
|