@ordinatio/scheduling-api 0.1.0 → 0.2.0
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/CHANGELOG.md +29 -0
- package/README.md +87 -1
- package/dist/{chunk-IAQBLF5F.mjs → chunk-MQO3VRMU.mjs} +59 -1
- package/dist/index.d.mts +63 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.js +991 -229
- package/dist/index.mjs +776 -59
- package/dist/schemas/index.d.mts +55 -1
- package/dist/schemas/index.d.ts +55 -1
- package/dist/schemas/index.js +65 -0
- package/dist/schemas/index.mjs +15 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BookingPageConfigSchema,
|
|
3
3
|
BookingPageThemeSchema,
|
|
4
|
+
BrandSchema,
|
|
4
5
|
CancelRecurringScopeSchema,
|
|
5
6
|
CancelRecurringSeriesSchema,
|
|
6
7
|
CreateAppointmentCategorySchema,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
CreateNotificationTemplateSchema,
|
|
12
13
|
CreatePackagePurchaseSchema,
|
|
13
14
|
CreateRecurringSeriesSchema,
|
|
15
|
+
CreateRoutingFormSchema,
|
|
14
16
|
CreateServicePackageSchema,
|
|
15
17
|
CreateWebhookEndpointSchema,
|
|
16
18
|
DEFAULT_CANCELLATION_POLICY,
|
|
@@ -19,9 +21,13 @@ import {
|
|
|
19
21
|
NotificationChannelSchema,
|
|
20
22
|
NotificationTriggerSchema,
|
|
21
23
|
OrgCancellationPolicySchema,
|
|
24
|
+
PrivacyEraseSchema,
|
|
25
|
+
PrivacySubjectSchema,
|
|
22
26
|
RecurringFrequencySchema,
|
|
27
|
+
RouteClientSchema,
|
|
23
28
|
SLOT_GRANULARITY_DEFAULT,
|
|
24
29
|
SLOT_GRANULARITY_OPTIONS,
|
|
30
|
+
TestRoutingFormSchema,
|
|
25
31
|
UpdateAppointmentCategorySchema,
|
|
26
32
|
UpdateAppointmentTypeSchema,
|
|
27
33
|
UpdateBookingPageSchema,
|
|
@@ -29,8 +35,9 @@ import {
|
|
|
29
35
|
UpdateCouponCodeSchema,
|
|
30
36
|
UpdateNotificationTemplateSchema,
|
|
31
37
|
UpdatePackagePurchaseSchema,
|
|
38
|
+
UpdateRoutingFormSchema,
|
|
32
39
|
UpdateServicePackageSchema
|
|
33
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-MQO3VRMU.mjs";
|
|
34
41
|
|
|
35
42
|
// src/cors.ts
|
|
36
43
|
function defaultCorsHeaders() {
|
|
@@ -253,6 +260,85 @@ function withWebhooks(inner, host) {
|
|
|
253
260
|
};
|
|
254
261
|
}
|
|
255
262
|
|
|
263
|
+
// src/waitlist/announce-offer.ts
|
|
264
|
+
import { enqueueWebhookEvent, tryOfferFreedSlot } from "@ordinatio/scheduling";
|
|
265
|
+
async function offerAndAnnounce(host, settings, db, slot) {
|
|
266
|
+
const offer = await tryOfferFreedSlot(db, { ...slot, offerMinutes: settings.offerMinutes });
|
|
267
|
+
if (!offer) return false;
|
|
268
|
+
const details = {
|
|
269
|
+
organizationId: slot.organizationId,
|
|
270
|
+
token: offer.token,
|
|
271
|
+
clientName: offer.entry.clientName,
|
|
272
|
+
clientEmail: offer.entry.clientEmail,
|
|
273
|
+
clientPhone: offer.entry.clientPhone,
|
|
274
|
+
typeName: offer.typeName,
|
|
275
|
+
calendarName: offer.calendarName,
|
|
276
|
+
startTime: offer.startTime,
|
|
277
|
+
timezone: offer.timezone,
|
|
278
|
+
expiresAt: offer.expiresAt
|
|
279
|
+
};
|
|
280
|
+
if (settings.notify) {
|
|
281
|
+
try {
|
|
282
|
+
await settings.notify(details);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error("[waitlist] the host could not send the offer; it stays open until it expires:", error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (host.webhooks) {
|
|
288
|
+
try {
|
|
289
|
+
await enqueueWebhookEvent(db, {
|
|
290
|
+
organizationId: slot.organizationId,
|
|
291
|
+
type: "waitlist.offered",
|
|
292
|
+
// Each offering has its own id, so offering the same slot to the same person again still reaches subscribers.
|
|
293
|
+
eventId: `waitlist_offer_${offer.entry.id}_${offer.startTime.getTime()}_${offer.expiresAt.getTime()}`,
|
|
294
|
+
data: {
|
|
295
|
+
offer: {
|
|
296
|
+
entryId: offer.entry.id,
|
|
297
|
+
token: offer.token,
|
|
298
|
+
clientName: details.clientName,
|
|
299
|
+
clientEmail: details.clientEmail,
|
|
300
|
+
clientPhone: details.clientPhone,
|
|
301
|
+
typeName: details.typeName,
|
|
302
|
+
calendarName: details.calendarName,
|
|
303
|
+
calendarId: offer.calendarId,
|
|
304
|
+
startTime: offer.startTime.toISOString(),
|
|
305
|
+
expiresAt: offer.expiresAt.toISOString()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error("[waitlist] could not queue the waitlist.offered webhook:", error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/waitlist/waitlist-events.ts
|
|
317
|
+
var FREED_BY = /* @__PURE__ */ new Set(["appointment.cancelled", "appointment.rejected", "appointment.updated"]);
|
|
318
|
+
function withWaitlist(inner, host) {
|
|
319
|
+
return {
|
|
320
|
+
async emit(organizationId, type, data) {
|
|
321
|
+
const appointmentId = data.appointmentId;
|
|
322
|
+
if (host.waitlist && FREED_BY.has(type) && typeof appointmentId === "string") {
|
|
323
|
+
try {
|
|
324
|
+
const db = await host.tenantDb(organizationId, "waitlist");
|
|
325
|
+
const appointment = db ? await db.appointment.findUnique({
|
|
326
|
+
where: { id: appointmentId },
|
|
327
|
+
select: { calendarId: true, startTime: true, rescheduledFrom: true, status: true, calendar: { select: { organizationId: true } } }
|
|
328
|
+
}) : null;
|
|
329
|
+
if (db && appointment && (appointment.calendar.organizationId ?? organizationId) === organizationId) {
|
|
330
|
+
const freed = type === "appointment.updated" ? appointment.rescheduledFrom : appointment.status === "CANCELLED" ? appointment.startTime : null;
|
|
331
|
+
if (freed) await offerAndAnnounce(host, host.waitlist, db, { organizationId, calendarId: appointment.calendarId, startTime: freed });
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error("[scheduling-api] could not offer a freed slot to the waitlist:", error);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
await inner.emit(organizationId, type, data);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
256
342
|
// src/handlers/manage-cancel.ts
|
|
257
343
|
import { schedError as schedError3 } from "@ordinatio/scheduling";
|
|
258
344
|
|
|
@@ -381,7 +467,7 @@ async function cancelViaToken(host, db, organizationId, appointment) {
|
|
|
381
467
|
const baseUrl = host.appUrl();
|
|
382
468
|
if (baseUrl) {
|
|
383
469
|
try {
|
|
384
|
-
await queueStaffNotification(db, appointment.id, "CANCELLATION", baseUrl);
|
|
470
|
+
await queueStaffNotification(db, appointment.id, "CANCELLATION", baseUrl, { brand: Boolean(host.brand) });
|
|
385
471
|
} catch (staffError) {
|
|
386
472
|
const diagnostic = schedError2("SCHED_607", { error: staffError, appointmentId: appointment.id });
|
|
387
473
|
console.error("[DELETE /manage]", diagnostic.ref, staffError instanceof Error ? staffError.message : staffError);
|
|
@@ -546,19 +632,19 @@ async function rescheduleViaToken(host, db, organizationId, appointment, newTime
|
|
|
546
632
|
);
|
|
547
633
|
const baseUrl = host.appUrl() ?? void 0;
|
|
548
634
|
if (baseUrl) {
|
|
549
|
-
await queueImmediateNotification(db, appointment.id, "RESCHEDULE", baseUrl);
|
|
635
|
+
await queueImmediateNotification(db, appointment.id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
|
|
550
636
|
} else {
|
|
551
637
|
console.warn("[manage] no appUrl \u2014 RESCHEDULE notification skipped");
|
|
552
638
|
}
|
|
553
639
|
try {
|
|
554
|
-
await scheduleNotifications(db, appointment.id, baseUrl, { excludeTriggers: ["CONFIRMATION"] });
|
|
640
|
+
await scheduleNotifications(db, appointment.id, baseUrl, { excludeTriggers: ["CONFIRMATION"], brand: Boolean(host.brand) });
|
|
555
641
|
} catch (error) {
|
|
556
642
|
const diagnostic = schedError5("SCHED_604", { error, appointmentId: appointment.id });
|
|
557
643
|
console.error("[PATCH /manage] scheduleNotifications failed (non-blocking):", diagnostic.ref, error instanceof Error ? error.message : error);
|
|
558
644
|
}
|
|
559
645
|
if (baseUrl) {
|
|
560
646
|
try {
|
|
561
|
-
await queueStaffNotification2(db, appointment.id, "RESCHEDULE", baseUrl);
|
|
647
|
+
await queueStaffNotification2(db, appointment.id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
|
|
562
648
|
} catch (staffError) {
|
|
563
649
|
const diagnostic = schedError5("SCHED_607", { error: staffError, appointmentId: appointment.id });
|
|
564
650
|
console.error("[PATCH /manage]", diagnostic.ref, staffError instanceof Error ? staffError.message : staffError);
|
|
@@ -945,7 +1031,7 @@ async function cancelByStaff(request, { host, params }) {
|
|
|
945
1031
|
const baseUrl = host.appUrl();
|
|
946
1032
|
if (baseUrl) {
|
|
947
1033
|
try {
|
|
948
|
-
await queueStaffNotification3(db, id, "CANCELLATION", baseUrl);
|
|
1034
|
+
await queueStaffNotification3(db, id, "CANCELLATION", baseUrl, { brand: Boolean(host.brand) });
|
|
949
1035
|
} catch (error) {
|
|
950
1036
|
await host.activity.log(db, {
|
|
951
1037
|
action: "scheduling.staff_notification_failed",
|
|
@@ -1260,7 +1346,7 @@ async function cancelFromCalendarSync(request, { host, params }) {
|
|
|
1260
1346
|
await host.workerCommands.queue(source === "google" ? "delete-from-outlook" : "delete-from-google", { appointmentId: id });
|
|
1261
1347
|
const baseUrl = host.appUrl();
|
|
1262
1348
|
if (baseUrl) {
|
|
1263
|
-
await queueImmediateNotification2(db, id, "CANCELLATION", baseUrl).catch(
|
|
1349
|
+
await queueImmediateNotification2(db, id, "CANCELLATION", baseUrl, { brand: Boolean(host.brand) }).catch(
|
|
1264
1350
|
(err) => console.warn("[cancel-sync] Failed to queue cancellation notification:", id, err instanceof Error ? err.message : err)
|
|
1265
1351
|
);
|
|
1266
1352
|
}
|
|
@@ -1366,7 +1452,7 @@ async function approveOrReject(request, { host, params }) {
|
|
|
1366
1452
|
await cancelPendingNotifications(db, id);
|
|
1367
1453
|
const baseUrl = host.appUrl();
|
|
1368
1454
|
if (!baseUrl) throw new Error("NEXT_PUBLIC_APP_URL is required");
|
|
1369
|
-
await scheduleNotifications2(db, id, baseUrl);
|
|
1455
|
+
await scheduleNotifications2(db, id, baseUrl, { brand: Boolean(host.brand) });
|
|
1370
1456
|
await host.availabilityCache.invalidate(appointment.calendarId);
|
|
1371
1457
|
await host.events.emit(organizationId, "appointment.confirmed", { appointmentId: id });
|
|
1372
1458
|
await host.activity.log(db, {
|
|
@@ -1429,14 +1515,14 @@ async function rescheduleByStaff(request, { host, params }) {
|
|
|
1429
1515
|
});
|
|
1430
1516
|
const baseUrl = host.appUrl();
|
|
1431
1517
|
if (!baseUrl) throw new Error("NEXT_PUBLIC_APP_URL is required");
|
|
1432
|
-
await queueImmediateNotification3(db, id, "RESCHEDULE", baseUrl);
|
|
1518
|
+
await queueImmediateNotification3(db, id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
|
|
1433
1519
|
try {
|
|
1434
|
-
await scheduleNotifications3(db, id, baseUrl, { excludeTriggers: ["CONFIRMATION"] });
|
|
1520
|
+
await scheduleNotifications3(db, id, baseUrl, { excludeTriggers: ["CONFIRMATION"], brand: Boolean(host.brand) });
|
|
1435
1521
|
} catch (error) {
|
|
1436
1522
|
console.warn("[reschedule] scheduleNotifications failed:", error instanceof Error ? error.message : error);
|
|
1437
1523
|
}
|
|
1438
1524
|
try {
|
|
1439
|
-
await queueStaffNotification4(db, id, "RESCHEDULE", baseUrl);
|
|
1525
|
+
await queueStaffNotification4(db, id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
|
|
1440
1526
|
} catch (error) {
|
|
1441
1527
|
await host.activity.log(db, {
|
|
1442
1528
|
action: "scheduling.staff_notification_failed",
|
|
@@ -3419,7 +3505,7 @@ async function runSeriesSideEffects(host, db, organizationId, seriesId, client)
|
|
|
3419
3505
|
const sendConfirmation = async (url, appointmentId) => {
|
|
3420
3506
|
for (let tryNumber = 1; tryNumber <= 2; tryNumber += 1) {
|
|
3421
3507
|
try {
|
|
3422
|
-
await scheduleNotifications4(db, appointmentId, url);
|
|
3508
|
+
await scheduleNotifications4(db, appointmentId, url, { brand: Boolean(host.brand) });
|
|
3423
3509
|
return;
|
|
3424
3510
|
} catch (error) {
|
|
3425
3511
|
if (tryNumber === 2) record("Client confirmation", error);
|
|
@@ -3431,10 +3517,9 @@ async function runSeriesSideEffects(host, db, organizationId, seriesId, client)
|
|
|
3431
3517
|
if (visit === confirmationVisit) {
|
|
3432
3518
|
await sendConfirmation(baseUrl, visit.id);
|
|
3433
3519
|
} else {
|
|
3434
|
-
await attempt(
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
);
|
|
3520
|
+
await attempt("Client notices", async () => {
|
|
3521
|
+
await scheduleNotifications4(db, visit.id, baseUrl, { excludeTriggers: ["CONFIRMATION"], brand: Boolean(host.brand) });
|
|
3522
|
+
});
|
|
3438
3523
|
}
|
|
3439
3524
|
}
|
|
3440
3525
|
await attempt(
|
|
@@ -3453,7 +3538,7 @@ async function runSeriesSideEffects(host, db, organizationId, seriesId, client)
|
|
|
3453
3538
|
await Promise.all(visits.slice(from, from + VISITS_PER_GROUP).map(handleVisit));
|
|
3454
3539
|
}
|
|
3455
3540
|
if (baseUrl && confirmationVisit) {
|
|
3456
|
-
await attempt("Staff alert", () => queueStaffNotification5(db, confirmationVisit.id, "CONFIRMATION", baseUrl));
|
|
3541
|
+
await attempt("Staff alert", () => queueStaffNotification5(db, confirmationVisit.id, "CONFIRMATION", baseUrl, { brand: Boolean(host.brand) }));
|
|
3457
3542
|
}
|
|
3458
3543
|
await attempt("Availability cache", () => host.availabilityCache.invalidate(first.calendarId));
|
|
3459
3544
|
await attempt(
|
|
@@ -3683,7 +3768,7 @@ async function runSeriesCancelSideEffects(host, db, organizationId, seriesId, se
|
|
|
3683
3768
|
}
|
|
3684
3769
|
const baseUrl = host.appUrl()?.trim();
|
|
3685
3770
|
if (baseUrl) {
|
|
3686
|
-
await attempt("Staff notice", () => queueStaffNotification6(db, occurrences[0].id, "CANCELLATION", baseUrl));
|
|
3771
|
+
await attempt("Staff notice", () => queueStaffNotification6(db, occurrences[0].id, "CANCELLATION", baseUrl, { brand: Boolean(host.brand) }));
|
|
3687
3772
|
} else {
|
|
3688
3773
|
record("Staff notice", "skipped because the application address is not set");
|
|
3689
3774
|
}
|
|
@@ -3888,6 +3973,36 @@ function escapeHtml(text) {
|
|
|
3888
3973
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3889
3974
|
}
|
|
3890
3975
|
|
|
3976
|
+
// src/brand/brand-http.ts
|
|
3977
|
+
import { getBrand, SchedulingValidationError as SchedulingValidationError4 } from "@ordinatio/scheduling";
|
|
3978
|
+
var NOT_ENABLED = { error: "Branding is not enabled on this server" };
|
|
3979
|
+
function brandErrorResponse(error, fallback, cors) {
|
|
3980
|
+
const send = (payload, status) => cors ? corsJson(cors, payload, { status }) : Response.json(payload, { status });
|
|
3981
|
+
if (error instanceof SchedulingValidationError4) {
|
|
3982
|
+
const details = error.context.errors;
|
|
3983
|
+
return send({ error: error.message, code: error.code, ref: getErrorRef(error), ...details ? { details } : {} }, 400);
|
|
3984
|
+
}
|
|
3985
|
+
console.error("[brand]", error);
|
|
3986
|
+
return send({ error: fallback }, 500);
|
|
3987
|
+
}
|
|
3988
|
+
async function requireBrandAdmin(host, request, options) {
|
|
3989
|
+
const staff = await requireAdminStaff(host, request, options);
|
|
3990
|
+
if (staff instanceof Response) return staff;
|
|
3991
|
+
if (!host.brand) return Response.json(NOT_ENABLED, { status: 501 });
|
|
3992
|
+
return staff;
|
|
3993
|
+
}
|
|
3994
|
+
async function businessName(host, db, organizationId) {
|
|
3995
|
+
if (host.brand) {
|
|
3996
|
+
try {
|
|
3997
|
+
const brand = await getBrand(db, organizationId);
|
|
3998
|
+
if (brand) return brand.displayName;
|
|
3999
|
+
} catch (error) {
|
|
4000
|
+
console.error("[brand] could not read the brand for a message:", error);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
return host.appName() || null;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
3891
4006
|
// src/handlers/appointment-messages.ts
|
|
3892
4007
|
var NOT_FOUND4 = () => Response.json({ error: "Appointment not found" }, { status: 404 });
|
|
3893
4008
|
function failure2(label, error, fallback) {
|
|
@@ -3928,7 +4043,7 @@ async function sendFollowup(request, { host, params }) {
|
|
|
3928
4043
|
{ status: 400 }
|
|
3929
4044
|
);
|
|
3930
4045
|
}
|
|
3931
|
-
const staffName = appointment.calendar.staffName || host
|
|
4046
|
+
const staffName = appointment.calendar.staffName || await businessName(host, db, organizationId) || "Your provider";
|
|
3932
4047
|
const serviceName = appointment.type.name;
|
|
3933
4048
|
await db.scheduledNotification.deleteMany({
|
|
3934
4049
|
where: { appointmentId: id, trigger: "FOLLOWUP_MANUAL", status: "PENDING" }
|
|
@@ -3999,6 +4114,7 @@ async function sendReminder(request, { host, params }) {
|
|
|
3999
4114
|
month: "long",
|
|
4000
4115
|
day: "numeric"
|
|
4001
4116
|
}) + " at " + time;
|
|
4117
|
+
const providerName = appointment.calendar.staffName || await businessName(host, db, organizationId);
|
|
4002
4118
|
await db.scheduledNotification.deleteMany({
|
|
4003
4119
|
where: { appointmentId: id, trigger: "REMINDER_MANUAL", status: "PENDING" }
|
|
4004
4120
|
});
|
|
@@ -4009,7 +4125,7 @@ async function sendReminder(request, { host, params }) {
|
|
|
4009
4125
|
channel: "SMS",
|
|
4010
4126
|
trigger: "REMINDER_MANUAL",
|
|
4011
4127
|
recipient: appointment.clientPhone,
|
|
4012
|
-
body: `Reminder: Your ${appointment.type.name} with ${
|
|
4128
|
+
body: `Reminder: Your ${appointment.type.name} with ${providerName || "your provider"} is on ${dateTime}.`,
|
|
4013
4129
|
scheduledFor: /* @__PURE__ */ new Date()
|
|
4014
4130
|
}
|
|
4015
4131
|
});
|
|
@@ -4030,7 +4146,7 @@ Date: ${dateTime}
|
|
|
4030
4146
|
|
|
4031
4147
|
We look forward to seeing you!
|
|
4032
4148
|
|
|
4033
|
-
\u2014 ${escapeHtml(
|
|
4149
|
+
\u2014 ${escapeHtml(providerName || "Your provider")}`,
|
|
4034
4150
|
scheduledFor: /* @__PURE__ */ new Date()
|
|
4035
4151
|
}
|
|
4036
4152
|
});
|
|
@@ -4474,7 +4590,7 @@ async function getLocationRecents(request, { host, params }) {
|
|
|
4474
4590
|
}
|
|
4475
4591
|
|
|
4476
4592
|
// src/handlers/admin-availability-rules.ts
|
|
4477
|
-
import { listAvailabilityRules, SchedulingValidationError as
|
|
4593
|
+
import { listAvailabilityRules, SchedulingValidationError as SchedulingValidationError5, setWeeklyAvailability } from "@ordinatio/scheduling";
|
|
4478
4594
|
|
|
4479
4595
|
// src/availability/admin-calendar.ts
|
|
4480
4596
|
async function verifyCalendar(db, calendarId, organizationId) {
|
|
@@ -4568,7 +4684,7 @@ async function setWeeklyRules(request, { host, params }) {
|
|
|
4568
4684
|
return Response.json(created);
|
|
4569
4685
|
} catch (error) {
|
|
4570
4686
|
const msg = getErrorMessage(error, "Failed to update availability");
|
|
4571
|
-
if (error instanceof
|
|
4687
|
+
if (error instanceof SchedulingValidationError5) {
|
|
4572
4688
|
console.error("[PUT /admin/availability]", error.ref, error.code, error.context);
|
|
4573
4689
|
return Response.json({ error: msg, ref: error.ref }, { status: 400 });
|
|
4574
4690
|
}
|
|
@@ -4582,7 +4698,7 @@ import {
|
|
|
4582
4698
|
addTimeBlock,
|
|
4583
4699
|
removeDateOverride,
|
|
4584
4700
|
SchedulingNotFoundError as SchedulingNotFoundError5,
|
|
4585
|
-
SchedulingValidationError as
|
|
4701
|
+
SchedulingValidationError as SchedulingValidationError6
|
|
4586
4702
|
} from "@ordinatio/scheduling";
|
|
4587
4703
|
|
|
4588
4704
|
// src/availability/override-helpers.ts
|
|
@@ -4667,7 +4783,7 @@ async function addOverride(request, { host, params }) {
|
|
|
4667
4783
|
return Response.json({ ...override, ...overlapWarning && { warning: overlapWarning } }, { status: 201 });
|
|
4668
4784
|
} catch (error) {
|
|
4669
4785
|
const msg = getErrorMessage(error, "Failed to add override");
|
|
4670
|
-
if (error instanceof
|
|
4786
|
+
if (error instanceof SchedulingValidationError6) {
|
|
4671
4787
|
console.error("[POST /admin/availability]", error.ref, error.code, error.context);
|
|
4672
4788
|
return Response.json({ error: msg, ref: error.ref }, { status: 400 });
|
|
4673
4789
|
}
|
|
@@ -4778,7 +4894,7 @@ async function createTemplate(request, { host }) {
|
|
|
4778
4894
|
}
|
|
4779
4895
|
|
|
4780
4896
|
// src/handlers/admin-template-update.ts
|
|
4781
|
-
import { deleteNotificationTemplate, SchedulingNotFoundError as SchedulingNotFoundError6, SchedulingValidationError as
|
|
4897
|
+
import { deleteNotificationTemplate, SchedulingNotFoundError as SchedulingNotFoundError6, SchedulingValidationError as SchedulingValidationError7, updateNotificationTemplate } from "@ordinatio/scheduling";
|
|
4782
4898
|
async function updateTemplate(request, { host, params }) {
|
|
4783
4899
|
try {
|
|
4784
4900
|
const staff = await requireAdminStaff(host, request, { action: "admin-templates-update", tier: "UPDATE" });
|
|
@@ -4824,7 +4940,7 @@ async function updateTemplate(request, { host, params }) {
|
|
|
4824
4940
|
return Response.json(updated);
|
|
4825
4941
|
} catch (error) {
|
|
4826
4942
|
const msg = getErrorMessage(error, "Failed to update template");
|
|
4827
|
-
if (error instanceof
|
|
4943
|
+
if (error instanceof SchedulingValidationError7) {
|
|
4828
4944
|
console.error("[PUT /admin/templates]", error.ref, error.code, error.context);
|
|
4829
4945
|
return Response.json({ error: msg, ref: error.ref }, { status: 400 });
|
|
4830
4946
|
}
|
|
@@ -4952,7 +5068,7 @@ async function createType(request, { host }) {
|
|
|
4952
5068
|
}
|
|
4953
5069
|
|
|
4954
5070
|
// src/handlers/admin-type-update.ts
|
|
4955
|
-
import { SchedulingValidationError as
|
|
5071
|
+
import { SchedulingValidationError as SchedulingValidationError8, updateAppointmentType } from "@ordinatio/scheduling";
|
|
4956
5072
|
|
|
4957
5073
|
// src/handlers/type-ownership.ts
|
|
4958
5074
|
async function getOwnedAppointmentType(db, id, organizationId) {
|
|
@@ -5050,7 +5166,7 @@ async function updateType(request, { host, params }) {
|
|
|
5050
5166
|
return Response.json({ error: getErrorMessage(error, "Validation error") }, { status: schedErrorStatus(code) });
|
|
5051
5167
|
}
|
|
5052
5168
|
const message = isSchedulingServiceError(error) ? error.description || error.message : getErrorMessage(error, "Failed to update appointment type");
|
|
5053
|
-
if (error instanceof
|
|
5169
|
+
if (error instanceof SchedulingValidationError8) return Response.json({ error: message }, { status: 400 });
|
|
5054
5170
|
return Response.json({ error: message }, { status: 500 });
|
|
5055
5171
|
}
|
|
5056
5172
|
}
|
|
@@ -5330,7 +5446,7 @@ async function getAppointmentIcal(request, { host }) {
|
|
|
5330
5446
|
const description = [appointment.type.description ?? "", `Manage your appointment: ${manageUrl}`].filter(Boolean).join("\n");
|
|
5331
5447
|
const calendar = ical({ name: appointment.calendar.name });
|
|
5332
5448
|
calendar.createEvent({
|
|
5333
|
-
id: `${appointment.id}
|
|
5449
|
+
id: `${appointment.id}@${host.icalUidDomain() || "ordinatio-scheduling"}`,
|
|
5334
5450
|
start: appointment.startTime,
|
|
5335
5451
|
end: appointment.endTime,
|
|
5336
5452
|
timezone: appointment.timezone,
|
|
@@ -5362,7 +5478,7 @@ import {
|
|
|
5362
5478
|
} from "@ordinatio/scheduling";
|
|
5363
5479
|
|
|
5364
5480
|
// src/webhooks/require-webhooks.ts
|
|
5365
|
-
import { SchedulingNotFoundError as SchedulingNotFoundError7, SchedulingValidationError as
|
|
5481
|
+
import { SchedulingNotFoundError as SchedulingNotFoundError7, SchedulingValidationError as SchedulingValidationError9 } from "@ordinatio/scheduling";
|
|
5366
5482
|
async function requireWebhooksAdmin(host, request, options) {
|
|
5367
5483
|
const staff = await requireAdminStaff(host, request, options);
|
|
5368
5484
|
if (staff instanceof Response) return staff;
|
|
@@ -5370,7 +5486,7 @@ async function requireWebhooksAdmin(host, request, options) {
|
|
|
5370
5486
|
return { staff, settings: host.webhooks };
|
|
5371
5487
|
}
|
|
5372
5488
|
function webhookErrorResponse(error, fallback) {
|
|
5373
|
-
if (error instanceof
|
|
5489
|
+
if (error instanceof SchedulingValidationError9) {
|
|
5374
5490
|
return Response.json({ error: error.message, ref: getErrorRef(error) }, { status: 400 });
|
|
5375
5491
|
}
|
|
5376
5492
|
if (error instanceof SchedulingNotFoundError7) {
|
|
@@ -5508,33 +5624,142 @@ async function deliverWebhooks(request, { host }) {
|
|
|
5508
5624
|
}
|
|
5509
5625
|
}
|
|
5510
5626
|
|
|
5511
|
-
// src/handlers/
|
|
5512
|
-
import {
|
|
5513
|
-
|
|
5514
|
-
// src/booking/booking-schema.ts
|
|
5627
|
+
// src/handlers/waitlist-join.ts
|
|
5628
|
+
import { joinWaitlist, leaveWaitlist } from "@ordinatio/scheduling";
|
|
5515
5629
|
import { z as z7 } from "zod";
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5630
|
+
|
|
5631
|
+
// src/waitlist/waitlist-http.ts
|
|
5632
|
+
import { SchedulingNotFoundError as SchedulingNotFoundError9, SchedulingValidationError as SchedulingValidationError10 } from "@ordinatio/scheduling";
|
|
5633
|
+
var NOT_ENABLED2 = { error: "The waitlist is not enabled on this server" };
|
|
5634
|
+
var STATUS_BY_CODE = {
|
|
5635
|
+
SCHED_1100: 404,
|
|
5636
|
+
SCHED_1103: 409,
|
|
5637
|
+
SCHED_1104: 409,
|
|
5638
|
+
SCHED_1106: 404,
|
|
5639
|
+
SCHED_1107: 410,
|
|
5640
|
+
SCHED_1108: 409
|
|
5641
|
+
};
|
|
5642
|
+
async function resolveWaitlistDb(host, orgSlug) {
|
|
5643
|
+
if (!orgSlug) return { db: await host.publicDb(), organizationId: null };
|
|
5644
|
+
return host.organizations.resolve({ orgSlug, logPrefix: "[waitlist]" });
|
|
5645
|
+
}
|
|
5646
|
+
function waitlistErrorResponse(cors, error, fallback) {
|
|
5647
|
+
if (error instanceof SchedulingValidationError10 || error instanceof SchedulingNotFoundError9) {
|
|
5648
|
+
const status = STATUS_BY_CODE[error.code] ?? (error instanceof SchedulingNotFoundError9 ? 404 : 400);
|
|
5649
|
+
return corsJson(cors, { error: error.message, code: error.code, ref: getErrorRef(error) }, { status });
|
|
5650
|
+
}
|
|
5651
|
+
console.error("[waitlist]", error);
|
|
5652
|
+
return corsJson(cors, { error: fallback }, { status: 500 });
|
|
5653
|
+
}
|
|
5654
|
+
async function requireWaitlistAdmin(host, request, options) {
|
|
5655
|
+
const staff = await requireAdminStaff(host, request, options);
|
|
5656
|
+
if (staff instanceof Response) return staff;
|
|
5657
|
+
if (!host.waitlist) return Response.json(NOT_ENABLED2, { status: 501 });
|
|
5658
|
+
return { staff, settings: host.waitlist };
|
|
5659
|
+
}
|
|
5660
|
+
|
|
5661
|
+
// src/handlers/waitlist-join.ts
|
|
5662
|
+
var JoinWaitlistSchema = z7.object({
|
|
5663
|
+
orgSlug: z7.string().min(1).max(100).optional(),
|
|
5519
5664
|
typeId: z7.string().min(1),
|
|
5520
|
-
|
|
5521
|
-
|
|
5665
|
+
calendarId: z7.string().min(1).optional(),
|
|
5666
|
+
day: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Day must be YYYY-MM-DD"),
|
|
5667
|
+
slotStart: z7.string().datetime({ message: "Invalid date/time format" }).optional(),
|
|
5522
5668
|
clientName: z7.string().trim().min(1, "Name is required").max(200),
|
|
5523
5669
|
clientEmail: z7.string().email("Invalid email").max(254),
|
|
5524
|
-
clientPhone: z7.string().max(30).optional()
|
|
5670
|
+
clientPhone: z7.string().max(30).optional()
|
|
5671
|
+
});
|
|
5672
|
+
async function joinWaitlistRoute(request, { host }) {
|
|
5673
|
+
const cors = host.cors(request);
|
|
5674
|
+
if (!host.waitlist) return corsJson(cors, NOT_ENABLED2, { status: 501 });
|
|
5675
|
+
try {
|
|
5676
|
+
const limited = await host.rateLimit(`waitlist-join:${clientAddress(request)}`, "PUBLIC_BOOKING");
|
|
5677
|
+
if (limited) return limited;
|
|
5678
|
+
const parsed = JoinWaitlistSchema.safeParse(await request.json().catch(() => null));
|
|
5679
|
+
if (!parsed.success) {
|
|
5680
|
+
const details = parsed.error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message }));
|
|
5681
|
+
return corsJson(cors, { error: "Validation failed", details }, { status: 400 });
|
|
5682
|
+
}
|
|
5683
|
+
const body = parsed.data;
|
|
5684
|
+
const context = await resolveWaitlistDb(host, body.orgSlug);
|
|
5685
|
+
if (!context) return corsJson(cors, { error: "Appointment type not found" }, { status: 404 });
|
|
5686
|
+
let organizationId = context.organizationId;
|
|
5687
|
+
if (!organizationId) {
|
|
5688
|
+
const type = await context.db.appointmentType.findUnique({ where: { id: body.typeId }, select: { organizationId: true } });
|
|
5689
|
+
organizationId = type?.organizationId ?? null;
|
|
5690
|
+
}
|
|
5691
|
+
if (!organizationId) return corsJson(cors, { error: "Appointment type not found" }, { status: 404 });
|
|
5692
|
+
const { entry, token } = await joinWaitlist(context.db, {
|
|
5693
|
+
organizationId,
|
|
5694
|
+
typeId: body.typeId,
|
|
5695
|
+
calendarId: body.calendarId,
|
|
5696
|
+
day: body.day,
|
|
5697
|
+
slotStart: body.slotStart ? new Date(body.slotStart) : void 0,
|
|
5698
|
+
clientName: body.clientName,
|
|
5699
|
+
clientEmail: body.clientEmail,
|
|
5700
|
+
clientPhone: body.clientPhone
|
|
5701
|
+
});
|
|
5702
|
+
return corsJson(cors, { entry, token }, { status: 201, headers: { "Cache-Control": "no-store" } });
|
|
5703
|
+
} catch (error) {
|
|
5704
|
+
return waitlistErrorResponse(cors, error, "Failed to join the waitlist");
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
async function leaveWaitlistRoute(request, { host }) {
|
|
5708
|
+
const cors = host.cors(request);
|
|
5709
|
+
if (!host.waitlist) return corsJson(cors, NOT_ENABLED2, { status: 501 });
|
|
5710
|
+
try {
|
|
5711
|
+
const limited = await host.rateLimit(`waitlist-leave:${clientAddress(request)}`, "GENERAL");
|
|
5712
|
+
if (limited) return limited;
|
|
5713
|
+
const url = new URL(request.url);
|
|
5714
|
+
const token = url.searchParams.get("token");
|
|
5715
|
+
if (!token) return corsJson(cors, { error: "token is required" }, { status: 400 });
|
|
5716
|
+
const context = await resolveWaitlistDb(host, url.searchParams.get("orgSlug"));
|
|
5717
|
+
if (!context) return corsJson(cors, { error: "No waitlist entry for this link", code: "SCHED_1106" }, { status: 404 });
|
|
5718
|
+
const freed = await leaveWaitlist(context.db, token);
|
|
5719
|
+
if (freed) await offerAndAnnounce(host, host.waitlist, context.db, freed);
|
|
5720
|
+
return corsJson(cors, { success: true });
|
|
5721
|
+
} catch (error) {
|
|
5722
|
+
return waitlistErrorResponse(cors, error, "Failed to leave the waitlist");
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
// src/handlers/waitlist-offer.ts
|
|
5727
|
+
import {
|
|
5728
|
+
beginWaitlistAccept,
|
|
5729
|
+
completeWaitlistAccept,
|
|
5730
|
+
getWaitlistOffer,
|
|
5731
|
+
releaseWaitlistAccept,
|
|
5732
|
+
settleBookedAccept
|
|
5733
|
+
} from "@ordinatio/scheduling";
|
|
5734
|
+
import { z as z9 } from "zod";
|
|
5735
|
+
|
|
5736
|
+
// src/handlers/appointments-book.ts
|
|
5737
|
+
import { bookAppointment } from "@ordinatio/scheduling";
|
|
5738
|
+
|
|
5739
|
+
// src/booking/booking-schema.ts
|
|
5740
|
+
import { z as z8 } from "zod";
|
|
5741
|
+
var BookingSchema = z8.object({
|
|
5742
|
+
calendarId: z8.string().min(1).optional(),
|
|
5743
|
+
preferredCalendarId: z8.string().min(1).optional(),
|
|
5744
|
+
typeId: z8.string().min(1),
|
|
5745
|
+
startTime: z8.string().datetime({ message: "Invalid date/time format" }),
|
|
5746
|
+
timezone: z8.string().max(50).optional(),
|
|
5747
|
+
clientName: z8.string().trim().min(1, "Name is required").max(200),
|
|
5748
|
+
clientEmail: z8.string().email("Invalid email").max(254),
|
|
5749
|
+
clientPhone: z8.string().max(30).optional(),
|
|
5525
5750
|
// Untrusted: the host's client resolver decides whether to honor it.
|
|
5526
|
-
clientId:
|
|
5527
|
-
notes:
|
|
5751
|
+
clientId: z8.string().optional(),
|
|
5752
|
+
notes: z8.string().max(1e3).optional(),
|
|
5528
5753
|
// Only the primitive types a form field produces; z.unknown() would accept anything.
|
|
5529
|
-
intakeResponses:
|
|
5530
|
-
|
|
5531
|
-
|
|
5754
|
+
intakeResponses: z8.record(
|
|
5755
|
+
z8.string(),
|
|
5756
|
+
z8.union([z8.string().max(1e3), z8.number(), z8.boolean(), z8.null()])
|
|
5532
5757
|
).optional(),
|
|
5533
|
-
paymentId:
|
|
5534
|
-
couponCode:
|
|
5535
|
-
packagePurchaseId:
|
|
5758
|
+
paymentId: z8.string().optional(),
|
|
5759
|
+
couponCode: z8.string().min(1).max(50).transform((value) => value.trim().toUpperCase()).optional(),
|
|
5760
|
+
packagePurchaseId: z8.string().optional(),
|
|
5536
5761
|
// Names the organization, so the calendar and type lookups can run against its own database.
|
|
5537
|
-
orgSlug:
|
|
5762
|
+
orgSlug: z8.string().min(1).max(100).optional()
|
|
5538
5763
|
});
|
|
5539
5764
|
|
|
5540
5765
|
// src/booking/finalize-booking.ts
|
|
@@ -5630,7 +5855,7 @@ async function finalizeBooking(host, cors, db, organizationId, body, appointment
|
|
|
5630
5855
|
const baseUrl = host.appUrl();
|
|
5631
5856
|
if (!baseUrl) throw new Error("The host must supply appUrl() to book appointments");
|
|
5632
5857
|
try {
|
|
5633
|
-
await scheduleNotifications5(db, appointment.id, baseUrl);
|
|
5858
|
+
await scheduleNotifications5(db, appointment.id, baseUrl, { brand: Boolean(host.brand) });
|
|
5634
5859
|
} catch (error) {
|
|
5635
5860
|
await logActivity(
|
|
5636
5861
|
"scheduling.notification_schedule_failed",
|
|
@@ -5639,7 +5864,7 @@ async function finalizeBooking(host, cors, db, organizationId, body, appointment
|
|
|
5639
5864
|
);
|
|
5640
5865
|
}
|
|
5641
5866
|
try {
|
|
5642
|
-
await queueStaffNotification7(db, appointment.id, "CONFIRMATION", baseUrl);
|
|
5867
|
+
await queueStaffNotification7(db, appointment.id, "CONFIRMATION", baseUrl, { brand: Boolean(host.brand) });
|
|
5643
5868
|
} catch (error) {
|
|
5644
5869
|
await logActivity(
|
|
5645
5870
|
"scheduling.staff_notification_failed",
|
|
@@ -6000,6 +6225,464 @@ async function bookAppointmentRoute(request, { host }) {
|
|
|
6000
6225
|
}
|
|
6001
6226
|
}
|
|
6002
6227
|
|
|
6228
|
+
// src/handlers/waitlist-offer.ts
|
|
6229
|
+
async function getWaitlistOfferRoute(request, { host }) {
|
|
6230
|
+
const cors = host.cors(request);
|
|
6231
|
+
if (!host.waitlist) return corsJson(cors, NOT_ENABLED2, { status: 501 });
|
|
6232
|
+
try {
|
|
6233
|
+
const limited = await host.rateLimit(`waitlist-offer:${clientAddress(request)}`, "AVAILABILITY");
|
|
6234
|
+
if (limited) return limited;
|
|
6235
|
+
const url = new URL(request.url);
|
|
6236
|
+
const token = url.searchParams.get("token");
|
|
6237
|
+
if (!token) return corsJson(cors, { error: "token is required" }, { status: 400 });
|
|
6238
|
+
const context = await resolveWaitlistDb(host, url.searchParams.get("orgSlug"));
|
|
6239
|
+
if (!context) return corsJson(cors, { error: "No open offer for this link", code: "SCHED_1106" }, { status: 404 });
|
|
6240
|
+
const offer = await getWaitlistOffer(context.db, token);
|
|
6241
|
+
return corsJson(cors, {
|
|
6242
|
+
typeName: offer.typeName,
|
|
6243
|
+
calendarName: offer.calendarName,
|
|
6244
|
+
startTime: offer.startTime,
|
|
6245
|
+
timezone: offer.timezone,
|
|
6246
|
+
expiresAt: offer.expiresAt,
|
|
6247
|
+
clientName: offer.clientName
|
|
6248
|
+
});
|
|
6249
|
+
} catch (error) {
|
|
6250
|
+
return waitlistErrorResponse(cors, error, "Failed to read the offer");
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
var AcceptSchema = z9.object({ token: z9.string().min(1).max(200), orgSlug: z9.string().min(1).max(100).optional() });
|
|
6254
|
+
async function acceptWaitlistOfferRoute(request, context) {
|
|
6255
|
+
const { host } = context;
|
|
6256
|
+
const cors = host.cors(request);
|
|
6257
|
+
const settings = host.waitlist;
|
|
6258
|
+
if (!settings) return corsJson(cors, NOT_ENABLED2, { status: 501 });
|
|
6259
|
+
let entryId = null;
|
|
6260
|
+
let db = null;
|
|
6261
|
+
let booked = false;
|
|
6262
|
+
try {
|
|
6263
|
+
const limited = await host.rateLimit(`waitlist-accept:${clientAddress(request)}`, "GENERAL");
|
|
6264
|
+
if (limited) return limited;
|
|
6265
|
+
const parsed = AcceptSchema.safeParse(await request.json().catch(() => null));
|
|
6266
|
+
if (!parsed.success) return corsJson(cors, { error: "token is required" }, { status: 400 });
|
|
6267
|
+
db = await resolveWaitlistDb(host, parsed.data.orgSlug);
|
|
6268
|
+
if (!db) return corsJson(cors, { error: "No open offer for this link", code: "SCHED_1106" }, { status: 404 });
|
|
6269
|
+
const offer = await beginWaitlistAccept(db.db, parsed.data.token);
|
|
6270
|
+
entryId = offer.entryId;
|
|
6271
|
+
const headers = new Headers(request.headers);
|
|
6272
|
+
headers.set("content-type", "application/json");
|
|
6273
|
+
const response = await bookAppointmentRoute(
|
|
6274
|
+
new Request(request.url, {
|
|
6275
|
+
method: "POST",
|
|
6276
|
+
headers,
|
|
6277
|
+
body: JSON.stringify({
|
|
6278
|
+
calendarId: offer.calendarId,
|
|
6279
|
+
typeId: offer.typeId,
|
|
6280
|
+
startTime: offer.startTime.toISOString(),
|
|
6281
|
+
timezone: offer.timezone,
|
|
6282
|
+
clientName: offer.clientName,
|
|
6283
|
+
clientEmail: offer.clientEmail,
|
|
6284
|
+
...offer.clientPhone ? { clientPhone: offer.clientPhone } : {},
|
|
6285
|
+
...parsed.data.orgSlug ? { orgSlug: parsed.data.orgSlug } : {}
|
|
6286
|
+
})
|
|
6287
|
+
}),
|
|
6288
|
+
context
|
|
6289
|
+
);
|
|
6290
|
+
if (response.status === 201) {
|
|
6291
|
+
booked = true;
|
|
6292
|
+
const appointment = await response.clone().json().catch(() => null);
|
|
6293
|
+
try {
|
|
6294
|
+
await completeWaitlistAccept(db.db, entryId, appointment?.id ?? "");
|
|
6295
|
+
} catch (completeError) {
|
|
6296
|
+
console.error("[waitlist] the booking was saved but its waitlist entry could not be marked booked:", completeError);
|
|
6297
|
+
}
|
|
6298
|
+
return response;
|
|
6299
|
+
}
|
|
6300
|
+
const saved = await settleBookedAccept(db.db, entryId);
|
|
6301
|
+
if (saved) return corsJson(cors, saved, { status: 201 });
|
|
6302
|
+
await endFailedAccept(host, settings, db.db, entryId, releaseFor(response.status));
|
|
6303
|
+
return response;
|
|
6304
|
+
} catch (error) {
|
|
6305
|
+
if (entryId && db && !booked) {
|
|
6306
|
+
try {
|
|
6307
|
+
const saved = await settleBookedAccept(db.db, entryId);
|
|
6308
|
+
if (saved) return corsJson(cors, saved, { status: 201 });
|
|
6309
|
+
await endFailedAccept(host, settings, db.db, entryId, "KEEP_OFFER");
|
|
6310
|
+
} catch (releaseError) {
|
|
6311
|
+
console.error("[waitlist] could not settle or release an accept:", releaseError);
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
return waitlistErrorResponse(cors, error, "Failed to accept the offer");
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
function releaseFor(status) {
|
|
6318
|
+
if (status === 409) return "BACK_IN_LINE";
|
|
6319
|
+
if (status === 429 || status >= 500) return "KEEP_OFFER";
|
|
6320
|
+
return "DROP";
|
|
6321
|
+
}
|
|
6322
|
+
async function endFailedAccept(host, settings, db, entryId, outcome) {
|
|
6323
|
+
const freed = await releaseWaitlistAccept(db, entryId, outcome);
|
|
6324
|
+
if (freed) await offerAndAnnounce(host, settings, db, freed);
|
|
6325
|
+
}
|
|
6326
|
+
|
|
6327
|
+
// src/handlers/admin-waitlist.ts
|
|
6328
|
+
import { listWaitlist, removeWaitlistEntry } from "@ordinatio/scheduling";
|
|
6329
|
+
var STATUSES2 = /* @__PURE__ */ new Set(["WAITING", "OFFERED", "ACCEPTING", "BOOKED", "EXPIRED"]);
|
|
6330
|
+
async function listWaitlistEntries(request, { host }) {
|
|
6331
|
+
try {
|
|
6332
|
+
const guard = await requireWaitlistAdmin(host, request, { action: "admin-waitlist-list", tier: "GENERAL" });
|
|
6333
|
+
if (guard instanceof Response) return guard;
|
|
6334
|
+
const status = new URL(request.url).searchParams.get("status");
|
|
6335
|
+
if (status && !STATUSES2.has(status)) {
|
|
6336
|
+
return Response.json({ error: `status must be one of ${[...STATUSES2].join(", ")}` }, { status: 400 });
|
|
6337
|
+
}
|
|
6338
|
+
const { limit, offset } = parsePagination(request);
|
|
6339
|
+
const { entries, total } = await listWaitlist(guard.staff.db, {
|
|
6340
|
+
organizationId: guard.staff.organizationId,
|
|
6341
|
+
status: status ?? void 0,
|
|
6342
|
+
limit,
|
|
6343
|
+
offset
|
|
6344
|
+
});
|
|
6345
|
+
return Response.json({ entries, total, hasMore: offset + entries.length < total });
|
|
6346
|
+
} catch (error) {
|
|
6347
|
+
return webhookErrorResponse(error, "Failed to list the waitlist");
|
|
6348
|
+
}
|
|
6349
|
+
}
|
|
6350
|
+
async function removeWaitlistEntryRoute(request, { host, params }) {
|
|
6351
|
+
try {
|
|
6352
|
+
const guard = await requireWaitlistAdmin(host, request, { action: "admin-waitlist-remove", tier: "DELETE" });
|
|
6353
|
+
if (guard instanceof Response) return guard;
|
|
6354
|
+
const { db, organizationId } = guard.staff;
|
|
6355
|
+
const freed = await removeWaitlistEntry(db, organizationId, params.id);
|
|
6356
|
+
await host.activity.log(db, {
|
|
6357
|
+
action: "scheduling.waitlist_entry_removed",
|
|
6358
|
+
description: "removed someone from the waitlist",
|
|
6359
|
+
metadata: { entryId: params.id }
|
|
6360
|
+
});
|
|
6361
|
+
if (freed) await offerAndAnnounce(host, guard.settings, db, freed);
|
|
6362
|
+
return Response.json({ success: true });
|
|
6363
|
+
} catch (error) {
|
|
6364
|
+
return webhookErrorResponse(error, "Failed to remove the waitlist entry");
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
// src/handlers/internal-waitlist-sweep.ts
|
|
6369
|
+
import { expireWaitlistOffers } from "@ordinatio/scheduling";
|
|
6370
|
+
async function sweepWaitlist(request, { host }) {
|
|
6371
|
+
if (!await host.internalAccess.isAuthorized(request)) return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
6372
|
+
if (!host.waitlist) return Response.json(NOT_ENABLED2, { status: 501 });
|
|
6373
|
+
const limited = await host.rateLimit(`${request.headers.get("x-forwarded-for") ?? "worker"}:waitlist-sweep`, "INTERNAL");
|
|
6374
|
+
if (limited) return limited;
|
|
6375
|
+
try {
|
|
6376
|
+
const db = await host.publicDb();
|
|
6377
|
+
const { lapsed, released, booked } = await expireWaitlistOffers(db);
|
|
6378
|
+
let reoffered = 0;
|
|
6379
|
+
for (const slot of lapsed) {
|
|
6380
|
+
if (await offerAndAnnounce(host, host.waitlist, db, slot)) reoffered++;
|
|
6381
|
+
}
|
|
6382
|
+
return Response.json({ lapsed: lapsed.length, released, booked, reoffered });
|
|
6383
|
+
} catch (error) {
|
|
6384
|
+
console.error("[scheduling-api] waitlist sweep failed:", error);
|
|
6385
|
+
return Response.json({ error: "Waitlist sweep failed" }, { status: 500 });
|
|
6386
|
+
}
|
|
6387
|
+
}
|
|
6388
|
+
|
|
6389
|
+
// src/handlers/routing-forms-public.ts
|
|
6390
|
+
import { getPublicRoutingForm, routeClient } from "@ordinatio/scheduling";
|
|
6391
|
+
|
|
6392
|
+
// src/routing/routing-http.ts
|
|
6393
|
+
import { SchedulingNotFoundError as SchedulingNotFoundError10, SchedulingValidationError as SchedulingValidationError11 } from "@ordinatio/scheduling";
|
|
6394
|
+
var NOT_ENABLED3 = { error: "Routing forms are not enabled on this server" };
|
|
6395
|
+
var STATUS_BY_CODE2 = { SCHED_1201: 404, SCHED_1202: 409, SCHED_1204: 409 };
|
|
6396
|
+
function routingErrorResponse(error, fallback, cors) {
|
|
6397
|
+
const send = (payload, status) => cors ? corsJson(cors, payload, { status }) : Response.json(payload, { status });
|
|
6398
|
+
if (error instanceof SchedulingValidationError11 || error instanceof SchedulingNotFoundError10) {
|
|
6399
|
+
const details = error.context.errors;
|
|
6400
|
+
const status = STATUS_BY_CODE2[error.code] ?? 400;
|
|
6401
|
+
return send({ error: error.message, code: error.code, ref: getErrorRef(error), ...details ? { details } : {} }, status);
|
|
6402
|
+
}
|
|
6403
|
+
console.error("[routing-forms]", error);
|
|
6404
|
+
return send({ error: fallback }, 500);
|
|
6405
|
+
}
|
|
6406
|
+
async function requireRoutingAdmin(host, request, options) {
|
|
6407
|
+
const staff = await requireAdminStaff(host, request, options);
|
|
6408
|
+
if (staff instanceof Response) return staff;
|
|
6409
|
+
if (!host.routingForms) return Response.json(NOT_ENABLED3, { status: 501 });
|
|
6410
|
+
return staff;
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6413
|
+
// src/handlers/routing-forms-public.ts
|
|
6414
|
+
async function resolveOrganization(host, orgSlug) {
|
|
6415
|
+
if (!orgSlug) return { db: await host.publicDb(), organizationId: null };
|
|
6416
|
+
return host.organizations.resolve({ orgSlug, logPrefix: "[routing-forms]" });
|
|
6417
|
+
}
|
|
6418
|
+
async function getRoutingFormRoute(request, { host }) {
|
|
6419
|
+
const cors = host.cors(request);
|
|
6420
|
+
if (!host.routingForms) return corsJson(cors, NOT_ENABLED3, { status: 501 });
|
|
6421
|
+
try {
|
|
6422
|
+
const limited = await host.rateLimit(`routing-form:${clientAddress(request)}`, "AVAILABILITY");
|
|
6423
|
+
if (limited) return limited;
|
|
6424
|
+
const url = new URL(request.url);
|
|
6425
|
+
const slug = url.searchParams.get("slug");
|
|
6426
|
+
if (!slug) return corsJson(cors, { error: "slug is required" }, { status: 400 });
|
|
6427
|
+
const context = await resolveOrganization(host, url.searchParams.get("orgSlug"));
|
|
6428
|
+
if (!context) return corsJson(cors, { error: "Routing form not found", code: "SCHED_1201" }, { status: 404 });
|
|
6429
|
+
return corsJson(cors, await getPublicRoutingForm(context.db, { organizationId: context.organizationId, slug }));
|
|
6430
|
+
} catch (error) {
|
|
6431
|
+
return routingErrorResponse(error, "Failed to read the form", cors);
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
async function routeClientRoute(request, { host }) {
|
|
6435
|
+
const cors = host.cors(request);
|
|
6436
|
+
if (!host.routingForms) return corsJson(cors, NOT_ENABLED3, { status: 501 });
|
|
6437
|
+
try {
|
|
6438
|
+
const limited = await host.rateLimit(`routing-route:${clientAddress(request)}`, "AVAILABILITY");
|
|
6439
|
+
if (limited) return limited;
|
|
6440
|
+
const parsed = RouteClientSchema.safeParse(await request.json().catch(() => null));
|
|
6441
|
+
if (!parsed.success) {
|
|
6442
|
+
const details = parsed.error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message }));
|
|
6443
|
+
return corsJson(cors, { error: "Validation failed", details }, { status: 400 });
|
|
6444
|
+
}
|
|
6445
|
+
const context = await resolveOrganization(host, parsed.data.orgSlug);
|
|
6446
|
+
if (!context) return corsJson(cors, { error: "Routing form not found", code: "SCHED_1201" }, { status: 404 });
|
|
6447
|
+
const outcome = await routeClient(context.db, { organizationId: context.organizationId, slug: parsed.data.slug, answers: parsed.data.answers });
|
|
6448
|
+
return corsJson(cors, outcome);
|
|
6449
|
+
} catch (error) {
|
|
6450
|
+
return routingErrorResponse(error, "Failed to route the answers", cors);
|
|
6451
|
+
}
|
|
6452
|
+
}
|
|
6453
|
+
|
|
6454
|
+
// src/handlers/admin-routing-forms.ts
|
|
6455
|
+
import { createRoutingForm, listRoutingForms } from "@ordinatio/scheduling";
|
|
6456
|
+
async function listRoutingFormsRoute(request, { host }) {
|
|
6457
|
+
try {
|
|
6458
|
+
const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-list", tier: "GENERAL" });
|
|
6459
|
+
if (staff instanceof Response) return staff;
|
|
6460
|
+
return Response.json({ forms: await listRoutingForms(staff.db, staff.organizationId) });
|
|
6461
|
+
} catch (error) {
|
|
6462
|
+
return routingErrorResponse(error, "Failed to list the forms");
|
|
6463
|
+
}
|
|
6464
|
+
}
|
|
6465
|
+
async function createRoutingFormRoute(request, { host }) {
|
|
6466
|
+
try {
|
|
6467
|
+
const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-create", tier: "CREATE" });
|
|
6468
|
+
if (staff instanceof Response) return staff;
|
|
6469
|
+
const validation = await parseBody(request, CreateRoutingFormSchema);
|
|
6470
|
+
if (!validation.success) return validation.response;
|
|
6471
|
+
const form = await createRoutingForm(staff.db, staff.organizationId, validation.data);
|
|
6472
|
+
await host.activity.log(staff.db, {
|
|
6473
|
+
action: "scheduling.routing_form_created",
|
|
6474
|
+
description: `created routing form "${form.name}"`,
|
|
6475
|
+
metadata: { formId: form.id, slug: form.slug }
|
|
6476
|
+
});
|
|
6477
|
+
return Response.json({ form }, { status: 201 });
|
|
6478
|
+
} catch (error) {
|
|
6479
|
+
return routingErrorResponse(error, "Failed to save the form");
|
|
6480
|
+
}
|
|
6481
|
+
}
|
|
6482
|
+
|
|
6483
|
+
// src/handlers/brand-public.ts
|
|
6484
|
+
import { getPublicBrand } from "@ordinatio/scheduling";
|
|
6485
|
+
async function getBrandRoute(request, { host }) {
|
|
6486
|
+
const cors = host.cors(request);
|
|
6487
|
+
if (!host.brand) return corsJson(cors, NOT_ENABLED, { status: 501 });
|
|
6488
|
+
try {
|
|
6489
|
+
const limited = await host.rateLimit(`brand:${clientAddress(request)}`, "AVAILABILITY");
|
|
6490
|
+
if (limited) return limited;
|
|
6491
|
+
const orgSlug = new URL(request.url).searchParams.get("orgSlug");
|
|
6492
|
+
const context = orgSlug ? await host.organizations.resolve({ orgSlug, logPrefix: "[brand]" }) : { db: await host.publicDb(), organizationId: null };
|
|
6493
|
+
const brand = context ? await getPublicBrand(context.db, context.organizationId) : null;
|
|
6494
|
+
if (!brand) return corsJson(cors, { error: "No brand set" }, { status: 404 });
|
|
6495
|
+
return corsJson(cors, { brand }, { headers: WIDGET_CACHE_HEADERS });
|
|
6496
|
+
} catch (error) {
|
|
6497
|
+
return brandErrorResponse(error, "Failed to read the brand", cors);
|
|
6498
|
+
}
|
|
6499
|
+
}
|
|
6500
|
+
|
|
6501
|
+
// src/handlers/admin-brand.ts
|
|
6502
|
+
import { deleteBrand, getBrand as getBrand2, saveBrand } from "@ordinatio/scheduling";
|
|
6503
|
+
async function getBrandAdminRoute(request, { host }) {
|
|
6504
|
+
try {
|
|
6505
|
+
const staff = await requireBrandAdmin(host, request, { action: "admin-brand-get", tier: "GENERAL" });
|
|
6506
|
+
if (staff instanceof Response) return staff;
|
|
6507
|
+
return Response.json({ brand: await getBrand2(staff.db, staff.organizationId) });
|
|
6508
|
+
} catch (error) {
|
|
6509
|
+
return brandErrorResponse(error, "Failed to read the brand");
|
|
6510
|
+
}
|
|
6511
|
+
}
|
|
6512
|
+
async function saveBrandRoute(request, { host }) {
|
|
6513
|
+
try {
|
|
6514
|
+
const staff = await requireBrandAdmin(host, request, { action: "admin-brand-save", tier: "UPDATE" });
|
|
6515
|
+
if (staff instanceof Response) return staff;
|
|
6516
|
+
const validation = await parseBody(request, BrandSchema);
|
|
6517
|
+
if (!validation.success) return validation.response;
|
|
6518
|
+
const verifier = host.brand?.isSendingDomainVerified;
|
|
6519
|
+
const brand = await saveBrand(staff.db, staff.organizationId, validation.data, {
|
|
6520
|
+
isSendingDomainVerified: verifier ? async (domain) => Boolean(await verifier.call(host.brand, domain, staff.organizationId)) : void 0
|
|
6521
|
+
});
|
|
6522
|
+
await host.activity.log(staff.db, {
|
|
6523
|
+
action: "scheduling.brand_saved",
|
|
6524
|
+
description: "changed the brand",
|
|
6525
|
+
metadata: { displayName: brand.displayName, sendsFromOwnDomain: Boolean(brand.fromEmail) }
|
|
6526
|
+
});
|
|
6527
|
+
return Response.json({ brand });
|
|
6528
|
+
} catch (error) {
|
|
6529
|
+
return brandErrorResponse(error, "Failed to save the brand");
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
async function deleteBrandRoute(request, { host }) {
|
|
6533
|
+
try {
|
|
6534
|
+
const staff = await requireBrandAdmin(host, request, { action: "admin-brand-delete", tier: "DELETE" });
|
|
6535
|
+
if (staff instanceof Response) return staff;
|
|
6536
|
+
const removed = await deleteBrand(staff.db, staff.organizationId);
|
|
6537
|
+
if (removed) await host.activity.log(staff.db, { action: "scheduling.brand_removed", description: "removed the brand", metadata: {} });
|
|
6538
|
+
return Response.json({ removed });
|
|
6539
|
+
} catch (error) {
|
|
6540
|
+
return brandErrorResponse(error, "Failed to remove the brand");
|
|
6541
|
+
}
|
|
6542
|
+
}
|
|
6543
|
+
|
|
6544
|
+
// src/handlers/admin-privacy.ts
|
|
6545
|
+
import { eraseSubject, exportSubject, listPrivacyRequests, previewErase } from "@ordinatio/scheduling";
|
|
6546
|
+
|
|
6547
|
+
// src/privacy/privacy-http.ts
|
|
6548
|
+
import { SchedulingValidationError as SchedulingValidationError12 } from "@ordinatio/scheduling";
|
|
6549
|
+
var NOT_ENABLED4 = { error: "Data export and erasure are not enabled on this server" };
|
|
6550
|
+
var STATUS_BY_CODE3 = { SCHED_1300: 400, SCHED_1301: 409 };
|
|
6551
|
+
function privacyErrorResponse(error, fallback) {
|
|
6552
|
+
if (error instanceof SchedulingValidationError12) {
|
|
6553
|
+
return Response.json(
|
|
6554
|
+
{ error: error.message, code: error.code, ref: getErrorRef(error), details: error.context },
|
|
6555
|
+
{ status: STATUS_BY_CODE3[error.code] ?? 400 }
|
|
6556
|
+
);
|
|
6557
|
+
}
|
|
6558
|
+
console.error("[privacy]", error);
|
|
6559
|
+
return Response.json({ error: fallback }, { status: 500 });
|
|
6560
|
+
}
|
|
6561
|
+
async function requirePrivacyAdmin(host, request, options) {
|
|
6562
|
+
const staff = await requireAdminStaff(host, request, options);
|
|
6563
|
+
if (staff instanceof Response) return staff;
|
|
6564
|
+
if (!host.privacy) return Response.json(NOT_ENABLED4, { status: 501 });
|
|
6565
|
+
return staff;
|
|
6566
|
+
}
|
|
6567
|
+
|
|
6568
|
+
// src/handlers/admin-privacy.ts
|
|
6569
|
+
async function logQuietly(host, db, entry) {
|
|
6570
|
+
try {
|
|
6571
|
+
await host.activity.log(db, entry);
|
|
6572
|
+
} catch (error) {
|
|
6573
|
+
console.error("[privacy] could not write the activity entry:", error);
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6576
|
+
async function exportPrivacyRoute(request, { host }) {
|
|
6577
|
+
try {
|
|
6578
|
+
const staff = await requirePrivacyAdmin(host, request, { action: "admin-privacy-export", tier: "CREATE" });
|
|
6579
|
+
if (staff instanceof Response) return staff;
|
|
6580
|
+
const validation = await parseBody(request, PrivacySubjectSchema);
|
|
6581
|
+
if (!validation.success) return validation.response;
|
|
6582
|
+
const result = await exportSubject(staff.db, { organizationId: staff.organizationId, requestedBy: staff.userId, ...validation.data });
|
|
6583
|
+
await logQuietly(host, staff.db, {
|
|
6584
|
+
action: "scheduling.privacy_export",
|
|
6585
|
+
description: "exported the data held about one person",
|
|
6586
|
+
metadata: { counts: result.counts }
|
|
6587
|
+
});
|
|
6588
|
+
const stamp = result.exportedAt.slice(0, 10);
|
|
6589
|
+
return new Response(JSON.stringify(result, null, 2), {
|
|
6590
|
+
status: 200,
|
|
6591
|
+
headers: {
|
|
6592
|
+
"content-type": "application/json; charset=utf-8",
|
|
6593
|
+
"content-disposition": `attachment; filename="scheduling-data-export-${stamp}.json"`,
|
|
6594
|
+
"cache-control": "no-store"
|
|
6595
|
+
}
|
|
6596
|
+
});
|
|
6597
|
+
} catch (error) {
|
|
6598
|
+
return privacyErrorResponse(error, "Failed to export the data");
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
async function erasePrivacyRoute(request, { host }) {
|
|
6602
|
+
try {
|
|
6603
|
+
const staff = await requirePrivacyAdmin(host, request, { action: "admin-privacy-erase", tier: "DELETE" });
|
|
6604
|
+
if (staff instanceof Response) return staff;
|
|
6605
|
+
const validation = await parseBody(request, PrivacyEraseSchema);
|
|
6606
|
+
if (!validation.success) return validation.response;
|
|
6607
|
+
const { confirm, ...subject } = validation.data;
|
|
6608
|
+
if (confirm !== true) {
|
|
6609
|
+
const preview = await previewErase(staff.db, { organizationId: staff.organizationId, ...subject });
|
|
6610
|
+
return Response.json({ erased: false, preview }, { headers: { "cache-control": "no-store" } });
|
|
6611
|
+
}
|
|
6612
|
+
const { counts } = await eraseSubject(staff.db, { organizationId: staff.organizationId, requestedBy: staff.userId, ...subject });
|
|
6613
|
+
await logQuietly(host, staff.db, {
|
|
6614
|
+
action: "scheduling.privacy_erase",
|
|
6615
|
+
description: "erased the data held about one person",
|
|
6616
|
+
metadata: { counts }
|
|
6617
|
+
});
|
|
6618
|
+
return Response.json({ erased: true, counts }, { headers: { "cache-control": "no-store" } });
|
|
6619
|
+
} catch (error) {
|
|
6620
|
+
return privacyErrorResponse(error, "Failed to erase the data");
|
|
6621
|
+
}
|
|
6622
|
+
}
|
|
6623
|
+
async function listPrivacyRequestsRoute(request, { host }) {
|
|
6624
|
+
try {
|
|
6625
|
+
const staff = await requirePrivacyAdmin(host, request, { action: "admin-privacy-requests", tier: "GENERAL" });
|
|
6626
|
+
if (staff instanceof Response) return staff;
|
|
6627
|
+
const { limit, offset } = parsePagination(request);
|
|
6628
|
+
const { requests, total } = await listPrivacyRequests(staff.db, { organizationId: staff.organizationId, limit, offset });
|
|
6629
|
+
return Response.json({ requests, total, hasMore: offset + requests.length < total });
|
|
6630
|
+
} catch (error) {
|
|
6631
|
+
return privacyErrorResponse(error, "Failed to list the requests");
|
|
6632
|
+
}
|
|
6633
|
+
}
|
|
6634
|
+
|
|
6635
|
+
// src/handlers/admin-routing-form-detail.ts
|
|
6636
|
+
import { deleteRoutingForm, getRoutingForm, testRoutingForm, updateRoutingForm } from "@ordinatio/scheduling";
|
|
6637
|
+
async function getRoutingFormAdminRoute(request, { host, params }) {
|
|
6638
|
+
try {
|
|
6639
|
+
const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-get", tier: "GENERAL" });
|
|
6640
|
+
if (staff instanceof Response) return staff;
|
|
6641
|
+
return Response.json({ form: await getRoutingForm(staff.db, staff.organizationId, params.id) });
|
|
6642
|
+
} catch (error) {
|
|
6643
|
+
return routingErrorResponse(error, "Failed to read the form");
|
|
6644
|
+
}
|
|
6645
|
+
}
|
|
6646
|
+
async function updateRoutingFormRoute(request, { host, params }) {
|
|
6647
|
+
try {
|
|
6648
|
+
const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-update", tier: "UPDATE" });
|
|
6649
|
+
if (staff instanceof Response) return staff;
|
|
6650
|
+
const validation = await parseBody(request, UpdateRoutingFormSchema);
|
|
6651
|
+
if (!validation.success) return validation.response;
|
|
6652
|
+
const form = await updateRoutingForm(staff.db, staff.organizationId, params.id, validation.data);
|
|
6653
|
+
await host.activity.log(staff.db, {
|
|
6654
|
+
action: "scheduling.routing_form_updated",
|
|
6655
|
+
description: `changed routing form "${form.name}"`,
|
|
6656
|
+
metadata: { formId: form.id, fields: Object.keys(validation.data) }
|
|
6657
|
+
});
|
|
6658
|
+
return Response.json({ form });
|
|
6659
|
+
} catch (error) {
|
|
6660
|
+
return routingErrorResponse(error, "Failed to save the form");
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
async function deleteRoutingFormRoute(request, { host, params }) {
|
|
6664
|
+
try {
|
|
6665
|
+
const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-delete", tier: "DELETE" });
|
|
6666
|
+
if (staff instanceof Response) return staff;
|
|
6667
|
+
await deleteRoutingForm(staff.db, staff.organizationId, params.id);
|
|
6668
|
+
await host.activity.log(staff.db, { action: "scheduling.routing_form_deleted", description: "removed a routing form", metadata: { formId: params.id } });
|
|
6669
|
+
return Response.json({ success: true });
|
|
6670
|
+
} catch (error) {
|
|
6671
|
+
return routingErrorResponse(error, "Failed to remove the form");
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
async function testRoutingFormRoute(request, { host, params }) {
|
|
6675
|
+
try {
|
|
6676
|
+
const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-test", tier: "GENERAL" });
|
|
6677
|
+
if (staff instanceof Response) return staff;
|
|
6678
|
+
const validation = await parseBody(request, TestRoutingFormSchema);
|
|
6679
|
+
if (!validation.success) return validation.response;
|
|
6680
|
+
return Response.json(await testRoutingForm(staff.db, { organizationId: staff.organizationId, id: params.id, answers: validation.data.answers }));
|
|
6681
|
+
} catch (error) {
|
|
6682
|
+
return routingErrorResponse(error, "Failed to test the form");
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
|
|
6003
6686
|
// src/handlers/appointments-list.ts
|
|
6004
6687
|
import { schedError as schedError28 } from "@ordinatio/scheduling";
|
|
6005
6688
|
var DEFAULT_LIMIT2 = 50;
|
|
@@ -6379,12 +7062,12 @@ async function getAppointmentTypes(request, { host }) {
|
|
|
6379
7062
|
}
|
|
6380
7063
|
|
|
6381
7064
|
// src/handlers/coupons-validate.ts
|
|
6382
|
-
import { z as
|
|
7065
|
+
import { z as z10 } from "zod";
|
|
6383
7066
|
import { schedError as schedError30 } from "@ordinatio/scheduling";
|
|
6384
|
-
var Schema4 =
|
|
6385
|
-
code:
|
|
6386
|
-
calendarId:
|
|
6387
|
-
price:
|
|
7067
|
+
var Schema4 = z10.object({
|
|
7068
|
+
code: z10.string().min(1).max(50).transform((v) => v.trim().toUpperCase()),
|
|
7069
|
+
calendarId: z10.string().min(1),
|
|
7070
|
+
price: z10.number().positive()
|
|
6388
7071
|
});
|
|
6389
7072
|
function calculateDiscount(price, discountType, discountValue) {
|
|
6390
7073
|
if (discountType === "PERCENTAGE") {
|
|
@@ -6715,6 +7398,13 @@ var ROUTES = [
|
|
|
6715
7398
|
{ method: "GET", path: "/availability", public: true, handler: getAvailability },
|
|
6716
7399
|
{ method: "GET", path: "/availability/team", public: true, handler: getTeamAvailability },
|
|
6717
7400
|
{ method: "POST", path: "/appointments", public: true, handler: bookAppointmentRoute },
|
|
7401
|
+
{ method: "GET", path: "/routing-forms", public: true, handler: getRoutingFormRoute },
|
|
7402
|
+
{ method: "POST", path: "/routing-forms/route", public: true, handler: routeClientRoute },
|
|
7403
|
+
{ method: "GET", path: "/brand", public: true, handler: getBrandRoute },
|
|
7404
|
+
{ method: "POST", path: "/waitlist", public: true, handler: joinWaitlistRoute },
|
|
7405
|
+
{ method: "DELETE", path: "/waitlist", public: true, handler: leaveWaitlistRoute },
|
|
7406
|
+
{ method: "GET", path: "/waitlist/offer", public: true, handler: getWaitlistOfferRoute },
|
|
7407
|
+
{ method: "POST", path: "/waitlist/offer/accept", public: true, handler: acceptWaitlistOfferRoute },
|
|
6718
7408
|
{ method: "GET", path: "/appointments", public: false, handler: listAppointments },
|
|
6719
7409
|
{ method: "GET", path: "/appointments/capacity", public: true, handler: getCapacity },
|
|
6720
7410
|
{ method: "GET", path: "/appointments/ical", public: false, handler: getAppointmentIcal },
|
|
@@ -6756,6 +7446,21 @@ var ROUTES = [
|
|
|
6756
7446
|
{ method: "GET", path: "/admin/webhooks/[id]/deliveries", public: false, handler: listWebhookEndpointDeliveries },
|
|
6757
7447
|
{ method: "POST", path: "/admin/webhook-deliveries/[id]/retry", public: false, handler: retryWebhook },
|
|
6758
7448
|
{ method: "POST", path: "/internal/webhooks/deliver", public: false, handler: deliverWebhooks },
|
|
7449
|
+
{ method: "GET", path: "/admin/routing-forms", public: false, handler: listRoutingFormsRoute },
|
|
7450
|
+
{ method: "POST", path: "/admin/routing-forms", public: false, handler: createRoutingFormRoute },
|
|
7451
|
+
{ method: "GET", path: "/admin/routing-forms/[id]", public: false, handler: getRoutingFormAdminRoute },
|
|
7452
|
+
{ method: "PUT", path: "/admin/routing-forms/[id]", public: false, handler: updateRoutingFormRoute },
|
|
7453
|
+
{ method: "DELETE", path: "/admin/routing-forms/[id]", public: false, handler: deleteRoutingFormRoute },
|
|
7454
|
+
{ method: "POST", path: "/admin/routing-forms/[id]/test", public: false, handler: testRoutingFormRoute },
|
|
7455
|
+
{ method: "GET", path: "/admin/brand", public: false, handler: getBrandAdminRoute },
|
|
7456
|
+
{ method: "PUT", path: "/admin/brand", public: false, handler: saveBrandRoute },
|
|
7457
|
+
{ method: "DELETE", path: "/admin/brand", public: false, handler: deleteBrandRoute },
|
|
7458
|
+
{ method: "POST", path: "/admin/privacy/export", public: false, handler: exportPrivacyRoute },
|
|
7459
|
+
{ method: "POST", path: "/admin/privacy/erase", public: false, handler: erasePrivacyRoute },
|
|
7460
|
+
{ method: "GET", path: "/admin/privacy/requests", public: false, handler: listPrivacyRequestsRoute },
|
|
7461
|
+
{ method: "GET", path: "/admin/waitlist", public: false, handler: listWaitlistEntries },
|
|
7462
|
+
{ method: "DELETE", path: "/admin/waitlist/[id]", public: false, handler: removeWaitlistEntryRoute },
|
|
7463
|
+
{ method: "POST", path: "/internal/waitlist/sweep", public: false, handler: sweepWaitlist },
|
|
6759
7464
|
{ method: "GET", path: "/analytics", public: false, handler: getAnalytics },
|
|
6760
7465
|
{ method: "GET", path: "/admin/types", public: false, handler: listTypes },
|
|
6761
7466
|
{ method: "POST", path: "/admin/types", public: false, handler: createType },
|
|
@@ -6834,6 +7539,11 @@ function createSchedulingApi(config) {
|
|
|
6834
7539
|
tenantDb: config.host.tenantDb?.bind(config.host) ?? (() => config.host.publicDb()),
|
|
6835
7540
|
events: { emit: async () => void 0 },
|
|
6836
7541
|
webhooks: config.host.webhooks ?? null,
|
|
7542
|
+
waitlist: config.host.waitlist ?? null,
|
|
7543
|
+
routingForms: config.host.routingForms ?? null,
|
|
7544
|
+
privacy: config.host.privacy ?? null,
|
|
7545
|
+
brand: config.host.brand ?? null,
|
|
7546
|
+
icalUidDomain: config.host.icalUidDomain?.bind(config.host) ?? (() => null),
|
|
6837
7547
|
calendarSync: config.host.calendarSync ?? { upsertEvent: async () => ({}), removeEvent: async () => void 0 },
|
|
6838
7548
|
activity: config.host.activity ?? { log: async () => void 0, isOpen: async () => false, resolve: async () => void 0 },
|
|
6839
7549
|
appUrl: config.host.appUrl?.bind(config.host) ?? (() => null),
|
|
@@ -6859,7 +7569,7 @@ function createSchedulingApi(config) {
|
|
|
6859
7569
|
jsonNull: config.host.jsonNull?.bind(config.host) ?? (() => null),
|
|
6860
7570
|
cors: config.host.cors?.bind(config.host) ?? defaultCorsHeaders
|
|
6861
7571
|
};
|
|
6862
|
-
host.events = withWebhooks(config.host.events ?? { emit: async () => void 0 }, host);
|
|
7572
|
+
host.events = withWebhooks(withWaitlist(config.host.events ?? { emit: async () => void 0 }, host), host);
|
|
6863
7573
|
async function handle(request) {
|
|
6864
7574
|
const { pathname } = new URL(request.url);
|
|
6865
7575
|
if (pathname !== basePath && !pathname.startsWith(`${basePath}/`)) return notFound();
|
|
@@ -7013,6 +7723,7 @@ export {
|
|
|
7013
7723
|
BalanceRequestInProgressError,
|
|
7014
7724
|
BookingPageConfigSchema,
|
|
7015
7725
|
BookingPageThemeSchema,
|
|
7726
|
+
BrandSchema,
|
|
7016
7727
|
CancelRecurringScopeSchema,
|
|
7017
7728
|
CancelRecurringSeriesSchema,
|
|
7018
7729
|
CreateAppointmentCategorySchema,
|
|
@@ -7023,6 +7734,7 @@ export {
|
|
|
7023
7734
|
CreateNotificationTemplateSchema,
|
|
7024
7735
|
CreatePackagePurchaseSchema,
|
|
7025
7736
|
CreateRecurringSeriesSchema,
|
|
7737
|
+
CreateRoutingFormSchema,
|
|
7026
7738
|
CreateServicePackageSchema,
|
|
7027
7739
|
CreateWebhookEndpointSchema,
|
|
7028
7740
|
DEFAULT_CANCELLATION_POLICY,
|
|
@@ -7031,13 +7743,17 @@ export {
|
|
|
7031
7743
|
NotificationChannelSchema,
|
|
7032
7744
|
NotificationTriggerSchema,
|
|
7033
7745
|
OrgCancellationPolicySchema,
|
|
7746
|
+
PrivacyEraseSchema,
|
|
7747
|
+
PrivacySubjectSchema,
|
|
7034
7748
|
RETRYABLE_REFUND_STATUSES,
|
|
7035
7749
|
RecurringFrequencySchema,
|
|
7036
7750
|
RefundOutcomePersistenceError,
|
|
7751
|
+
RouteClientSchema,
|
|
7037
7752
|
SLOT_GRANULARITY_DEFAULT,
|
|
7038
7753
|
SLOT_GRANULARITY_OPTIONS,
|
|
7039
7754
|
TERMINAL_SUCCESS_STATUSES,
|
|
7040
7755
|
TOKEN_TTL_DAYS,
|
|
7756
|
+
TestRoutingFormSchema,
|
|
7041
7757
|
UnverifiedPaymentError,
|
|
7042
7758
|
UpdateAppointmentCategorySchema,
|
|
7043
7759
|
UpdateAppointmentTypeSchema,
|
|
@@ -7046,6 +7762,7 @@ export {
|
|
|
7046
7762
|
UpdateCouponCodeSchema,
|
|
7047
7763
|
UpdateNotificationTemplateSchema,
|
|
7048
7764
|
UpdatePackagePurchaseSchema,
|
|
7765
|
+
UpdateRoutingFormSchema,
|
|
7049
7766
|
UpdateServicePackageSchema,
|
|
7050
7767
|
attemptLegRefund,
|
|
7051
7768
|
createBalanceIntent,
|