@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/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-IAQBLF5F.mjs";
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
- "Client notices",
3436
- () => scheduleNotifications4(db, visit.id, baseUrl, { excludeTriggers: ["CONFIRMATION"] })
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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.appName() || "Your provider";
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 ${appointment.calendar.staffName || host.appName() || "your provider"} is on ${dateTime}.`,
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(appointment.calendar.staffName || host.appName() || "Your provider")}`,
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 SchedulingValidationError4, setWeeklyAvailability } from "@ordinatio/scheduling";
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 SchedulingValidationError4) {
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 SchedulingValidationError5
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 SchedulingValidationError5) {
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 SchedulingValidationError6, updateNotificationTemplate } from "@ordinatio/scheduling";
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 SchedulingValidationError6) {
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 SchedulingValidationError7, updateAppointmentType } from "@ordinatio/scheduling";
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 SchedulingValidationError7) return Response.json({ error: message }, { status: 400 });
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}@system1701`,
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 SchedulingValidationError8 } from "@ordinatio/scheduling";
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 SchedulingValidationError8) {
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/appointments-book.ts
5512
- import { bookAppointment } from "@ordinatio/scheduling";
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
- var BookingSchema = z7.object({
5517
- calendarId: z7.string().min(1).optional(),
5518
- preferredCalendarId: z7.string().min(1).optional(),
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
- startTime: z7.string().datetime({ message: "Invalid date/time format" }),
5521
- timezone: z7.string().max(50).optional(),
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: z7.string().optional(),
5527
- notes: z7.string().max(1e3).optional(),
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: z7.record(
5530
- z7.string(),
5531
- z7.union([z7.string().max(1e3), z7.number(), z7.boolean(), z7.null()])
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: z7.string().optional(),
5534
- couponCode: z7.string().min(1).max(50).transform((value) => value.trim().toUpperCase()).optional(),
5535
- packagePurchaseId: z7.string().optional(),
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: z7.string().min(1).max(100).optional()
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 z8 } from "zod";
7065
+ import { z as z10 } from "zod";
6383
7066
  import { schedError as schedError30 } from "@ordinatio/scheduling";
6384
- var Schema4 = z8.object({
6385
- code: z8.string().min(1).max(50).transform((v) => v.trim().toUpperCase()),
6386
- calendarId: z8.string().min(1),
6387
- price: z8.number().positive()
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,