@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.js CHANGED
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  BalanceRequestInProgressError: () => BalanceRequestInProgressError,
40
40
  BookingPageConfigSchema: () => BookingPageConfigSchema,
41
41
  BookingPageThemeSchema: () => BookingPageThemeSchema,
42
+ BrandSchema: () => BrandSchema,
42
43
  CancelRecurringScopeSchema: () => CancelRecurringScopeSchema,
43
44
  CancelRecurringSeriesSchema: () => CancelRecurringSeriesSchema,
44
45
  CreateAppointmentCategorySchema: () => CreateAppointmentCategorySchema,
@@ -49,6 +50,7 @@ __export(index_exports, {
49
50
  CreateNotificationTemplateSchema: () => CreateNotificationTemplateSchema,
50
51
  CreatePackagePurchaseSchema: () => CreatePackagePurchaseSchema,
51
52
  CreateRecurringSeriesSchema: () => CreateRecurringSeriesSchema,
53
+ CreateRoutingFormSchema: () => CreateRoutingFormSchema,
52
54
  CreateServicePackageSchema: () => CreateServicePackageSchema,
53
55
  CreateWebhookEndpointSchema: () => CreateWebhookEndpointSchema,
54
56
  DEFAULT_CANCELLATION_POLICY: () => DEFAULT_CANCELLATION_POLICY,
@@ -57,13 +59,17 @@ __export(index_exports, {
57
59
  NotificationChannelSchema: () => NotificationChannelSchema,
58
60
  NotificationTriggerSchema: () => NotificationTriggerSchema,
59
61
  OrgCancellationPolicySchema: () => OrgCancellationPolicySchema,
62
+ PrivacyEraseSchema: () => PrivacyEraseSchema,
63
+ PrivacySubjectSchema: () => PrivacySubjectSchema,
60
64
  RETRYABLE_REFUND_STATUSES: () => RETRYABLE_REFUND_STATUSES,
61
65
  RecurringFrequencySchema: () => RecurringFrequencySchema,
62
66
  RefundOutcomePersistenceError: () => RefundOutcomePersistenceError,
67
+ RouteClientSchema: () => RouteClientSchema,
63
68
  SLOT_GRANULARITY_DEFAULT: () => SLOT_GRANULARITY_DEFAULT,
64
69
  SLOT_GRANULARITY_OPTIONS: () => SLOT_GRANULARITY_OPTIONS,
65
70
  TERMINAL_SUCCESS_STATUSES: () => TERMINAL_SUCCESS_STATUSES,
66
71
  TOKEN_TTL_DAYS: () => TOKEN_TTL_DAYS,
72
+ TestRoutingFormSchema: () => TestRoutingFormSchema,
67
73
  UnverifiedPaymentError: () => UnverifiedPaymentError,
68
74
  UpdateAppointmentCategorySchema: () => UpdateAppointmentCategorySchema,
69
75
  UpdateAppointmentTypeSchema: () => UpdateAppointmentTypeSchema,
@@ -72,6 +78,7 @@ __export(index_exports, {
72
78
  UpdateCouponCodeSchema: () => UpdateCouponCodeSchema,
73
79
  UpdateNotificationTemplateSchema: () => UpdateNotificationTemplateSchema,
74
80
  UpdatePackagePurchaseSchema: () => UpdatePackagePurchaseSchema,
81
+ UpdateRoutingFormSchema: () => UpdateRoutingFormSchema,
75
82
  UpdateServicePackageSchema: () => UpdateServicePackageSchema,
76
83
  attemptLegRefund: () => attemptLegRefund,
77
84
  createBalanceIntent: () => createBalanceIntent,
@@ -322,8 +329,87 @@ function withWebhooks(inner, host) {
322
329
  };
323
330
  }
324
331
 
332
+ // src/waitlist/announce-offer.ts
333
+ var import_scheduling2 = require("@ordinatio/scheduling");
334
+ async function offerAndAnnounce(host, settings, db, slot) {
335
+ const offer = await (0, import_scheduling2.tryOfferFreedSlot)(db, { ...slot, offerMinutes: settings.offerMinutes });
336
+ if (!offer) return false;
337
+ const details = {
338
+ organizationId: slot.organizationId,
339
+ token: offer.token,
340
+ clientName: offer.entry.clientName,
341
+ clientEmail: offer.entry.clientEmail,
342
+ clientPhone: offer.entry.clientPhone,
343
+ typeName: offer.typeName,
344
+ calendarName: offer.calendarName,
345
+ startTime: offer.startTime,
346
+ timezone: offer.timezone,
347
+ expiresAt: offer.expiresAt
348
+ };
349
+ if (settings.notify) {
350
+ try {
351
+ await settings.notify(details);
352
+ } catch (error) {
353
+ console.error("[waitlist] the host could not send the offer; it stays open until it expires:", error);
354
+ }
355
+ }
356
+ if (host.webhooks) {
357
+ try {
358
+ await (0, import_scheduling2.enqueueWebhookEvent)(db, {
359
+ organizationId: slot.organizationId,
360
+ type: "waitlist.offered",
361
+ // Each offering has its own id, so offering the same slot to the same person again still reaches subscribers.
362
+ eventId: `waitlist_offer_${offer.entry.id}_${offer.startTime.getTime()}_${offer.expiresAt.getTime()}`,
363
+ data: {
364
+ offer: {
365
+ entryId: offer.entry.id,
366
+ token: offer.token,
367
+ clientName: details.clientName,
368
+ clientEmail: details.clientEmail,
369
+ clientPhone: details.clientPhone,
370
+ typeName: details.typeName,
371
+ calendarName: details.calendarName,
372
+ calendarId: offer.calendarId,
373
+ startTime: offer.startTime.toISOString(),
374
+ expiresAt: offer.expiresAt.toISOString()
375
+ }
376
+ }
377
+ });
378
+ } catch (error) {
379
+ console.error("[waitlist] could not queue the waitlist.offered webhook:", error);
380
+ }
381
+ }
382
+ return true;
383
+ }
384
+
385
+ // src/waitlist/waitlist-events.ts
386
+ var FREED_BY = /* @__PURE__ */ new Set(["appointment.cancelled", "appointment.rejected", "appointment.updated"]);
387
+ function withWaitlist(inner, host) {
388
+ return {
389
+ async emit(organizationId, type, data) {
390
+ const appointmentId = data.appointmentId;
391
+ if (host.waitlist && FREED_BY.has(type) && typeof appointmentId === "string") {
392
+ try {
393
+ const db = await host.tenantDb(organizationId, "waitlist");
394
+ const appointment = db ? await db.appointment.findUnique({
395
+ where: { id: appointmentId },
396
+ select: { calendarId: true, startTime: true, rescheduledFrom: true, status: true, calendar: { select: { organizationId: true } } }
397
+ }) : null;
398
+ if (db && appointment && (appointment.calendar.organizationId ?? organizationId) === organizationId) {
399
+ const freed = type === "appointment.updated" ? appointment.rescheduledFrom : appointment.status === "CANCELLED" ? appointment.startTime : null;
400
+ if (freed) await offerAndAnnounce(host, host.waitlist, db, { organizationId, calendarId: appointment.calendarId, startTime: freed });
401
+ }
402
+ } catch (error) {
403
+ console.error("[scheduling-api] could not offer a freed slot to the waitlist:", error);
404
+ }
405
+ }
406
+ await inner.emit(organizationId, type, data);
407
+ }
408
+ };
409
+ }
410
+
325
411
  // src/handlers/manage-cancel.ts
326
- var import_scheduling5 = require("@ordinatio/scheduling");
412
+ var import_scheduling6 = require("@ordinatio/scheduling");
327
413
 
328
414
  // src/client-address.ts
329
415
  function forwardedAddress(request) {
@@ -334,7 +420,7 @@ function clientAddress(request) {
334
420
  }
335
421
 
336
422
  // src/error-guards.ts
337
- var import_scheduling2 = require("@ordinatio/scheduling");
423
+ var import_scheduling3 = require("@ordinatio/scheduling");
338
424
  function getErrorCode(error) {
339
425
  if (typeof error === "object" && error !== null && "code" in error) {
340
426
  const code = error.code;
@@ -361,20 +447,20 @@ function getErrorMessage(error, fallback) {
361
447
  return fallback;
362
448
  }
363
449
  function schedErrorStatus(code) {
364
- return import_scheduling2.SCHEDULING_ERRORS[code]?.httpStatus ?? 400;
450
+ return import_scheduling3.SCHEDULING_ERRORS[code]?.httpStatus ?? 400;
365
451
  }
366
452
 
367
453
  // src/manage/cancel-via-token.ts
368
- var import_scheduling4 = require("@ordinatio/scheduling");
454
+ var import_scheduling5 = require("@ordinatio/scheduling");
369
455
 
370
456
  // src/calendar-sync.ts
371
- var import_scheduling3 = require("@ordinatio/scheduling");
457
+ var import_scheduling4 = require("@ordinatio/scheduling");
372
458
  async function runWithoutBlocking(code, appointmentId, step, recordFailure) {
373
459
  try {
374
460
  await step();
375
461
  return { queued: true };
376
462
  } catch (error) {
377
- const diagnostic = (0, import_scheduling3.schedError)(code, { error, appointmentId });
463
+ const diagnostic = (0, import_scheduling4.schedError)(code, { error, appointmentId });
378
464
  console.error("[calendar-sync]", diagnostic.ref, error instanceof Error ? error.message : error);
379
465
  try {
380
466
  await recordFailure?.(diagnostic.ref);
@@ -394,7 +480,7 @@ function runCalendarRemoveWithoutBlocking(sync, ref, recordFailure) {
394
480
  // src/manage/cancel-via-token.ts
395
481
  async function cancelViaToken(host, db, organizationId, appointment) {
396
482
  const hasPayment = !!(appointment.paymentId && (appointment.paymentStatus === "PAID" || appointment.paymentStatus === "DEPOSIT_PAID"));
397
- await (0, import_scheduling4.cancelAppointment)(db, {
483
+ await (0, import_scheduling5.cancelAppointment)(db, {
398
484
  appointmentId: appointment.id,
399
485
  cancelledBy: "client",
400
486
  reason: "Cancelled via self-service link"
@@ -405,7 +491,7 @@ async function cancelViaToken(host, db, organizationId, appointment) {
405
491
  data: { refundStatus: hasPayment ? "PENDING_REVIEW" : "NO_PAYMENT" }
406
492
  });
407
493
  } catch (refundStatusError) {
408
- const diagnostic = (0, import_scheduling4.schedError)("SCHED_602", {
494
+ const diagnostic = (0, import_scheduling5.schedError)("SCHED_602", {
409
495
  error: refundStatusError,
410
496
  appointmentId: appointment.id,
411
497
  context: "refundStatus write failed after cancellation already committed",
@@ -450,9 +536,9 @@ async function cancelViaToken(host, db, organizationId, appointment) {
450
536
  const baseUrl = host.appUrl();
451
537
  if (baseUrl) {
452
538
  try {
453
- await (0, import_scheduling4.queueStaffNotification)(db, appointment.id, "CANCELLATION", baseUrl);
539
+ await (0, import_scheduling5.queueStaffNotification)(db, appointment.id, "CANCELLATION", baseUrl, { brand: Boolean(host.brand) });
454
540
  } catch (staffError) {
455
- const diagnostic = (0, import_scheduling4.schedError)("SCHED_607", { error: staffError, appointmentId: appointment.id });
541
+ const diagnostic = (0, import_scheduling5.schedError)("SCHED_607", { error: staffError, appointmentId: appointment.id });
456
542
  console.error("[DELETE /manage]", diagnostic.ref, staffError instanceof Error ? staffError.message : staffError);
457
543
  }
458
544
  }
@@ -526,14 +612,14 @@ async function cancelManagedAppointment(request, { host }) {
526
612
  { status: 409 }
527
613
  );
528
614
  }
529
- const diagnostic = (0, import_scheduling5.schedError)("SCHED_602", { error, token: token ?? "unknown", appointmentId });
615
+ const diagnostic = (0, import_scheduling6.schedError)("SCHED_602", { error, token: token ?? "unknown", appointmentId });
530
616
  console.error("[DELETE /manage]", diagnostic.ref, error instanceof Error ? error.message : error);
531
617
  return Response.json({ error: "Failed to cancel", ref: diagnostic.ref }, { status: 500 });
532
618
  }
533
619
  }
534
620
 
535
621
  // src/handlers/manage-get.ts
536
- var import_scheduling6 = require("@ordinatio/scheduling");
622
+ var import_scheduling7 = require("@ordinatio/scheduling");
537
623
  async function getManagedAppointment(request, { host }) {
538
624
  let token = null;
539
625
  try {
@@ -567,24 +653,24 @@ async function getManagedAppointment(request, { host }) {
567
653
  staffName: appointment.calendar.staffName,
568
654
  calendarId: appointment.calendarId,
569
655
  allowReschedule: appointment.type.allowReschedule,
570
- location: (0, import_scheduling6.resolveLocation)(appointment),
656
+ location: (0, import_scheduling7.resolveLocation)(appointment),
571
657
  meetingUrl: appointment.meetingUrl ?? null,
572
658
  theme: appointment.calendar.theme ?? null
573
659
  });
574
660
  } catch (error) {
575
- const diagnostic = (0, import_scheduling6.schedError)("SCHED_601", { error, token: token ?? "unknown" });
661
+ const diagnostic = (0, import_scheduling7.schedError)("SCHED_601", { error, token: token ?? "unknown" });
576
662
  console.error("[GET /manage]", diagnostic.ref, error instanceof Error ? error.message : error);
577
663
  return Response.json({ error: "Failed to fetch appointment", ref: diagnostic.ref }, { status: 500 });
578
664
  }
579
665
  }
580
666
 
581
667
  // src/handlers/manage-reschedule.ts
582
- var import_scheduling8 = require("@ordinatio/scheduling");
668
+ var import_scheduling9 = require("@ordinatio/scheduling");
583
669
 
584
670
  // src/manage/reschedule-via-token.ts
585
- var import_scheduling7 = require("@ordinatio/scheduling");
671
+ var import_scheduling8 = require("@ordinatio/scheduling");
586
672
  async function rescheduleViaToken(host, db, organizationId, appointment, newTime) {
587
- const result = await (0, import_scheduling7.rescheduleAppointment)(db, {
673
+ const result = await (0, import_scheduling8.rescheduleAppointment)(db, {
588
674
  appointmentId: appointment.id,
589
675
  newStartTime: newTime,
590
676
  reason: "Rescheduled via self-service link"
@@ -609,21 +695,21 @@ async function rescheduleViaToken(host, db, organizationId, appointment, newTime
609
695
  );
610
696
  const baseUrl = host.appUrl() ?? void 0;
611
697
  if (baseUrl) {
612
- await (0, import_scheduling7.queueImmediateNotification)(db, appointment.id, "RESCHEDULE", baseUrl);
698
+ await (0, import_scheduling8.queueImmediateNotification)(db, appointment.id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
613
699
  } else {
614
700
  console.warn("[manage] no appUrl \u2014 RESCHEDULE notification skipped");
615
701
  }
616
702
  try {
617
- await (0, import_scheduling7.scheduleNotifications)(db, appointment.id, baseUrl, { excludeTriggers: ["CONFIRMATION"] });
703
+ await (0, import_scheduling8.scheduleNotifications)(db, appointment.id, baseUrl, { excludeTriggers: ["CONFIRMATION"], brand: Boolean(host.brand) });
618
704
  } catch (error) {
619
- const diagnostic = (0, import_scheduling7.schedError)("SCHED_604", { error, appointmentId: appointment.id });
705
+ const diagnostic = (0, import_scheduling8.schedError)("SCHED_604", { error, appointmentId: appointment.id });
620
706
  console.error("[PATCH /manage] scheduleNotifications failed (non-blocking):", diagnostic.ref, error instanceof Error ? error.message : error);
621
707
  }
622
708
  if (baseUrl) {
623
709
  try {
624
- await (0, import_scheduling7.queueStaffNotification)(db, appointment.id, "RESCHEDULE", baseUrl);
710
+ await (0, import_scheduling8.queueStaffNotification)(db, appointment.id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
625
711
  } catch (staffError) {
626
- const diagnostic = (0, import_scheduling7.schedError)("SCHED_607", { error: staffError, appointmentId: appointment.id });
712
+ const diagnostic = (0, import_scheduling8.schedError)("SCHED_607", { error: staffError, appointmentId: appointment.id });
627
713
  console.error("[PATCH /manage]", diagnostic.ref, staffError instanceof Error ? staffError.message : staffError);
628
714
  }
629
715
  }
@@ -682,7 +768,7 @@ async function rescheduleManagedAppointment(request, { host }) {
682
768
  if (target instanceof Response) return target;
683
769
  return await rescheduleViaToken(host, target.db, target.organizationId, appointment, newTime);
684
770
  } catch (error) {
685
- if (error instanceof import_scheduling8.SchedulingValidationError) {
771
+ if (error instanceof import_scheduling9.SchedulingValidationError) {
686
772
  return Response.json({ error: error.message, ref: error.ref }, { status: 400 });
687
773
  }
688
774
  const code = getErrorCode(error);
@@ -701,14 +787,14 @@ async function rescheduleManagedAppointment(request, { host }) {
701
787
  console.error("[PATCH /manage]", ref, "SCHED_306", error);
702
788
  return Response.json({ error: "Appointment status prevents rescheduling", ...ref && { ref } }, { status: 400 });
703
789
  }
704
- const diagnostic = (0, import_scheduling8.schedError)("SCHED_603", { error, token: token ?? "unknown", appointmentId });
790
+ const diagnostic = (0, import_scheduling9.schedError)("SCHED_603", { error, token: token ?? "unknown", appointmentId });
705
791
  console.error("[PATCH /manage]", diagnostic.ref, error instanceof Error ? error.message : error);
706
792
  return Response.json({ error: "Failed to reschedule", ref: diagnostic.ref }, { status: 500 });
707
793
  }
708
794
  }
709
795
 
710
796
  // src/handlers/capacity.ts
711
- var import_scheduling9 = require("@ordinatio/scheduling");
797
+ var import_scheduling10 = require("@ordinatio/scheduling");
712
798
 
713
799
  // src/respond.ts
714
800
  var WIDGET_CACHE_HEADERS = {
@@ -755,7 +841,7 @@ async function getCapacity(request, { host }) {
755
841
  select: { isGroup: true }
756
842
  });
757
843
  if (!type || !type.isGroup) return corsJson(cors, NOT_FOUND, { status: 404 });
758
- return corsJson(cors, await (0, import_scheduling9.getGroupCapacity)(db, { typeId, calendarId, startTime }));
844
+ return corsJson(cors, await (0, import_scheduling10.getGroupCapacity)(db, { typeId, calendarId, startTime }));
759
845
  } catch (error) {
760
846
  console.error("[group-capacity] unexpected error:", error instanceof Error ? error.message : error);
761
847
  return corsJson(cors, { error: "Failed to fetch capacity" }, { status: 500 });
@@ -763,7 +849,7 @@ async function getCapacity(request, { host }) {
763
849
  }
764
850
 
765
851
  // src/handlers/payment-intent.ts
766
- var import_scheduling10 = require("@ordinatio/scheduling");
852
+ var import_scheduling11 = require("@ordinatio/scheduling");
767
853
  var import_zod = require("zod");
768
854
 
769
855
  // src/payments/create-payment-intent.ts
@@ -879,7 +965,7 @@ async function createPaymentIntentRoute(request, { host }) {
879
965
  if (!result.ok) return corsJson(cors, { error: result.error }, { status: result.status });
880
966
  return corsJson(cors, result.body, { status: result.status });
881
967
  } catch (error) {
882
- const err = (0, import_scheduling10.schedError)("SCHED_407", {
968
+ const err = (0, import_scheduling11.schedError)("SCHED_407", {
883
969
  typeId,
884
970
  calendarId,
885
971
  error: error instanceof Error ? error.message : String(error)
@@ -890,7 +976,7 @@ async function createPaymentIntentRoute(request, { host }) {
890
976
  }
891
977
 
892
978
  // src/handlers/appointment-cancel.ts
893
- var import_scheduling12 = require("@ordinatio/scheduling");
979
+ var import_scheduling13 = require("@ordinatio/scheduling");
894
980
 
895
981
  // src/appointments/owned-appointment.ts
896
982
  async function verifyAppointmentOwnership(db, appointmentId, organizationId) {
@@ -943,7 +1029,7 @@ async function writeRefundStatusAfterCancel(host, db, input) {
943
1029
  }
944
1030
 
945
1031
  // src/staff/require-staff.ts
946
- var import_scheduling11 = require("@ordinatio/scheduling");
1032
+ var import_scheduling12 = require("@ordinatio/scheduling");
947
1033
  async function requireAdminStaff(host, request, options) {
948
1034
  const staff = await host.staff.resolve(request);
949
1035
  if (!staff) return Response.json({ error: "Unauthorized" }, { status: 401 });
@@ -952,7 +1038,7 @@ async function requireAdminStaff(host, request, options) {
952
1038
  if (limited) return limited;
953
1039
  }
954
1040
  if (!staff.isAdmin && !options.allowAnyRole) {
955
- const err = (0, import_scheduling11.schedError)("SCHED_900", { userId: staff.userId, role: staff.role });
1041
+ const err = (0, import_scheduling12.schedError)("SCHED_900", { userId: staff.userId, role: staff.role });
956
1042
  return Response.json({ error: "Forbidden", ref: err.ref }, { status: 403 });
957
1043
  }
958
1044
  if (!staff.schedulingEnabled && !options.ignoreSchedulingSwitch) {
@@ -988,7 +1074,7 @@ async function cancelByStaff(request, { host, params }) {
988
1074
  const appointment = await verifyAppointmentOwnership(db, id, organizationId);
989
1075
  if (!appointment) return Response.json({ error: "Appointment not found" }, { status: 404 });
990
1076
  const hasPayment = hasPendingPayment(appointment.paymentId, appointment.paymentStatus);
991
- const result = await (0, import_scheduling12.cancelAppointment)(db, { appointmentId: id, cancelledBy: "staff", reason });
1077
+ const result = await (0, import_scheduling13.cancelAppointment)(db, { appointmentId: id, cancelledBy: "staff", reason });
992
1078
  await writeRefundStatusAfterCancel(host, db, { appointmentId: id, hasPayment, clientName: appointment.clientName });
993
1079
  if (hasPayment) {
994
1080
  await host.activity.log(db, {
@@ -1008,7 +1094,7 @@ async function cancelByStaff(request, { host, params }) {
1008
1094
  const baseUrl = host.appUrl();
1009
1095
  if (baseUrl) {
1010
1096
  try {
1011
- await (0, import_scheduling12.queueStaffNotification)(db, id, "CANCELLATION", baseUrl);
1097
+ await (0, import_scheduling13.queueStaffNotification)(db, id, "CANCELLATION", baseUrl, { brand: Boolean(host.brand) });
1012
1098
  } catch (error) {
1013
1099
  await host.activity.log(db, {
1014
1100
  action: "scheduling.staff_notification_failed",
@@ -1035,7 +1121,7 @@ async function cancelByStaff(request, { host, params }) {
1035
1121
  });
1036
1122
  return Response.json(result);
1037
1123
  } catch (error) {
1038
- if (error instanceof import_scheduling12.SchedulingNotFoundError) {
1124
+ if (error instanceof import_scheduling13.SchedulingNotFoundError) {
1039
1125
  return Response.json({ error: error.message, ref: error.ref }, { status: 404 });
1040
1126
  }
1041
1127
  const ref = getErrorRef(error);
@@ -1240,7 +1326,7 @@ function finishOutlookConnection(request, { host }) {
1240
1326
  }
1241
1327
 
1242
1328
  // src/handlers/appointment-cancel-sync.ts
1243
- var import_scheduling13 = require("@ordinatio/scheduling");
1329
+ var import_scheduling14 = require("@ordinatio/scheduling");
1244
1330
  var import_zod2 = require("zod");
1245
1331
  var Schema2 = import_zod2.z.object({
1246
1332
  source: import_zod2.z.enum(["google", "outlook"]),
@@ -1283,13 +1369,13 @@ async function cancelFromCalendarSync(request, { host, params }) {
1283
1369
  return Response.json({ error: "Appointment not available for sync" }, { status: 503 });
1284
1370
  }
1285
1371
  try {
1286
- await (0, import_scheduling13.cancelAppointment)(db, {
1372
+ await (0, import_scheduling14.cancelAppointment)(db, {
1287
1373
  appointmentId: id,
1288
1374
  cancelledBy: "system",
1289
1375
  reason: reason ?? `Cancelled via ${source === "google" ? "Google" : "Outlook"} Calendar`
1290
1376
  });
1291
1377
  } catch (error) {
1292
- if (error instanceof import_scheduling13.SchedulingNotFoundError) {
1378
+ if (error instanceof import_scheduling14.SchedulingNotFoundError) {
1293
1379
  return Response.json({ error: "Appointment not found", ref: error.ref }, { status: 404 });
1294
1380
  }
1295
1381
  const ref = getErrorRef(error);
@@ -1323,7 +1409,7 @@ async function cancelFromCalendarSync(request, { host, params }) {
1323
1409
  await host.workerCommands.queue(source === "google" ? "delete-from-outlook" : "delete-from-google", { appointmentId: id });
1324
1410
  const baseUrl = host.appUrl();
1325
1411
  if (baseUrl) {
1326
- await (0, import_scheduling13.queueImmediateNotification)(db, id, "CANCELLATION", baseUrl).catch(
1412
+ await (0, import_scheduling14.queueImmediateNotification)(db, id, "CANCELLATION", baseUrl, { brand: Boolean(host.brand) }).catch(
1327
1413
  (err) => console.warn("[cancel-sync] Failed to queue cancellation notification:", id, err instanceof Error ? err.message : err)
1328
1414
  );
1329
1415
  }
@@ -1336,10 +1422,10 @@ async function cancelFromCalendarSync(request, { host, params }) {
1336
1422
  }
1337
1423
 
1338
1424
  // src/handlers/appointment-approve.ts
1339
- var import_scheduling14 = require("@ordinatio/scheduling");
1425
+ var import_scheduling15 = require("@ordinatio/scheduling");
1340
1426
  async function rejectAppointment(host, db, organizationId, appointment, reason) {
1341
1427
  const { id } = appointment;
1342
- const result = await (0, import_scheduling14.cancelAppointment)(db, {
1428
+ const result = await (0, import_scheduling15.cancelAppointment)(db, {
1343
1429
  appointmentId: id,
1344
1430
  cancelledBy: "staff",
1345
1431
  reason: reason || "Appointment request rejected"
@@ -1421,10 +1507,10 @@ async function approveOrReject(request, { host, params }) {
1421
1507
  if (confirmed.count === 0) {
1422
1508
  return Response.json({ error: "Appointment was already processed by a concurrent request" }, { status: 409 });
1423
1509
  }
1424
- await (0, import_scheduling14.cancelPendingNotifications)(db, id);
1510
+ await (0, import_scheduling15.cancelPendingNotifications)(db, id);
1425
1511
  const baseUrl = host.appUrl();
1426
1512
  if (!baseUrl) throw new Error("NEXT_PUBLIC_APP_URL is required");
1427
- await (0, import_scheduling14.scheduleNotifications)(db, id, baseUrl);
1513
+ await (0, import_scheduling15.scheduleNotifications)(db, id, baseUrl, { brand: Boolean(host.brand) });
1428
1514
  await host.availabilityCache.invalidate(appointment.calendarId);
1429
1515
  await host.events.emit(organizationId, "appointment.confirmed", { appointmentId: id });
1430
1516
  await host.activity.log(db, {
@@ -1432,7 +1518,7 @@ async function approveOrReject(request, { host, params }) {
1432
1518
  description: `approved ${appointment.clientName}'s appointment`,
1433
1519
  metadata: { appointmentId: id, clientName: appointment.clientName }
1434
1520
  });
1435
- const confirmedAppointment = await (0, import_scheduling14.getAppointment)(db, id);
1521
+ const confirmedAppointment = await (0, import_scheduling15.getAppointment)(db, id);
1436
1522
  return Response.json({ success: true, status: "CONFIRMED", appointment: confirmedAppointment });
1437
1523
  } catch (error) {
1438
1524
  if (getErrorCode(error) === "SCHED_306") {
@@ -1445,7 +1531,7 @@ async function approveOrReject(request, { host, params }) {
1445
1531
  }
1446
1532
 
1447
1533
  // src/handlers/appointment-reschedule.ts
1448
- var import_scheduling15 = require("@ordinatio/scheduling");
1534
+ var import_scheduling16 = require("@ordinatio/scheduling");
1449
1535
  var RESCHEDULE_BLOCKER_CODES = ["SCHED_301", "SCHED_307", "SCHED_309", "SCHED_802"];
1450
1536
  async function rescheduleByStaff(request, { host, params }) {
1451
1537
  try {
@@ -1473,21 +1559,21 @@ async function rescheduleByStaff(request, { host, params }) {
1473
1559
  if (newTime < /* @__PURE__ */ new Date()) {
1474
1560
  return Response.json({ error: "Cannot reschedule to a past time" }, { status: 400 });
1475
1561
  }
1476
- const result = await (0, import_scheduling15.rescheduleAppointment)(db, {
1562
+ const result = await (0, import_scheduling16.rescheduleAppointment)(db, {
1477
1563
  appointmentId: id,
1478
1564
  newStartTime: newTime,
1479
1565
  reason: "Rescheduled via calendar drag"
1480
1566
  });
1481
1567
  const baseUrl = host.appUrl();
1482
1568
  if (!baseUrl) throw new Error("NEXT_PUBLIC_APP_URL is required");
1483
- await (0, import_scheduling15.queueImmediateNotification)(db, id, "RESCHEDULE", baseUrl);
1569
+ await (0, import_scheduling16.queueImmediateNotification)(db, id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
1484
1570
  try {
1485
- await (0, import_scheduling15.scheduleNotifications)(db, id, baseUrl, { excludeTriggers: ["CONFIRMATION"] });
1571
+ await (0, import_scheduling16.scheduleNotifications)(db, id, baseUrl, { excludeTriggers: ["CONFIRMATION"], brand: Boolean(host.brand) });
1486
1572
  } catch (error) {
1487
1573
  console.warn("[reschedule] scheduleNotifications failed:", error instanceof Error ? error.message : error);
1488
1574
  }
1489
1575
  try {
1490
- await (0, import_scheduling15.queueStaffNotification)(db, id, "RESCHEDULE", baseUrl);
1576
+ await (0, import_scheduling16.queueStaffNotification)(db, id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
1491
1577
  } catch (error) {
1492
1578
  await host.activity.log(db, {
1493
1579
  action: "scheduling.staff_notification_failed",
@@ -1525,8 +1611,8 @@ async function rescheduleByStaff(request, { host, params }) {
1525
1611
  });
1526
1612
  return Response.json(result);
1527
1613
  } catch (error) {
1528
- if (error instanceof import_scheduling15.SchedulingValidationError) return Response.json({ error: error.message }, { status: 400 });
1529
- if (error instanceof import_scheduling15.SchedulingNotFoundError) return Response.json({ error: error.message }, { status: 404 });
1614
+ if (error instanceof import_scheduling16.SchedulingValidationError) return Response.json({ error: error.message }, { status: 400 });
1615
+ if (error instanceof import_scheduling16.SchedulingNotFoundError) return Response.json({ error: error.message }, { status: 404 });
1530
1616
  const errorCode = getErrorCode(error) ?? "";
1531
1617
  const status = errorCode === "SCHED_306" ? 400 : RESCHEDULE_BLOCKER_CODES.includes(errorCode) ? 409 : 500;
1532
1618
  return Response.json({ error: getErrorMessage(error, "Failed to reschedule") }, { status });
@@ -1534,7 +1620,7 @@ async function rescheduleByStaff(request, { host, params }) {
1534
1620
  }
1535
1621
 
1536
1622
  // src/handlers/appointment-reassign.ts
1537
- var import_scheduling16 = require("@ordinatio/scheduling");
1623
+ var import_scheduling17 = require("@ordinatio/scheduling");
1538
1624
  var import_zod3 = require("zod");
1539
1625
  var REASSIGNABLE_STATUSES = ["PENDING", "CONFIRMED"];
1540
1626
  var REASSIGN_MAX_ATTEMPTS = 2;
@@ -1585,7 +1671,7 @@ async function reassignAppointment(request, { host, params }) {
1585
1671
  if (!current || !REASSIGNABLE_STATUSES.includes(current.status)) {
1586
1672
  return { outcome: "statusChanged", currentStatus: current?.status ?? "unknown" };
1587
1673
  }
1588
- const available = await (0, import_scheduling16.isSlotAvailable)(txDb, calendarId, current.startTime, current.duration, id);
1674
+ const available = await (0, import_scheduling17.isSlotAvailable)(txDb, calendarId, current.startTime, current.duration, id);
1589
1675
  if (!available) return { outcome: "conflict" };
1590
1676
  const updated2 = await tx.appointment.update({
1591
1677
  where: { id, status: { in: [...REASSIGNABLE_STATUSES] } },
@@ -1611,14 +1697,14 @@ async function reassignAppointment(request, { host, params }) {
1611
1697
  };
1612
1698
  const result = await attemptReassign();
1613
1699
  if (result.outcome === "statusChanged") {
1614
- const diagnostic = (0, import_scheduling16.schedError)("SCHED_309", { appointmentId: id, calendarId, currentStatus: result.currentStatus });
1700
+ const diagnostic = (0, import_scheduling17.schedError)("SCHED_309", { appointmentId: id, calendarId, currentStatus: result.currentStatus });
1615
1701
  return Response.json(
1616
1702
  { error: "Appointment status changed before the reassignment could commit", ref: diagnostic.ref },
1617
1703
  { status: 409 }
1618
1704
  );
1619
1705
  }
1620
1706
  if (result.outcome === "conflict") {
1621
- const diagnostic = (0, import_scheduling16.schedError)("SCHED_301", { appointmentId: id, calendarId });
1707
+ const diagnostic = (0, import_scheduling17.schedError)("SCHED_301", { appointmentId: id, calendarId });
1622
1708
  return Response.json(
1623
1709
  { error: "Target calendar is not available at this appointment's time", ref: diagnostic.ref },
1624
1710
  { status: 409 }
@@ -1781,7 +1867,7 @@ async function listNotifications(request, { host, params }) {
1781
1867
  }
1782
1868
 
1783
1869
  // src/handlers/appointment-no-show.ts
1784
- var import_scheduling17 = require("@ordinatio/scheduling");
1870
+ var import_scheduling18 = require("@ordinatio/scheduling");
1785
1871
  async function markAppointmentNoShow(request, { host, params }) {
1786
1872
  try {
1787
1873
  const staff = await requireAdminStaff(host, request, {
@@ -1798,7 +1884,7 @@ async function markAppointmentNoShow(request, { host, params }) {
1798
1884
  }
1799
1885
  const appointment = await verifyAppointmentOwnership(db, id, organizationId);
1800
1886
  if (!appointment) return Response.json({ error: "Appointment not found" }, { status: 404 });
1801
- const result = await (0, import_scheduling17.markNoShow)(db, { appointmentId: id });
1887
+ const result = await (0, import_scheduling18.markNoShow)(db, { appointmentId: id });
1802
1888
  await host.events.emit(organizationId, "appointment.no_show", { appointmentId: id });
1803
1889
  await host.activity.log(db, {
1804
1890
  action: "scheduling.appointment_no_show",
@@ -1807,7 +1893,7 @@ async function markAppointmentNoShow(request, { host, params }) {
1807
1893
  });
1808
1894
  return Response.json(result);
1809
1895
  } catch (error) {
1810
- if (error instanceof import_scheduling17.SchedulingNotFoundError) {
1896
+ if (error instanceof import_scheduling18.SchedulingNotFoundError) {
1811
1897
  return Response.json({ error: error.message, ref: error.ref }, { status: 404 });
1812
1898
  }
1813
1899
  const ref = getErrorRef(error);
@@ -1817,7 +1903,7 @@ async function markAppointmentNoShow(request, { host, params }) {
1817
1903
  }
1818
1904
 
1819
1905
  // src/handlers/appointment-collect-balance.ts
1820
- var import_scheduling20 = require("@ordinatio/scheduling");
1906
+ var import_scheduling21 = require("@ordinatio/scheduling");
1821
1907
 
1822
1908
  // src/payments/balance/eligibility.ts
1823
1909
  var BALANCE_BLOCKED_APPOINTMENT_STATUS = "CANCELLED";
@@ -1913,7 +1999,7 @@ function resolveBalanceEligibility(appointment, organizationId) {
1913
1999
  }
1914
2000
 
1915
2001
  // src/payments/balance/record-intent.ts
1916
- var import_scheduling18 = require("@ordinatio/scheduling");
2002
+ var import_scheduling19 = require("@ordinatio/scheduling");
1917
2003
  var BALANCE_OUTCOME_TRACE = {
1918
2004
  cancelled: {
1919
2005
  action: "scheduling.balance_intent_cancelled",
@@ -1956,7 +2042,7 @@ async function recordBalanceIntent(db, payments, target) {
1956
2042
  data: { balancePaymentId: intentId }
1957
2043
  });
1958
2044
  } catch (writeError) {
1959
- const diagnostic = (0, import_scheduling18.schedError)("SCHED_605", {
2045
+ const diagnostic = (0, import_scheduling19.schedError)("SCHED_605", {
1960
2046
  error: writeError,
1961
2047
  appointmentId,
1962
2048
  paymentIntentId: intentId,
@@ -1978,7 +2064,7 @@ async function recordBalanceIntent(db, payments, target) {
1978
2064
  select: { balancePaymentId: true, status: true, refundStatus: true, paymentStatus: true }
1979
2065
  });
1980
2066
  } catch (rereadError) {
1981
- const diagnostic = (0, import_scheduling18.schedError)("SCHED_605", {
2067
+ const diagnostic = (0, import_scheduling19.schedError)("SCHED_605", {
1982
2068
  error: rereadError,
1983
2069
  appointmentId,
1984
2070
  paymentIntentId: intentId,
@@ -1995,7 +2081,7 @@ async function recordBalanceIntent(db, payments, target) {
1995
2081
  const stillEligible = current !== null && !isBalanceBlockedByStatus(current.status) && !hasBalanceRefundStarted(current.refundStatus);
1996
2082
  if (current?.balancePaymentId === intentId) {
1997
2083
  if (stillEligible) return { ok: true };
1998
- const diagnostic = (0, import_scheduling18.schedError)("SCHED_605", {
2084
+ const diagnostic = (0, import_scheduling19.schedError)("SCHED_605", {
1999
2085
  appointmentId,
2000
2086
  paymentIntentId: intentId,
2001
2087
  status: current.status,
@@ -2037,7 +2123,7 @@ async function recordBalanceIntent(db, payments, target) {
2037
2123
  outcome = "cancel-failed";
2038
2124
  }
2039
2125
  if (outcome !== cancelledOutcome) {
2040
- const diagnostic = (0, import_scheduling18.schedError)("SCHED_605", {
2126
+ const diagnostic = (0, import_scheduling19.schedError)("SCHED_605", {
2041
2127
  error: cancelError,
2042
2128
  appointmentId,
2043
2129
  paymentIntentId: intentId,
@@ -2085,7 +2171,7 @@ async function recordBalanceIntent(db, payments, target) {
2085
2171
  }
2086
2172
 
2087
2173
  // src/payments/balance/create-intent.ts
2088
- var import_scheduling19 = require("@ordinatio/scheduling");
2174
+ var import_scheduling20 = require("@ordinatio/scheduling");
2089
2175
 
2090
2176
  // src/payments/unverified-payment-error.ts
2091
2177
  var UnverifiedPaymentError = class extends Error {
@@ -2131,7 +2217,7 @@ async function createBalanceIntent(payments, input, appointmentId) {
2131
2217
  });
2132
2218
  } catch (error) {
2133
2219
  if (error instanceof UnverifiedPaymentError) {
2134
- const diagnostic2 = (0, import_scheduling19.schedError)("SCHED_605", {
2220
+ const diagnostic2 = (0, import_scheduling20.schedError)("SCHED_605", {
2135
2221
  error,
2136
2222
  appointmentId,
2137
2223
  paymentIntentId: error.paymentId,
@@ -2151,7 +2237,7 @@ async function createBalanceIntent(payments, input, appointmentId) {
2151
2237
  return { paymentId: result.paymentId, clientSecret: result.clientSecret ?? null };
2152
2238
  }
2153
2239
  if (result.code === "IN_PROGRESS") {
2154
- const diagnostic2 = (0, import_scheduling19.schedError)("SCHED_605", {
2240
+ const diagnostic2 = (0, import_scheduling20.schedError)("SCHED_605", {
2155
2241
  error: result.error,
2156
2242
  appointmentId,
2157
2243
  key,
@@ -2161,7 +2247,7 @@ async function createBalanceIntent(payments, input, appointmentId) {
2161
2247
  throw new BalanceRequestInProgressError(diagnostic2.ref);
2162
2248
  }
2163
2249
  if (result.code === "KEY_REUSED") {
2164
- const diagnostic2 = (0, import_scheduling19.schedError)("SCHED_605", {
2250
+ const diagnostic2 = (0, import_scheduling20.schedError)("SCHED_605", {
2165
2251
  error: result.error,
2166
2252
  appointmentId,
2167
2253
  abandonedKey: key,
@@ -2177,7 +2263,7 @@ async function createBalanceIntent(payments, input, appointmentId) {
2177
2263
  if (result.code === "CANCELED") continue;
2178
2264
  throw new Error(result.error ?? "The payment provider did not create the balance payment");
2179
2265
  }
2180
- const diagnostic = (0, import_scheduling19.schedError)("SCHED_605", {
2266
+ const diagnostic = (0, import_scheduling20.schedError)("SCHED_605", {
2181
2267
  appointmentId,
2182
2268
  phase: "balance-key-generations-exhausted",
2183
2269
  generations: MAX_KEY_GENERATIONS2
@@ -2269,7 +2355,7 @@ async function collectAppointmentBalance(request, { host, params }) {
2269
2355
  if (error instanceof BalanceRequestInProgressError || error instanceof BalanceIntentGenerationsExhaustedError) {
2270
2356
  return Response.json({ error: error.message, ref: error.ref }, { status: 409 });
2271
2357
  }
2272
- const diagnostic = (0, import_scheduling20.schedError)("SCHED_605", { error, appointmentId });
2358
+ const diagnostic = (0, import_scheduling21.schedError)("SCHED_605", { error, appointmentId });
2273
2359
  console.error("[POST /collect-balance]", diagnostic.ref, error instanceof Error ? error.message : error);
2274
2360
  return Response.json({ error: "Failed to create balance payment", ref: diagnostic.ref }, { status: 500 });
2275
2361
  }
@@ -2279,7 +2365,7 @@ async function collectAppointmentBalance(request, { host, params }) {
2279
2365
  var import_zod6 = require("zod");
2280
2366
 
2281
2367
  // src/payments/refund/refund-status.ts
2282
- var import_scheduling21 = require("@ordinatio/scheduling");
2368
+ var import_scheduling22 = require("@ordinatio/scheduling");
2283
2369
  var REFUND_STATUS_BUCKET = {
2284
2370
  PENDING_REVIEW: "retryable",
2285
2371
  REFUND_FAILED: "retryable",
@@ -2301,7 +2387,7 @@ var CREDIT_REFUSED_NEXT_STEP = "Retry the refund to resolve the remaining balanc
2301
2387
  var CREDIT_REFUSED_NO_AMOUNT_NEXT_STEP = "Check the refunded amount in Stripe, then retry the refund or resolve this manually before issuing a credit.";
2302
2388
  function checkCreditGuard(action, refundAmount, refundStatus) {
2303
2389
  if (refundStatus == null) {
2304
- const diagnostic = (0, import_scheduling21.schedError)("SCHED_610", {
2390
+ const diagnostic = (0, import_scheduling22.schedError)("SCHED_610", {
2305
2391
  action,
2306
2392
  refundAmount,
2307
2393
  error: "checkCreditGuard was called without a refundStatus"
@@ -2347,10 +2433,10 @@ function checkCreditGuard(action, refundAmount, refundStatus) {
2347
2433
  }
2348
2434
 
2349
2435
  // src/money.ts
2350
- var import_scheduling22 = require("@ordinatio/scheduling");
2436
+ var import_scheduling23 = require("@ordinatio/scheduling");
2351
2437
  function decimalToCents(value, field = "unknown") {
2352
2438
  if (value === null || value === void 0) {
2353
- throw new import_scheduling22.SchedulingValidationError(
2439
+ throw new import_scheduling23.SchedulingValidationError(
2354
2440
  `Could not convert ${field} to cents \u2014 value is ${value}`,
2355
2441
  "SCHED_406",
2356
2442
  { field, value }
@@ -2358,7 +2444,7 @@ function decimalToCents(value, field = "unknown") {
2358
2444
  }
2359
2445
  const cents = Math.round(Number(value) * 100);
2360
2446
  if (!Number.isFinite(cents)) {
2361
- throw new import_scheduling22.SchedulingValidationError(
2447
+ throw new import_scheduling23.SchedulingValidationError(
2362
2448
  `Could not convert ${field} to cents \u2014 value is not numeric`,
2363
2449
  "SCHED_406",
2364
2450
  { field, value }
@@ -2369,7 +2455,7 @@ function decimalToCents(value, field = "unknown") {
2369
2455
 
2370
2456
  // src/payments/refund/refund-leg.ts
2371
2457
  var import_node_crypto2 = require("crypto");
2372
- var import_scheduling23 = require("@ordinatio/scheduling");
2458
+ var import_scheduling24 = require("@ordinatio/scheduling");
2373
2459
  async function attemptLegRefund(payments, paymentIntentId, idempotencyKeySuffix, appointmentId, errorCode, logLabel) {
2374
2460
  const baseKey = `refund:${appointmentId}:${idempotencyKeySuffix}`;
2375
2461
  let idempotencyKey = baseKey;
@@ -2402,7 +2488,7 @@ async function attemptLegRefund(payments, paymentIntentId, idempotencyKeySuffix,
2402
2488
  return { succeeded: true, amountCents: null };
2403
2489
  }
2404
2490
  if (result.code === "IN_PROGRESS") {
2405
- const inFlight = (0, import_scheduling23.schedError)(errorCode, { error: result.error, appointmentId, paymentIntentId, kind: "idempotency_key_in_use" });
2491
+ const inFlight = (0, import_scheduling24.schedError)(errorCode, { error: result.error, appointmentId, paymentIntentId, kind: "idempotency_key_in_use" });
2406
2492
  console.warn(
2407
2493
  `[resolve-refund] ${logLabel} leg is already being refunded by another in-flight request (idempotency key in use) \u2014 its outcome is unknown, so this leg is recorded as retryable:`,
2408
2494
  inFlight.ref,
@@ -2428,7 +2514,7 @@ async function attemptLegRefund(payments, paymentIntentId, idempotencyKeySuffix,
2428
2514
  }
2429
2515
  }
2430
2516
  function failedLeg(errorCode, logLabel, appointmentId, paymentIntentId, idempotencyKey, retriedAfterReplay, failure3) {
2431
- const diagnostic = (0, import_scheduling23.schedError)(errorCode, { error: failure3.error, appointmentId, paymentIntentId });
2517
+ const diagnostic = (0, import_scheduling24.schedError)(errorCode, { error: failure3.error, appointmentId, paymentIntentId });
2432
2518
  console.error(
2433
2519
  `[resolve-refund] ${logLabel} refund failed${retriedAfterReplay ? " (after a fresh-key re-attempt)" : ""} \u2014 manual refund may be required:`,
2434
2520
  diagnostic.ref,
@@ -2442,7 +2528,7 @@ function failedLeg(errorCode, logLabel, appointmentId, paymentIntentId, idempote
2442
2528
  }
2443
2529
 
2444
2530
  // src/payments/refund/persist-refund-outcome.ts
2445
- var import_scheduling24 = require("@ordinatio/scheduling");
2531
+ var import_scheduling25 = require("@ordinatio/scheduling");
2446
2532
  var WEBHOOK_RACE_OUTCOMES = ["REFUNDED", "PARTIAL_REFUND"];
2447
2533
  var SUCCESS_RANK = {
2448
2534
  REFUND_FAILED: 0,
@@ -2487,7 +2573,7 @@ async function persistRefundOutcome(db, id, refundOutcome, refundAmount, legs) {
2487
2573
  });
2488
2574
  } catch (writeErr) {
2489
2575
  const dbMessage = writeErr instanceof Error ? writeErr.message : String(writeErr);
2490
- const diagnostic = (0, import_scheduling24.schedError)("SCHED_610", {
2576
+ const diagnostic = (0, import_scheduling25.schedError)("SCHED_610", {
2491
2577
  error: writeErr,
2492
2578
  dbMessage,
2493
2579
  appointmentId: id,
@@ -2511,7 +2597,7 @@ async function persistRefundOutcome(db, id, refundOutcome, refundAmount, legs) {
2511
2597
  }
2512
2598
  if (writeResult.count === 0) {
2513
2599
  const buildLostRaceError = () => {
2514
- const diagnostic = (0, import_scheduling24.schedError)("SCHED_610", {
2600
+ const diagnostic = (0, import_scheduling25.schedError)("SCHED_610", {
2515
2601
  appointmentId: id,
2516
2602
  phase: "persist-cas-lost-race",
2517
2603
  refundOutcome,
@@ -2550,7 +2636,7 @@ async function persistRefundOutcome(db, id, refundOutcome, refundAmount, legs) {
2550
2636
  if (reconcileResult.count === 0) {
2551
2637
  throw buildLostRaceError();
2552
2638
  }
2553
- const reconcileDiagnostic = (0, import_scheduling24.schedError)("SCHED_610", {
2639
+ const reconcileDiagnostic = (0, import_scheduling25.schedError)("SCHED_610", {
2554
2640
  appointmentId: id,
2555
2641
  phase: "persist-cas-reconciled",
2556
2642
  refundOutcome,
@@ -2649,7 +2735,7 @@ async function executeRefundLegs(db, id, preClaimSnapshot, payments) {
2649
2735
  }
2650
2736
 
2651
2737
  // src/payments/refund/refund-housekeeping.ts
2652
- var import_scheduling25 = require("@ordinatio/scheduling");
2738
+ var import_scheduling26 = require("@ordinatio/scheduling");
2653
2739
  async function runHousekeeping(db, activity, appointment, id, action, newRefundStatus, userId) {
2654
2740
  const isTerminalSuccess = TERMINAL_SUCCESS_STATUSES.includes(newRefundStatus);
2655
2741
  try {
@@ -2665,7 +2751,7 @@ async function runHousekeeping(db, activity, appointment, id, action, newRefundS
2665
2751
  });
2666
2752
  return void 0;
2667
2753
  } catch (err) {
2668
- const diagnostic = (0, import_scheduling25.schedError)("SCHED_606", { error: err, appointmentId: id, phase: "housekeeping" });
2754
+ const diagnostic = (0, import_scheduling26.schedError)("SCHED_606", { error: err, appointmentId: id, phase: "housekeeping" });
2669
2755
  console.error(
2670
2756
  isTerminalSuccess ? "[resolve-refund] Sticky-activity resolution or audit log failed after a successful refund/credit:" : "[resolve-refund] Sticky-activity resolution or audit log failed after a non-terminal refund outcome (REFUND_FAILED/PARTIAL_REFUND):",
2671
2757
  diagnostic.ref,
@@ -2750,9 +2836,9 @@ async function evaluatePreClaimState(input) {
2750
2836
  }
2751
2837
 
2752
2838
  // src/payments/refund/refund-failure-response.ts
2753
- var import_scheduling26 = require("@ordinatio/scheduling");
2839
+ var import_scheduling27 = require("@ordinatio/scheduling");
2754
2840
  function refundFailureResponse({ error, id, stripeMayHaveMoved }) {
2755
- const diagnostic = (0, import_scheduling26.schedError)("SCHED_610", { error, appointmentId: id, stripeMayHaveMoved });
2841
+ const diagnostic = (0, import_scheduling27.schedError)("SCHED_610", { error, appointmentId: id, stripeMayHaveMoved });
2756
2842
  console.error("[POST /resolve-refund]", diagnostic.ref, error instanceof Error ? error.message : error);
2757
2843
  if (!stripeMayHaveMoved) {
2758
2844
  return Response.json({ error: "Failed to resolve refund", ref: diagnostic.ref }, { status: 500 });
@@ -3229,6 +3315,57 @@ var CreateWebhookEndpointSchema = import_zod11.z.object({
3229
3315
  description: optionalString(200, "Description")
3230
3316
  });
3231
3317
 
3318
+ // src/schemas/routing.schema.ts
3319
+ var import_zod12 = require("zod");
3320
+ var bounded = {
3321
+ name: import_zod12.z.string().max(200),
3322
+ slug: import_zod12.z.string().max(100),
3323
+ description: import_zod12.z.string().max(1e3).nullable().optional(),
3324
+ isActive: import_zod12.z.boolean().optional(),
3325
+ questions: import_zod12.z.array(import_zod12.z.unknown()).max(30),
3326
+ rules: import_zod12.z.array(import_zod12.z.unknown()).max(60),
3327
+ fallback: import_zod12.z.unknown()
3328
+ };
3329
+ var CreateRoutingFormSchema = import_zod12.z.object({
3330
+ ...bounded,
3331
+ fallback: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown())
3332
+ });
3333
+ var UpdateRoutingFormSchema = import_zod12.z.object({ ...bounded, fallback: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()) }).partial().refine((value) => Object.keys(value).length > 0, { message: "Send at least one field to change" });
3334
+ var RouteClientSchema = import_zod12.z.object({
3335
+ slug: import_zod12.z.string().min(1).max(100),
3336
+ orgSlug: import_zod12.z.string().min(1).max(100).optional(),
3337
+ answers: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown())
3338
+ });
3339
+ var TestRoutingFormSchema = import_zod12.z.object({ answers: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()) });
3340
+
3341
+ // src/schemas/privacy.schema.ts
3342
+ var import_zod13 = require("zod");
3343
+ var subject = {
3344
+ email: import_zod13.z.string().max(320).optional(),
3345
+ clientId: import_zod13.z.string().max(200).optional()
3346
+ };
3347
+ var namesSomeone = (value) => Boolean(value.email?.trim() || value.clientId?.trim());
3348
+ var namesSomeoneMessage = { message: "Send an email address, a client id, or both" };
3349
+ var PrivacySubjectSchema = import_zod13.z.object(subject).strict().refine(namesSomeone, namesSomeoneMessage);
3350
+ var PrivacyEraseSchema = import_zod13.z.object({ ...subject, confirm: import_zod13.z.boolean().optional() }).strict().refine(namesSomeone, namesSomeoneMessage);
3351
+
3352
+ // src/schemas/brand.schema.ts
3353
+ var import_zod14 = require("zod");
3354
+ var optionalText = (max) => import_zod14.z.string().max(max).nullable().optional();
3355
+ var BrandSchema = import_zod14.z.object({
3356
+ displayName: import_zod14.z.string().max(200),
3357
+ logoUrl: optionalText(1e3),
3358
+ primaryColor: optionalText(20),
3359
+ accentColor: optionalText(20),
3360
+ backgroundColor: optionalText(20),
3361
+ textColor: optionalText(20),
3362
+ fontFamily: optionalText(200),
3363
+ footerText: optionalText(1e3),
3364
+ replyToEmail: optionalText(400),
3365
+ fromName: optionalText(200),
3366
+ fromEmail: optionalText(400)
3367
+ }).strict();
3368
+
3232
3369
  // src/pagination.ts
3233
3370
  var DEFAULT_LIMIT = 50;
3234
3371
  var MAX_LIMIT = 200;
@@ -3250,12 +3387,12 @@ function parsePagination(request, maxLimit = MAX_LIMIT, defaultLimit = DEFAULT_L
3250
3387
  }
3251
3388
 
3252
3389
  // src/validation.ts
3253
- var import_zod12 = require("zod");
3390
+ var import_zod15 = require("zod");
3254
3391
  async function parseBody(request, schema) {
3255
3392
  try {
3256
3393
  return { success: true, data: schema.parse(await request.json()) };
3257
3394
  } catch (error) {
3258
- if (error instanceof import_zod12.ZodError) {
3395
+ if (error instanceof import_zod15.ZodError) {
3259
3396
  const details = error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message }));
3260
3397
  return { success: false, response: Response.json({ error: "Validation failed", details }, { status: 400 }) };
3261
3398
  }
@@ -3269,7 +3406,7 @@ function validateData(data, schema) {
3269
3406
  try {
3270
3407
  return { success: true, data: schema.parse(data) };
3271
3408
  } catch (error) {
3272
- if (error instanceof import_zod12.ZodError) {
3409
+ if (error instanceof import_zod15.ZodError) {
3273
3410
  return { success: false, errors: error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message })) };
3274
3411
  }
3275
3412
  return { success: false, errors: [{ field: "", message: "Validation failed" }] };
@@ -3739,7 +3876,7 @@ async function updatePackagePurchase(request, { host, params }) {
3739
3876
  }
3740
3877
 
3741
3878
  // src/handlers/admin-group-sessions.ts
3742
- var import_scheduling27 = require("@ordinatio/scheduling");
3879
+ var import_scheduling28 = require("@ordinatio/scheduling");
3743
3880
  var MAX_RANGE_MS = 366 * 24 * 60 * 60 * 1e3;
3744
3881
  async function listGroupSessions(request, { host }) {
3745
3882
  try {
@@ -3757,29 +3894,29 @@ async function listGroupSessions(request, { host }) {
3757
3894
  return Response.json({ error: "Invalid date format \u2014 use ISO 8601" }, { status: 400 });
3758
3895
  }
3759
3896
  if (startDate >= endDate) {
3760
- const err = (0, import_scheduling27.schedError)("SCHED_310", { startDateStr, endDateStr });
3897
+ const err = (0, import_scheduling28.schedError)("SCHED_310", { startDateStr, endDateStr });
3761
3898
  return Response.json({ error: "startDate must be before endDate", ref: err.ref }, { status: 400 });
3762
3899
  }
3763
3900
  if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {
3764
- const err = (0, import_scheduling27.schedError)("SCHED_312", { startDateStr, endDateStr });
3901
+ const err = (0, import_scheduling28.schedError)("SCHED_312", { startDateStr, endDateStr });
3765
3902
  return Response.json({ error: "Date range exceeds maximum allowed (366 days)", ref: err.ref }, { status: 400 });
3766
3903
  }
3767
3904
  const calendarId = params.get("calendarId") || void 0;
3768
3905
  const visibleCalendarIds = await staff.visibleCalendarIds();
3769
- const { sessions, hasMore } = await (0, import_scheduling27.getGroupSessions)(staff.db, { startDate, endDate, calendarId, visibleCalendarIds });
3906
+ const { sessions, hasMore } = await (0, import_scheduling28.getGroupSessions)(staff.db, { startDate, endDate, calendarId, visibleCalendarIds });
3770
3907
  return Response.json({ sessions, total: sessions.length, hasMore });
3771
3908
  } catch (error) {
3772
- const err = (0, import_scheduling27.schedError)("SCHED_311", { error });
3909
+ const err = (0, import_scheduling28.schedError)("SCHED_311", { error });
3773
3910
  console.error("[group-sessions]", err.ref, error);
3774
3911
  return Response.json({ error: "Failed to fetch group sessions", ref: err.ref }, { status: 500 });
3775
3912
  }
3776
3913
  }
3777
3914
 
3778
3915
  // src/handlers/admin-recurring.ts
3779
- var import_scheduling30 = require("@ordinatio/scheduling");
3916
+ var import_scheduling31 = require("@ordinatio/scheduling");
3780
3917
 
3781
3918
  // src/recurring/series-side-effects.ts
3782
- var import_scheduling28 = require("@ordinatio/scheduling");
3919
+ var import_scheduling29 = require("@ordinatio/scheduling");
3783
3920
 
3784
3921
  // src/recurring/failure-collector.ts
3785
3922
  function createFailureCollector() {
@@ -3843,7 +3980,7 @@ async function runSeriesSideEffects(host, db, organizationId, seriesId, client)
3843
3980
  const sendConfirmation = async (url, appointmentId) => {
3844
3981
  for (let tryNumber = 1; tryNumber <= 2; tryNumber += 1) {
3845
3982
  try {
3846
- await (0, import_scheduling28.scheduleNotifications)(db, appointmentId, url);
3983
+ await (0, import_scheduling29.scheduleNotifications)(db, appointmentId, url, { brand: Boolean(host.brand) });
3847
3984
  return;
3848
3985
  } catch (error) {
3849
3986
  if (tryNumber === 2) record("Client confirmation", error);
@@ -3855,10 +3992,9 @@ async function runSeriesSideEffects(host, db, organizationId, seriesId, client)
3855
3992
  if (visit === confirmationVisit) {
3856
3993
  await sendConfirmation(baseUrl, visit.id);
3857
3994
  } else {
3858
- await attempt(
3859
- "Client notices",
3860
- () => (0, import_scheduling28.scheduleNotifications)(db, visit.id, baseUrl, { excludeTriggers: ["CONFIRMATION"] })
3861
- );
3995
+ await attempt("Client notices", async () => {
3996
+ await (0, import_scheduling29.scheduleNotifications)(db, visit.id, baseUrl, { excludeTriggers: ["CONFIRMATION"], brand: Boolean(host.brand) });
3997
+ });
3862
3998
  }
3863
3999
  }
3864
4000
  await attempt(
@@ -3877,7 +4013,7 @@ async function runSeriesSideEffects(host, db, organizationId, seriesId, client)
3877
4013
  await Promise.all(visits.slice(from, from + VISITS_PER_GROUP).map(handleVisit));
3878
4014
  }
3879
4015
  if (baseUrl && confirmationVisit) {
3880
- await attempt("Staff alert", () => (0, import_scheduling28.queueStaffNotification)(db, confirmationVisit.id, "CONFIRMATION", baseUrl));
4016
+ await attempt("Staff alert", () => (0, import_scheduling29.queueStaffNotification)(db, confirmationVisit.id, "CONFIRMATION", baseUrl, { brand: Boolean(host.brand) }));
3881
4017
  }
3882
4018
  await attempt("Availability cache", () => host.availabilityCache.invalidate(first.calendarId));
3883
4019
  await attempt(
@@ -3893,11 +4029,11 @@ async function runSeriesSideEffects(host, db, organizationId, seriesId, client)
3893
4029
  }
3894
4030
 
3895
4031
  // src/recurring/validate-timezone.ts
3896
- var import_scheduling29 = require("@ordinatio/scheduling");
4032
+ var import_scheduling30 = require("@ordinatio/scheduling");
3897
4033
  var SUPPORTED_TIMEZONES = new Set(Intl.supportedValuesOf("timeZone"));
3898
4034
  function validateRecurringTimezone(timezone) {
3899
4035
  if (isValidIanaTimezone(timezone)) return null;
3900
- const err = (0, import_scheduling29.schedError)("SCHED_704", { timezone });
4036
+ const err = (0, import_scheduling30.schedError)("SCHED_704", { timezone });
3901
4037
  return Response.json({ error: err.description, ref: err.ref }, { status: 422 });
3902
4038
  }
3903
4039
  function isValidIanaTimezone(timezone) {
@@ -3950,12 +4086,12 @@ async function createRecurringSeries(request, { host }) {
3950
4086
  const validation = await parseBody(request, CreateRecurringSeriesSchema);
3951
4087
  if (!validation.success) return validation.response;
3952
4088
  const input = validation.data;
3953
- if (input.occurrences < 1 || input.occurrences > import_scheduling30.RECURRING_MAX_OCCURRENCES) {
3954
- const err = (0, import_scheduling30.schedError)("SCHED_701", { occurrences: input.occurrences });
4089
+ if (input.occurrences < 1 || input.occurrences > import_scheduling31.RECURRING_MAX_OCCURRENCES) {
4090
+ const err = (0, import_scheduling31.schedError)("SCHED_701", { occurrences: input.occurrences });
3955
4091
  return Response.json({ error: err.description, ref: err.ref }, { status: 422 });
3956
4092
  }
3957
- if (!(0, import_scheduling30.parseTimeLocal)(input.startTimeLocal)) {
3958
- const err = (0, import_scheduling30.schedError)("SCHED_700", { startTimeLocal: input.startTimeLocal });
4093
+ if (!(0, import_scheduling31.parseTimeLocal)(input.startTimeLocal)) {
4094
+ const err = (0, import_scheduling31.schedError)("SCHED_700", { startTimeLocal: input.startTimeLocal });
3959
4095
  return Response.json({ error: err.description, ref: err.ref }, { status: 422 });
3960
4096
  }
3961
4097
  const tzError = validateRecurringTimezone(input.timezone);
@@ -3984,7 +4120,7 @@ async function createRecurringSeries(request, { host }) {
3984
4120
  });
3985
4121
  const firstDate = new Date(input.firstDate);
3986
4122
  const endsAt = input.endsAt ? new Date(input.endsAt) : void 0;
3987
- const slots = (0, import_scheduling30.buildRecurringSlots)(
4123
+ const slots = (0, import_scheduling31.buildRecurringSlots)(
3988
4124
  firstDate,
3989
4125
  input.frequency,
3990
4126
  input.occurrences,
@@ -4065,17 +4201,17 @@ async function createRecurringSeries(request, { host }) {
4065
4201
  }
4066
4202
 
4067
4203
  // src/handlers/admin-recurring-detail.ts
4068
- var import_scheduling32 = require("@ordinatio/scheduling");
4204
+ var import_scheduling33 = require("@ordinatio/scheduling");
4069
4205
 
4070
4206
  // src/recurring/series-cancel-side-effects.ts
4071
- var import_scheduling31 = require("@ordinatio/scheduling");
4207
+ var import_scheduling32 = require("@ordinatio/scheduling");
4072
4208
  var OCCURRENCES_PER_GROUP = 8;
4073
4209
  async function runSeriesCancelSideEffects(host, db, organizationId, seriesId, seriesCalendarId, occurrences) {
4074
4210
  if (occurrences.length === 0) return { failures: [] };
4075
4211
  const { attempt, record, lines } = createFailureCollector();
4076
4212
  const handle = async (occurrence) => {
4077
4213
  const { id } = occurrence;
4078
- await attempt("Pending reminders", () => (0, import_scheduling31.cancelPendingNotifications)(db, id));
4214
+ await attempt("Pending reminders", () => (0, import_scheduling32.cancelPendingNotifications)(db, id));
4079
4215
  const hasPayment = hasPendingPayment(occurrence.paymentId, occurrence.paymentStatus);
4080
4216
  await attempt("Refund review flag", async () => {
4081
4217
  await writeRefundStatusAfterCancel(host, db, { appointmentId: id, hasPayment, clientName: occurrence.clientName });
@@ -4107,7 +4243,7 @@ async function runSeriesCancelSideEffects(host, db, organizationId, seriesId, se
4107
4243
  }
4108
4244
  const baseUrl = host.appUrl()?.trim();
4109
4245
  if (baseUrl) {
4110
- await attempt("Staff notice", () => (0, import_scheduling31.queueStaffNotification)(db, occurrences[0].id, "CANCELLATION", baseUrl));
4246
+ await attempt("Staff notice", () => (0, import_scheduling32.queueStaffNotification)(db, occurrences[0].id, "CANCELLATION", baseUrl, { brand: Boolean(host.brand) }));
4111
4247
  } else {
4112
4248
  record("Staff notice", "skipped because the application address is not set");
4113
4249
  }
@@ -4153,7 +4289,7 @@ async function getRecurringSeries(request, { host, params }) {
4153
4289
  }
4154
4290
  });
4155
4291
  if (!series || !await staff.canSeeCalendar(series.calendar.id)) {
4156
- const err = (0, import_scheduling32.schedError)("SCHED_702", { seriesId: id, organizationId });
4292
+ const err = (0, import_scheduling33.schedError)("SCHED_702", { seriesId: id, organizationId });
4157
4293
  return Response.json({ error: err.description, ref: err.ref }, { status: 404 });
4158
4294
  }
4159
4295
  return Response.json({ series });
@@ -4180,14 +4316,14 @@ async function cancelRecurringSeries(request, { host, params }) {
4180
4316
  select: { id: true, status: true, clientName: true, clientEmail: true, frequency: true, occurrences: true, calendarId: true }
4181
4317
  });
4182
4318
  if (!series) {
4183
- const err = (0, import_scheduling32.schedError)("SCHED_702", { seriesId: id, organizationId });
4319
+ const err = (0, import_scheduling33.schedError)("SCHED_702", { seriesId: id, organizationId });
4184
4320
  return Response.json({ error: err.description, ref: err.ref }, { status: 404 });
4185
4321
  }
4186
4322
  if (!await staff.canSeeCalendar(series.calendarId)) {
4187
4323
  return Response.json({ error: "Calendar not found" }, { status: 404 });
4188
4324
  }
4189
4325
  if ((input.scope === "ONE" || input.scope === "THIS_AND_FOLLOWING") && !input.fromAppointmentId) {
4190
- const err = (0, import_scheduling32.schedError)("SCHED_703", { scope: input.scope });
4326
+ const err = (0, import_scheduling33.schedError)("SCHED_703", { scope: input.scope });
4191
4327
  return Response.json({ error: err.description, ref: err.ref }, { status: 422 });
4192
4328
  }
4193
4329
  const NON_CANCELLABLE_STATUSES = ["CANCELLED", "COMPLETED", "NO_SHOW"];
@@ -4215,7 +4351,7 @@ async function cancelRecurringSeries(request, { host, params }) {
4215
4351
  if (hiddenCalendars) {
4216
4352
  const hidden = await db.appointment.count({ where: { ...open, calendar: { organizationId, NOT: hiddenCalendars } } });
4217
4353
  if (hidden > 0) {
4218
- const err = (0, import_scheduling32.schedError)("SCHED_702", { seriesId: id, organizationId, reason: "open occurrence on a calendar outside the viewer scope" });
4354
+ const err = (0, import_scheduling33.schedError)("SCHED_702", { seriesId: id, organizationId, reason: "open occurrence on a calendar outside the viewer scope" });
4219
4355
  return Response.json({ error: err.description, ref: err.ref }, { status: 404 });
4220
4356
  }
4221
4357
  }
@@ -4248,7 +4384,7 @@ async function cancelRecurringSeries(request, { host, params }) {
4248
4384
  return Response.json({ cancelledCount, newSeriesStatus, ...warnings.length > 0 && { warnings } });
4249
4385
  } catch (error) {
4250
4386
  if (error?.code === "P2025") {
4251
- const err = (0, import_scheduling32.schedError)("SCHED_702", { race: "series vanished between org-scoped pre-check and cancellation transaction" });
4387
+ const err = (0, import_scheduling33.schedError)("SCHED_702", { race: "series vanished between org-scoped pre-check and cancellation transaction" });
4252
4388
  return Response.json({ error: err.description, ref: err.ref }, { status: 404 });
4253
4389
  }
4254
4390
  return Response.json({ error: getErrorMessage(error, "Failed to cancel recurring series") }, { status: 500 });
@@ -4256,7 +4392,7 @@ async function cancelRecurringSeries(request, { host, params }) {
4256
4392
  }
4257
4393
 
4258
4394
  // src/handlers/booking-analytics.ts
4259
- var import_scheduling33 = require("@ordinatio/scheduling");
4395
+ var import_scheduling34 = require("@ordinatio/scheduling");
4260
4396
  async function getAnalytics(request, { host }) {
4261
4397
  try {
4262
4398
  const staff = await host.staff.resolve(request);
@@ -4268,7 +4404,7 @@ async function getAnalytics(request, { host }) {
4268
4404
  const limited = await host.rateLimit(`analytics:${ip}`, "GENERAL");
4269
4405
  if (limited) return limited;
4270
4406
  if (!staff.isAdmin) {
4271
- const err = (0, import_scheduling33.schedError)("SCHED_900", { userId: staff.userId, role: staff.role });
4407
+ const err = (0, import_scheduling34.schedError)("SCHED_900", { userId: staff.userId, role: staff.role });
4272
4408
  return Response.json({ error: "Forbidden", ref: err.ref }, { status: 403 });
4273
4409
  }
4274
4410
  const params = new URL(request.url).searchParams;
@@ -4294,7 +4430,7 @@ async function getAnalytics(request, { host }) {
4294
4430
  }
4295
4431
  }
4296
4432
  const visibleCalendarIds = await staff.visibleCalendarIds();
4297
- const analytics = await (0, import_scheduling33.getBookingAnalytics)(staff.db, { days, calendarId, visibleCalendarIds });
4433
+ const analytics = await (0, import_scheduling34.getBookingAnalytics)(staff.db, { days, calendarId, visibleCalendarIds });
4298
4434
  return Response.json(analytics);
4299
4435
  } catch (error) {
4300
4436
  const ref = getErrorRef(error);
@@ -4312,6 +4448,36 @@ function escapeHtml(text) {
4312
4448
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
4313
4449
  }
4314
4450
 
4451
+ // src/brand/brand-http.ts
4452
+ var import_scheduling35 = require("@ordinatio/scheduling");
4453
+ var NOT_ENABLED = { error: "Branding is not enabled on this server" };
4454
+ function brandErrorResponse(error, fallback, cors) {
4455
+ const send = (payload, status) => cors ? corsJson(cors, payload, { status }) : Response.json(payload, { status });
4456
+ if (error instanceof import_scheduling35.SchedulingValidationError) {
4457
+ const details = error.context.errors;
4458
+ return send({ error: error.message, code: error.code, ref: getErrorRef(error), ...details ? { details } : {} }, 400);
4459
+ }
4460
+ console.error("[brand]", error);
4461
+ return send({ error: fallback }, 500);
4462
+ }
4463
+ async function requireBrandAdmin(host, request, options) {
4464
+ const staff = await requireAdminStaff(host, request, options);
4465
+ if (staff instanceof Response) return staff;
4466
+ if (!host.brand) return Response.json(NOT_ENABLED, { status: 501 });
4467
+ return staff;
4468
+ }
4469
+ async function businessName(host, db, organizationId) {
4470
+ if (host.brand) {
4471
+ try {
4472
+ const brand = await (0, import_scheduling35.getBrand)(db, organizationId);
4473
+ if (brand) return brand.displayName;
4474
+ } catch (error) {
4475
+ console.error("[brand] could not read the brand for a message:", error);
4476
+ }
4477
+ }
4478
+ return host.appName() || null;
4479
+ }
4480
+
4315
4481
  // src/handlers/appointment-messages.ts
4316
4482
  var NOT_FOUND4 = () => Response.json({ error: "Appointment not found" }, { status: 404 });
4317
4483
  function failure2(label, error, fallback) {
@@ -4352,7 +4518,7 @@ async function sendFollowup(request, { host, params }) {
4352
4518
  { status: 400 }
4353
4519
  );
4354
4520
  }
4355
- const staffName = appointment.calendar.staffName || host.appName() || "Your provider";
4521
+ const staffName = appointment.calendar.staffName || await businessName(host, db, organizationId) || "Your provider";
4356
4522
  const serviceName = appointment.type.name;
4357
4523
  await db.scheduledNotification.deleteMany({
4358
4524
  where: { appointmentId: id, trigger: "FOLLOWUP_MANUAL", status: "PENDING" }
@@ -4423,6 +4589,7 @@ async function sendReminder(request, { host, params }) {
4423
4589
  month: "long",
4424
4590
  day: "numeric"
4425
4591
  }) + " at " + time;
4592
+ const providerName = appointment.calendar.staffName || await businessName(host, db, organizationId);
4426
4593
  await db.scheduledNotification.deleteMany({
4427
4594
  where: { appointmentId: id, trigger: "REMINDER_MANUAL", status: "PENDING" }
4428
4595
  });
@@ -4433,7 +4600,7 @@ async function sendReminder(request, { host, params }) {
4433
4600
  channel: "SMS",
4434
4601
  trigger: "REMINDER_MANUAL",
4435
4602
  recipient: appointment.clientPhone,
4436
- body: `Reminder: Your ${appointment.type.name} with ${appointment.calendar.staffName || host.appName() || "your provider"} is on ${dateTime}.`,
4603
+ body: `Reminder: Your ${appointment.type.name} with ${providerName || "your provider"} is on ${dateTime}.`,
4437
4604
  scheduledFor: /* @__PURE__ */ new Date()
4438
4605
  }
4439
4606
  });
@@ -4454,7 +4621,7 @@ Date: ${dateTime}
4454
4621
 
4455
4622
  We look forward to seeing you!
4456
4623
 
4457
- \u2014 ${escapeHtml(appointment.calendar.staffName || host.appName() || "Your provider")}`,
4624
+ \u2014 ${escapeHtml(providerName || "Your provider")}`,
4458
4625
  scheduledFor: /* @__PURE__ */ new Date()
4459
4626
  }
4460
4627
  });
@@ -4470,7 +4637,7 @@ We look forward to seeing you!
4470
4637
  }
4471
4638
 
4472
4639
  // src/handlers/calendar-ownership.ts
4473
- var import_scheduling34 = require("@ordinatio/scheduling");
4640
+ var import_scheduling36 = require("@ordinatio/scheduling");
4474
4641
  async function getOwnedCalendarForRoute(db, id, organizationId) {
4475
4642
  const calendar = await db.schedulingCalendar.findUnique({
4476
4643
  where: { id },
@@ -4501,7 +4668,7 @@ async function getOwnedCalendarForRoute(db, id, organizationId) {
4501
4668
  async function validateStaffLink(host, userId, organizationId) {
4502
4669
  if (!userId) return null;
4503
4670
  if (await host.staffDirectory.isMember(organizationId, userId)) return null;
4504
- const err = (0, import_scheduling34.schedError)("SCHED_104", { userId, organizationId });
4671
+ const err = (0, import_scheduling36.schedError)("SCHED_104", { userId, organizationId });
4505
4672
  return Response.json({ error: "That staff member is not part of this organization", ref: err.ref }, { status: 400 });
4506
4673
  }
4507
4674
  function buildCalendarUpdateData(input) {
@@ -4634,7 +4801,7 @@ async function createCalendar(request, { host }) {
4634
4801
  }
4635
4802
 
4636
4803
  // src/handlers/admin-calendar-update.ts
4637
- var import_scheduling35 = require("@ordinatio/scheduling");
4804
+ var import_scheduling37 = require("@ordinatio/scheduling");
4638
4805
  var NOT_FOUND5 = () => Response.json({ error: "Calendar not found" }, { status: 404 });
4639
4806
  async function getCalendar(request, { host, params }) {
4640
4807
  try {
@@ -4664,7 +4831,7 @@ async function putCalendar(request, { host, params }) {
4664
4831
  const input = validation.data;
4665
4832
  const linkError = input.userId === void 0 ? null : await validateStaffLink(host, input.userId, organizationId);
4666
4833
  if (linkError) return linkError;
4667
- const { calendar: updated, wasActive } = await (0, import_scheduling35.updateCalendar)(db, id, organizationId, buildCalendarUpdateData(input));
4834
+ const { calendar: updated, wasActive } = await (0, import_scheduling37.updateCalendar)(db, id, organizationId, buildCalendarUpdateData(input));
4668
4835
  if (input.slotGranularityMinutes !== void 0) await host.availabilityCache.invalidate(id);
4669
4836
  if (input.isActive === false && wasActive) {
4670
4837
  await releaseCalendarSlugIfStillInactive(host, db, id, organizationId, existing.slug);
@@ -4689,7 +4856,7 @@ async function deleteCalendar(request, { host, params }) {
4689
4856
  if (!await staff.canSeeCalendar(id)) return NOT_FOUND5();
4690
4857
  const existing = await getOwnedCalendarForRoute(db, id, organizationId);
4691
4858
  if (!existing) return NOT_FOUND5();
4692
- await (0, import_scheduling35.archiveCalendar)(db, id, organizationId);
4859
+ await (0, import_scheduling37.archiveCalendar)(db, id, organizationId);
4693
4860
  await releaseCalendarSlugIfStillInactive(host, db, id, organizationId, existing.slug);
4694
4861
  await bustCalendarCachesOnArchive(host, organizationId, existing.slug, id);
4695
4862
  await host.activity.log(db, {
@@ -4846,7 +5013,7 @@ async function disconnectOutlook(request, { host, params }) {
4846
5013
  }
4847
5014
 
4848
5015
  // src/handlers/admin-calendar-location-recents.ts
4849
- var import_scheduling36 = require("@ordinatio/scheduling");
5016
+ var import_scheduling38 = require("@ordinatio/scheduling");
4850
5017
  async function getLocationRecents(request, { host, params }) {
4851
5018
  try {
4852
5019
  const staff = await requireAdminStaff(host, request, {
@@ -4891,14 +5058,14 @@ async function getLocationRecents(request, { host, params }) {
4891
5058
  }
4892
5059
  return Response.json({ locations });
4893
5060
  } catch (error) {
4894
- const diagnostic = (0, import_scheduling36.schedError)("SCHED_207", { error });
5061
+ const diagnostic = (0, import_scheduling38.schedError)("SCHED_207", { error });
4895
5062
  console.error("[location-recents]", diagnostic.ref, error);
4896
5063
  return Response.json({ error: "Failed to fetch location recents", ref: diagnostic.ref }, { status: 500 });
4897
5064
  }
4898
5065
  }
4899
5066
 
4900
5067
  // src/handlers/admin-availability-rules.ts
4901
- var import_scheduling37 = require("@ordinatio/scheduling");
5068
+ var import_scheduling39 = require("@ordinatio/scheduling");
4902
5069
 
4903
5070
  // src/availability/admin-calendar.ts
4904
5071
  async function verifyCalendar(db, calendarId, organizationId) {
@@ -4963,7 +5130,7 @@ async function listRules(request, { host, params }) {
4963
5130
  const calendar = await requireOwnedVisibleCalendar(staff, calendarId);
4964
5131
  if (calendar instanceof Response) return calendar;
4965
5132
  const { limit, offset } = parsePagination(request, 500, 200);
4966
- const { rules, total, hasMore } = await (0, import_scheduling37.listAvailabilityRules)(staff.db, calendarId, { limit, offset });
5133
+ const { rules, total, hasMore } = await (0, import_scheduling39.listAvailabilityRules)(staff.db, calendarId, { limit, offset });
4967
5134
  return Response.json({ rules, total, hasMore });
4968
5135
  } catch (error) {
4969
5136
  return Response.json({ error: getErrorMessage(error, "Failed to fetch availability") }, { status: 500 });
@@ -4982,7 +5149,7 @@ async function setWeeklyRules(request, { host, params }) {
4982
5149
  if (!Array.isArray(rules)) return Response.json({ error: "rules must be an array" }, { status: 400 });
4983
5150
  const validationError = validateWeeklyRules(rules);
4984
5151
  if (validationError) return Response.json({ error: validationError }, { status: 400 });
4985
- const created = await (0, import_scheduling37.setWeeklyAvailability)(db, calendarId, rules);
5152
+ const created = await (0, import_scheduling39.setWeeklyAvailability)(db, calendarId, rules);
4986
5153
  const activeDays = rules.map((r) => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][r.dayOfWeek]).join(", ");
4987
5154
  await host.activity.log(db, {
4988
5155
  action: "scheduling.availability_updated",
@@ -4992,7 +5159,7 @@ async function setWeeklyRules(request, { host, params }) {
4992
5159
  return Response.json(created);
4993
5160
  } catch (error) {
4994
5161
  const msg = getErrorMessage(error, "Failed to update availability");
4995
- if (error instanceof import_scheduling37.SchedulingValidationError) {
5162
+ if (error instanceof import_scheduling39.SchedulingValidationError) {
4996
5163
  console.error("[PUT /admin/availability]", error.ref, error.code, error.context);
4997
5164
  return Response.json({ error: msg, ref: error.ref }, { status: 400 });
4998
5165
  }
@@ -5001,18 +5168,18 @@ async function setWeeklyRules(request, { host, params }) {
5001
5168
  }
5002
5169
 
5003
5170
  // src/handlers/admin-availability-overrides.ts
5004
- var import_scheduling39 = require("@ordinatio/scheduling");
5171
+ var import_scheduling41 = require("@ordinatio/scheduling");
5005
5172
 
5006
5173
  // src/availability/override-helpers.ts
5007
- var import_scheduling38 = require("@ordinatio/scheduling");
5174
+ var import_scheduling40 = require("@ordinatio/scheduling");
5008
5175
  function sanitizeBlockReason(raw) {
5009
5176
  if (!raw) return void 0;
5010
5177
  return String(raw).replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]*>/g, "").slice(0, 500).trim() || void 0;
5011
5178
  }
5012
5179
  async function checkOverlappingAppointments(db, calendarId, calendarTimezone, dateStr, startTime, endTime) {
5013
5180
  const tz = calendarTimezone ?? "America/New_York";
5014
- const blockStart = (0, import_scheduling38.localToUTC)(dateStr, startTime, tz);
5015
- const blockEnd = (0, import_scheduling38.localToUTC)(dateStr, endTime, tz);
5181
+ const blockStart = (0, import_scheduling40.localToUTC)(dateStr, startTime, tz);
5182
+ const blockEnd = (0, import_scheduling40.localToUTC)(dateStr, endTime, tz);
5016
5183
  const conflictCount = await db.appointment.count({
5017
5184
  where: {
5018
5185
  calendarId,
@@ -5042,14 +5209,14 @@ async function addOverride(request, { host, params }) {
5042
5209
  const isBlockedFlag = body.isBlocked ?? true;
5043
5210
  let override;
5044
5211
  if (isBlockedFlag && body.startTime && body.endTime) {
5045
- override = await (0, import_scheduling39.addTimeBlock)(db, calendarId, {
5212
+ override = await (0, import_scheduling41.addTimeBlock)(db, calendarId, {
5046
5213
  date,
5047
5214
  startTime: body.startTime,
5048
5215
  endTime: body.endTime,
5049
5216
  reason: blockReason
5050
5217
  });
5051
5218
  } else {
5052
- override = await (0, import_scheduling39.addDateOverride)(db, calendarId, {
5219
+ override = await (0, import_scheduling41.addDateOverride)(db, calendarId, {
5053
5220
  date,
5054
5221
  isBlocked: isBlockedFlag,
5055
5222
  blockReason,
@@ -5085,7 +5252,7 @@ async function addOverride(request, { host, params }) {
5085
5252
  return Response.json({ ...override, ...overlapWarning && { warning: overlapWarning } }, { status: 201 });
5086
5253
  } catch (error) {
5087
5254
  const msg = getErrorMessage(error, "Failed to add override");
5088
- if (error instanceof import_scheduling39.SchedulingValidationError) {
5255
+ if (error instanceof import_scheduling41.SchedulingValidationError) {
5089
5256
  console.error("[POST /admin/availability]", error.ref, error.code, error.context);
5090
5257
  return Response.json({ error: msg, ref: error.ref }, { status: 400 });
5091
5258
  }
@@ -5102,7 +5269,7 @@ async function removeOverride(request, { host, params }) {
5102
5269
  if (calendar instanceof Response) return calendar;
5103
5270
  const { ruleId } = await request.json();
5104
5271
  if (!ruleId) return Response.json({ error: "ruleId is required" }, { status: 400 });
5105
- const deletedRule = await (0, import_scheduling39.removeDateOverride)(db, calendarId, ruleId);
5272
+ const deletedRule = await (0, import_scheduling41.removeDateOverride)(db, calendarId, ruleId);
5106
5273
  const deletedIsTimeBlock = deletedRule?.isBlocked && !(deletedRule.startTime === "00:00" && deletedRule.endTime === "00:00");
5107
5274
  await host.activity.log(db, {
5108
5275
  action: "scheduling.date_override_removed",
@@ -5112,7 +5279,7 @@ async function removeOverride(request, { host, params }) {
5112
5279
  return Response.json({ success: true });
5113
5280
  } catch (error) {
5114
5281
  const msg = getErrorMessage(error, "Failed to remove override");
5115
- if (error instanceof import_scheduling39.SchedulingNotFoundError) {
5282
+ if (error instanceof import_scheduling41.SchedulingNotFoundError) {
5116
5283
  console.error("[DELETE /admin/availability]", error.ref, error.code, error.context);
5117
5284
  return Response.json({ error: msg, ref: error.ref }, { status: 404 });
5118
5285
  }
@@ -5121,7 +5288,7 @@ async function removeOverride(request, { host, params }) {
5121
5288
  }
5122
5289
 
5123
5290
  // src/handlers/admin-templates.ts
5124
- var import_scheduling40 = require("@ordinatio/scheduling");
5291
+ var import_scheduling42 = require("@ordinatio/scheduling");
5125
5292
 
5126
5293
  // src/sanitize/notification-body.ts
5127
5294
  var import_sanitize_html = __toESM(require("sanitize-html"));
@@ -5145,7 +5312,7 @@ async function listTemplates(request, { host }) {
5145
5312
  try {
5146
5313
  const staff = await requireAdminStaff(host, request, { action: "admin-templates-list", tier: "GENERAL" });
5147
5314
  if (staff instanceof Response) return staff;
5148
- return Response.json(await (0, import_scheduling40.listNotificationTemplates)(staff.db, staff.organizationId));
5315
+ return Response.json(await (0, import_scheduling42.listNotificationTemplates)(staff.db, staff.organizationId));
5149
5316
  } catch (error) {
5150
5317
  return Response.json({ error: getErrorMessage(error, "Failed to fetch templates") }, { status: 500 });
5151
5318
  }
@@ -5170,7 +5337,7 @@ async function createTemplate(request, { host }) {
5170
5337
  if (input.channel === "EMAIL" && !input.subject?.trim()) {
5171
5338
  return Response.json({ error: "EMAIL templates require a subject" }, { status: 400 });
5172
5339
  }
5173
- const template = await (0, import_scheduling40.createNotificationTemplate)(db, organizationId, {
5340
+ const template = await (0, import_scheduling42.createNotificationTemplate)(db, organizationId, {
5174
5341
  name: input.name,
5175
5342
  channel: input.channel,
5176
5343
  trigger: input.trigger,
@@ -5196,7 +5363,7 @@ async function createTemplate(request, { host }) {
5196
5363
  }
5197
5364
 
5198
5365
  // src/handlers/admin-template-update.ts
5199
- var import_scheduling41 = require("@ordinatio/scheduling");
5366
+ var import_scheduling43 = require("@ordinatio/scheduling");
5200
5367
  async function updateTemplate(request, { host, params }) {
5201
5368
  try {
5202
5369
  const staff = await requireAdminStaff(host, request, { action: "admin-templates-update", tier: "UPDATE" });
@@ -5228,7 +5395,7 @@ async function updateTemplate(request, { host, params }) {
5228
5395
  return Response.json({ error: "EMAIL templates require a subject" }, { status: 400 });
5229
5396
  }
5230
5397
  }
5231
- const updated = await (0, import_scheduling41.updateNotificationTemplate)(db, organizationId, id, {
5398
+ const updated = await (0, import_scheduling43.updateNotificationTemplate)(db, organizationId, id, {
5232
5399
  name: input.name,
5233
5400
  subject: sanitizedSubject,
5234
5401
  body: input.body !== void 0 ? sanitizeNotificationBody(input.body, existing.channel) : void 0,
@@ -5242,11 +5409,11 @@ async function updateTemplate(request, { host, params }) {
5242
5409
  return Response.json(updated);
5243
5410
  } catch (error) {
5244
5411
  const msg = getErrorMessage(error, "Failed to update template");
5245
- if (error instanceof import_scheduling41.SchedulingValidationError) {
5412
+ if (error instanceof import_scheduling43.SchedulingValidationError) {
5246
5413
  console.error("[PUT /admin/templates]", error.ref, error.code, error.context);
5247
5414
  return Response.json({ error: msg, ref: error.ref }, { status: 400 });
5248
5415
  }
5249
- if (error instanceof import_scheduling41.SchedulingNotFoundError) {
5416
+ if (error instanceof import_scheduling43.SchedulingNotFoundError) {
5250
5417
  console.error("[PUT /admin/templates]", error.ref, error.code, error.context);
5251
5418
  return Response.json({ error: msg, ref: error.ref }, { status: 404 });
5252
5419
  }
@@ -5263,7 +5430,7 @@ async function deleteTemplate(request, { host, params }) {
5263
5430
  where: { id, organizationId },
5264
5431
  select: { name: true }
5265
5432
  });
5266
- await (0, import_scheduling41.deleteNotificationTemplate)(db, organizationId, id);
5433
+ await (0, import_scheduling43.deleteNotificationTemplate)(db, organizationId, id);
5267
5434
  await host.activity.log(db, {
5268
5435
  action: "scheduling.template_deleted",
5269
5436
  description: `deleted notification template "${template?.name || id}"`,
@@ -5272,7 +5439,7 @@ async function deleteTemplate(request, { host, params }) {
5272
5439
  return Response.json({ success: true });
5273
5440
  } catch (error) {
5274
5441
  const msg = getErrorMessage(error, "Failed to delete template");
5275
- if (error instanceof import_scheduling41.SchedulingNotFoundError) {
5442
+ if (error instanceof import_scheduling43.SchedulingNotFoundError) {
5276
5443
  console.error("[DELETE /admin/templates]", error.ref, error.code, error.context);
5277
5444
  return Response.json({ error: msg, ref: error.ref }, { status: 404 });
5278
5445
  }
@@ -5281,7 +5448,7 @@ async function deleteTemplate(request, { host, params }) {
5281
5448
  }
5282
5449
 
5283
5450
  // src/handlers/admin-types.ts
5284
- var import_scheduling42 = require("@ordinatio/scheduling");
5451
+ var import_scheduling44 = require("@ordinatio/scheduling");
5285
5452
  async function listTypes(request, { host }) {
5286
5453
  try {
5287
5454
  const staff = await requireAdminStaff(host, request, { action: "admin-types-list", tier: "GENERAL" });
@@ -5311,7 +5478,7 @@ async function createType(request, { host }) {
5311
5478
  if (!validation.success) return validation.response;
5312
5479
  if (!organizationId) return Response.json({ error: "Organization context required" }, { status: 400 });
5313
5480
  const input = validation.data;
5314
- const type = await (0, import_scheduling42.createAppointmentType)(db, {
5481
+ const type = await (0, import_scheduling44.createAppointmentType)(db, {
5315
5482
  name: input.name,
5316
5483
  slug: input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
5317
5484
  description: input.description || void 0,
@@ -5370,7 +5537,7 @@ async function createType(request, { host }) {
5370
5537
  }
5371
5538
 
5372
5539
  // src/handlers/admin-type-update.ts
5373
- var import_scheduling43 = require("@ordinatio/scheduling");
5540
+ var import_scheduling45 = require("@ordinatio/scheduling");
5374
5541
 
5375
5542
  // src/handlers/type-ownership.ts
5376
5543
  async function getOwnedAppointmentType(db, id, organizationId) {
@@ -5451,7 +5618,7 @@ async function updateType(request, { host, params }) {
5451
5618
  const validation = await parseBody(request, UpdateAppointmentTypeSchema);
5452
5619
  if (!validation.success) return validation.response;
5453
5620
  const input = validation.data;
5454
- const { type: updated, wasActive } = await (0, import_scheduling43.updateAppointmentType)(db, id, organizationId, input);
5621
+ const { type: updated, wasActive } = await (0, import_scheduling45.updateAppointmentType)(db, id, organizationId, input);
5455
5622
  if (input.isActive === false && wasActive && typeToUpdate.organizationId) {
5456
5623
  await releaseTypeSlugIfStillInactive(host, db, id, typeToUpdate.organizationId, typeToUpdate.slug);
5457
5624
  }
@@ -5468,7 +5635,7 @@ async function updateType(request, { host, params }) {
5468
5635
  return Response.json({ error: getErrorMessage(error, "Validation error") }, { status: schedErrorStatus(code) });
5469
5636
  }
5470
5637
  const message = isSchedulingServiceError(error) ? error.description || error.message : getErrorMessage(error, "Failed to update appointment type");
5471
- if (error instanceof import_scheduling43.SchedulingValidationError) return Response.json({ error: message }, { status: 400 });
5638
+ if (error instanceof import_scheduling45.SchedulingValidationError) return Response.json({ error: message }, { status: 400 });
5472
5639
  return Response.json({ error: message }, { status: 500 });
5473
5640
  }
5474
5641
  }
@@ -5720,7 +5887,7 @@ async function getStaffOptions(request, { host }) {
5720
5887
 
5721
5888
  // src/handlers/appointment-ical.ts
5722
5889
  var import_ical_generator = __toESM(require("ical-generator"));
5723
- var import_scheduling44 = require("@ordinatio/scheduling");
5890
+ var import_scheduling46 = require("@ordinatio/scheduling");
5724
5891
  async function getAppointmentIcal(request, { host }) {
5725
5892
  let token = null;
5726
5893
  try {
@@ -5748,13 +5915,13 @@ async function getAppointmentIcal(request, { host }) {
5748
5915
  const description = [appointment.type.description ?? "", `Manage your appointment: ${manageUrl}`].filter(Boolean).join("\n");
5749
5916
  const calendar = (0, import_ical_generator.default)({ name: appointment.calendar.name });
5750
5917
  calendar.createEvent({
5751
- id: `${appointment.id}@system1701`,
5918
+ id: `${appointment.id}@${host.icalUidDomain() || "ordinatio-scheduling"}`,
5752
5919
  start: appointment.startTime,
5753
5920
  end: appointment.endTime,
5754
5921
  timezone: appointment.timezone,
5755
5922
  summary,
5756
5923
  description,
5757
- location: (0, import_scheduling44.resolveLocation)(appointment) ?? void 0,
5924
+ location: (0, import_scheduling46.resolveLocation)(appointment) ?? void 0,
5758
5925
  url: manageUrl,
5759
5926
  organizer: staffName && staffEmail ? { name: staffName, email: staffEmail } : void 0
5760
5927
  });
@@ -5766,17 +5933,17 @@ async function getAppointmentIcal(request, { host }) {
5766
5933
  }
5767
5934
  });
5768
5935
  } catch (error) {
5769
- const diagnostic = (0, import_scheduling44.schedError)("SCHED_600", { error, token: token ?? "unknown" });
5936
+ const diagnostic = (0, import_scheduling46.schedError)("SCHED_600", { error, token: token ?? "unknown" });
5770
5937
  console.error("[GET /ical]", diagnostic.ref, error instanceof Error ? error.message : error);
5771
5938
  return Response.json({ error: "Failed to generate calendar file", ref: diagnostic.ref }, { status: 500 });
5772
5939
  }
5773
5940
  }
5774
5941
 
5775
5942
  // src/handlers/admin-webhooks.ts
5776
- var import_scheduling46 = require("@ordinatio/scheduling");
5943
+ var import_scheduling48 = require("@ordinatio/scheduling");
5777
5944
 
5778
5945
  // src/webhooks/require-webhooks.ts
5779
- var import_scheduling45 = require("@ordinatio/scheduling");
5946
+ var import_scheduling47 = require("@ordinatio/scheduling");
5780
5947
  async function requireWebhooksAdmin(host, request, options) {
5781
5948
  const staff = await requireAdminStaff(host, request, options);
5782
5949
  if (staff instanceof Response) return staff;
@@ -5784,10 +5951,10 @@ async function requireWebhooksAdmin(host, request, options) {
5784
5951
  return { staff, settings: host.webhooks };
5785
5952
  }
5786
5953
  function webhookErrorResponse(error, fallback) {
5787
- if (error instanceof import_scheduling45.SchedulingValidationError) {
5954
+ if (error instanceof import_scheduling47.SchedulingValidationError) {
5788
5955
  return Response.json({ error: error.message, ref: getErrorRef(error) }, { status: 400 });
5789
5956
  }
5790
- if (error instanceof import_scheduling45.SchedulingNotFoundError) {
5957
+ if (error instanceof import_scheduling47.SchedulingNotFoundError) {
5791
5958
  return Response.json({ error: error.message, ref: getErrorRef(error) }, { status: 404 });
5792
5959
  }
5793
5960
  return Response.json({ error: getErrorMessage(error, fallback) }, { status: 500 });
@@ -5799,7 +5966,7 @@ async function listWebhooks(request, { host }) {
5799
5966
  const guard = await requireWebhooksAdmin(host, request, { action: "admin-webhooks-list", tier: "GENERAL" });
5800
5967
  if (guard instanceof Response) return guard;
5801
5968
  const { db, organizationId } = guard.staff;
5802
- return Response.json({ endpoints: await (0, import_scheduling46.listWebhookEndpoints)(db, organizationId), eventTypes: import_scheduling46.WEBHOOK_EVENT_TYPES });
5969
+ return Response.json({ endpoints: await (0, import_scheduling48.listWebhookEndpoints)(db, organizationId), eventTypes: import_scheduling48.WEBHOOK_EVENT_TYPES });
5803
5970
  } catch (error) {
5804
5971
  return webhookErrorResponse(error, "Failed to list webhooks");
5805
5972
  }
@@ -5812,7 +5979,7 @@ async function createWebhook(request, { host }) {
5812
5979
  const validation = await parseBody(request, CreateWebhookEndpointSchema);
5813
5980
  if (!validation.success) return validation.response;
5814
5981
  const input = validation.data;
5815
- const { endpoint, secret } = await (0, import_scheduling46.createWebhookEndpoint)(
5982
+ const { endpoint, secret } = await (0, import_scheduling48.createWebhookEndpoint)(
5816
5983
  db,
5817
5984
  { organizationId, url: input.url, eventTypes: input.eventTypes, description: input.description },
5818
5985
  { allowInsecureTargets: guard.settings.allowInsecureTargets }
@@ -5829,14 +5996,14 @@ async function createWebhook(request, { host }) {
5829
5996
  }
5830
5997
 
5831
5998
  // src/handlers/admin-webhook-detail.ts
5832
- var import_scheduling47 = require("@ordinatio/scheduling");
5999
+ var import_scheduling49 = require("@ordinatio/scheduling");
5833
6000
  var STATUSES = /* @__PURE__ */ new Set(["PENDING", "PROCESSING", "SUCCEEDED", "DEAD_LETTER"]);
5834
6001
  async function deleteWebhook(request, { host, params }) {
5835
6002
  try {
5836
6003
  const guard = await requireWebhooksAdmin(host, request, { action: "admin-webhooks-delete", tier: "DELETE" });
5837
6004
  if (guard instanceof Response) return guard;
5838
6005
  const { db, organizationId } = guard.staff;
5839
- await (0, import_scheduling47.deleteWebhookEndpoint)(db, organizationId, params.id);
6006
+ await (0, import_scheduling49.deleteWebhookEndpoint)(db, organizationId, params.id);
5840
6007
  await host.activity.log(db, {
5841
6008
  action: "scheduling.webhook_deleted",
5842
6009
  description: "removed a webhook endpoint",
@@ -5852,7 +6019,7 @@ async function testWebhook(request, { host, params }) {
5852
6019
  const guard = await requireWebhooksAdmin(host, request, { action: "admin-webhooks-test", tier: "CREATE" });
5853
6020
  if (guard instanceof Response) return guard;
5854
6021
  const { db, organizationId } = guard.staff;
5855
- const result = await (0, import_scheduling47.sendTestWebhook)(
6022
+ const result = await (0, import_scheduling49.sendTestWebhook)(
5856
6023
  db,
5857
6024
  { organizationId, endpointId: params.id },
5858
6025
  { transport: guard.settings.transport, allowInsecureTargets: guard.settings.allowInsecureTargets }
@@ -5868,13 +6035,13 @@ async function listWebhookEndpointDeliveries(request, { host, params }) {
5868
6035
  if (guard instanceof Response) return guard;
5869
6036
  const { db, organizationId } = guard.staff;
5870
6037
  const owned = await db.webhookEndpoint.count({ where: { id: params.id, organizationId } });
5871
- if (owned === 0) throw new import_scheduling47.SchedulingNotFoundError("Webhook endpoint not found", "SCHED_1003", { endpointId: params.id });
6038
+ if (owned === 0) throw new import_scheduling49.SchedulingNotFoundError("Webhook endpoint not found", "SCHED_1003", { endpointId: params.id });
5872
6039
  const status = new URL(request.url).searchParams.get("status");
5873
6040
  if (status && !STATUSES.has(status)) {
5874
6041
  return Response.json({ error: `status must be one of ${[...STATUSES].join(", ")}` }, { status: 400 });
5875
6042
  }
5876
6043
  const { limit, offset } = parsePagination(request);
5877
- const { deliveries, total } = await (0, import_scheduling47.listWebhookDeliveries)(db, {
6044
+ const { deliveries, total } = await (0, import_scheduling49.listWebhookDeliveries)(db, {
5878
6045
  organizationId,
5879
6046
  endpointId: params.id,
5880
6047
  status: status ?? void 0,
@@ -5888,13 +6055,13 @@ async function listWebhookEndpointDeliveries(request, { host, params }) {
5888
6055
  }
5889
6056
 
5890
6057
  // src/handlers/admin-webhook-delivery-retry.ts
5891
- var import_scheduling48 = require("@ordinatio/scheduling");
6058
+ var import_scheduling50 = require("@ordinatio/scheduling");
5892
6059
  async function retryWebhook(request, { host, params }) {
5893
6060
  try {
5894
6061
  const guard = await requireWebhooksAdmin(host, request, { action: "admin-webhooks-retry", tier: "UPDATE" });
5895
6062
  if (guard instanceof Response) return guard;
5896
6063
  const { db, organizationId } = guard.staff;
5897
- await (0, import_scheduling48.retryWebhookDelivery)(db, organizationId, params.id);
6064
+ await (0, import_scheduling50.retryWebhookDelivery)(db, organizationId, params.id);
5898
6065
  return Response.json({ success: true });
5899
6066
  } catch (error) {
5900
6067
  return webhookErrorResponse(error, "Failed to retry the delivery");
@@ -5902,7 +6069,7 @@ async function retryWebhook(request, { host, params }) {
5902
6069
  }
5903
6070
 
5904
6071
  // src/handlers/internal-webhooks-deliver.ts
5905
- var import_scheduling49 = require("@ordinatio/scheduling");
6072
+ var import_scheduling51 = require("@ordinatio/scheduling");
5906
6073
  async function deliverWebhooks(request, { host }) {
5907
6074
  if (!await host.internalAccess.isAuthorized(request)) {
5908
6075
  return Response.json({ error: "Unauthorized" }, { status: 401 });
@@ -5911,7 +6078,7 @@ async function deliverWebhooks(request, { host }) {
5911
6078
  const limited = await host.rateLimit(`${request.headers.get("x-forwarded-for") ?? "worker"}:webhooks-deliver`, "INTERNAL");
5912
6079
  if (limited) return limited;
5913
6080
  try {
5914
- const summary = await (0, import_scheduling49.deliverDueWebhooks)(await host.publicDb(), {
6081
+ const summary = await (0, import_scheduling51.deliverDueWebhooks)(await host.publicDb(), {
5915
6082
  transport: host.webhooks.transport,
5916
6083
  allowInsecureTargets: host.webhooks.allowInsecureTargets
5917
6084
  });
@@ -5922,37 +6089,140 @@ async function deliverWebhooks(request, { host }) {
5922
6089
  }
5923
6090
  }
5924
6091
 
5925
- // src/handlers/appointments-book.ts
6092
+ // src/handlers/waitlist-join.ts
5926
6093
  var import_scheduling53 = require("@ordinatio/scheduling");
6094
+ var import_zod16 = require("zod");
6095
+
6096
+ // src/waitlist/waitlist-http.ts
6097
+ var import_scheduling52 = require("@ordinatio/scheduling");
6098
+ var NOT_ENABLED2 = { error: "The waitlist is not enabled on this server" };
6099
+ var STATUS_BY_CODE = {
6100
+ SCHED_1100: 404,
6101
+ SCHED_1103: 409,
6102
+ SCHED_1104: 409,
6103
+ SCHED_1106: 404,
6104
+ SCHED_1107: 410,
6105
+ SCHED_1108: 409
6106
+ };
6107
+ async function resolveWaitlistDb(host, orgSlug) {
6108
+ if (!orgSlug) return { db: await host.publicDb(), organizationId: null };
6109
+ return host.organizations.resolve({ orgSlug, logPrefix: "[waitlist]" });
6110
+ }
6111
+ function waitlistErrorResponse(cors, error, fallback) {
6112
+ if (error instanceof import_scheduling52.SchedulingValidationError || error instanceof import_scheduling52.SchedulingNotFoundError) {
6113
+ const status = STATUS_BY_CODE[error.code] ?? (error instanceof import_scheduling52.SchedulingNotFoundError ? 404 : 400);
6114
+ return corsJson(cors, { error: error.message, code: error.code, ref: getErrorRef(error) }, { status });
6115
+ }
6116
+ console.error("[waitlist]", error);
6117
+ return corsJson(cors, { error: fallback }, { status: 500 });
6118
+ }
6119
+ async function requireWaitlistAdmin(host, request, options) {
6120
+ const staff = await requireAdminStaff(host, request, options);
6121
+ if (staff instanceof Response) return staff;
6122
+ if (!host.waitlist) return Response.json(NOT_ENABLED2, { status: 501 });
6123
+ return { staff, settings: host.waitlist };
6124
+ }
6125
+
6126
+ // src/handlers/waitlist-join.ts
6127
+ var JoinWaitlistSchema = import_zod16.z.object({
6128
+ orgSlug: import_zod16.z.string().min(1).max(100).optional(),
6129
+ typeId: import_zod16.z.string().min(1),
6130
+ calendarId: import_zod16.z.string().min(1).optional(),
6131
+ day: import_zod16.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Day must be YYYY-MM-DD"),
6132
+ slotStart: import_zod16.z.string().datetime({ message: "Invalid date/time format" }).optional(),
6133
+ clientName: import_zod16.z.string().trim().min(1, "Name is required").max(200),
6134
+ clientEmail: import_zod16.z.string().email("Invalid email").max(254),
6135
+ clientPhone: import_zod16.z.string().max(30).optional()
6136
+ });
6137
+ async function joinWaitlistRoute(request, { host }) {
6138
+ const cors = host.cors(request);
6139
+ if (!host.waitlist) return corsJson(cors, NOT_ENABLED2, { status: 501 });
6140
+ try {
6141
+ const limited = await host.rateLimit(`waitlist-join:${clientAddress(request)}`, "PUBLIC_BOOKING");
6142
+ if (limited) return limited;
6143
+ const parsed = JoinWaitlistSchema.safeParse(await request.json().catch(() => null));
6144
+ if (!parsed.success) {
6145
+ const details = parsed.error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message }));
6146
+ return corsJson(cors, { error: "Validation failed", details }, { status: 400 });
6147
+ }
6148
+ const body = parsed.data;
6149
+ const context = await resolveWaitlistDb(host, body.orgSlug);
6150
+ if (!context) return corsJson(cors, { error: "Appointment type not found" }, { status: 404 });
6151
+ let organizationId = context.organizationId;
6152
+ if (!organizationId) {
6153
+ const type = await context.db.appointmentType.findUnique({ where: { id: body.typeId }, select: { organizationId: true } });
6154
+ organizationId = type?.organizationId ?? null;
6155
+ }
6156
+ if (!organizationId) return corsJson(cors, { error: "Appointment type not found" }, { status: 404 });
6157
+ const { entry, token } = await (0, import_scheduling53.joinWaitlist)(context.db, {
6158
+ organizationId,
6159
+ typeId: body.typeId,
6160
+ calendarId: body.calendarId,
6161
+ day: body.day,
6162
+ slotStart: body.slotStart ? new Date(body.slotStart) : void 0,
6163
+ clientName: body.clientName,
6164
+ clientEmail: body.clientEmail,
6165
+ clientPhone: body.clientPhone
6166
+ });
6167
+ return corsJson(cors, { entry, token }, { status: 201, headers: { "Cache-Control": "no-store" } });
6168
+ } catch (error) {
6169
+ return waitlistErrorResponse(cors, error, "Failed to join the waitlist");
6170
+ }
6171
+ }
6172
+ async function leaveWaitlistRoute(request, { host }) {
6173
+ const cors = host.cors(request);
6174
+ if (!host.waitlist) return corsJson(cors, NOT_ENABLED2, { status: 501 });
6175
+ try {
6176
+ const limited = await host.rateLimit(`waitlist-leave:${clientAddress(request)}`, "GENERAL");
6177
+ if (limited) return limited;
6178
+ const url = new URL(request.url);
6179
+ const token = url.searchParams.get("token");
6180
+ if (!token) return corsJson(cors, { error: "token is required" }, { status: 400 });
6181
+ const context = await resolveWaitlistDb(host, url.searchParams.get("orgSlug"));
6182
+ if (!context) return corsJson(cors, { error: "No waitlist entry for this link", code: "SCHED_1106" }, { status: 404 });
6183
+ const freed = await (0, import_scheduling53.leaveWaitlist)(context.db, token);
6184
+ if (freed) await offerAndAnnounce(host, host.waitlist, context.db, freed);
6185
+ return corsJson(cors, { success: true });
6186
+ } catch (error) {
6187
+ return waitlistErrorResponse(cors, error, "Failed to leave the waitlist");
6188
+ }
6189
+ }
6190
+
6191
+ // src/handlers/waitlist-offer.ts
6192
+ var import_scheduling58 = require("@ordinatio/scheduling");
6193
+ var import_zod18 = require("zod");
6194
+
6195
+ // src/handlers/appointments-book.ts
6196
+ var import_scheduling57 = require("@ordinatio/scheduling");
5927
6197
 
5928
6198
  // src/booking/booking-schema.ts
5929
- var import_zod13 = require("zod");
5930
- var BookingSchema = import_zod13.z.object({
5931
- calendarId: import_zod13.z.string().min(1).optional(),
5932
- preferredCalendarId: import_zod13.z.string().min(1).optional(),
5933
- typeId: import_zod13.z.string().min(1),
5934
- startTime: import_zod13.z.string().datetime({ message: "Invalid date/time format" }),
5935
- timezone: import_zod13.z.string().max(50).optional(),
5936
- clientName: import_zod13.z.string().trim().min(1, "Name is required").max(200),
5937
- clientEmail: import_zod13.z.string().email("Invalid email").max(254),
5938
- clientPhone: import_zod13.z.string().max(30).optional(),
6199
+ var import_zod17 = require("zod");
6200
+ var BookingSchema = import_zod17.z.object({
6201
+ calendarId: import_zod17.z.string().min(1).optional(),
6202
+ preferredCalendarId: import_zod17.z.string().min(1).optional(),
6203
+ typeId: import_zod17.z.string().min(1),
6204
+ startTime: import_zod17.z.string().datetime({ message: "Invalid date/time format" }),
6205
+ timezone: import_zod17.z.string().max(50).optional(),
6206
+ clientName: import_zod17.z.string().trim().min(1, "Name is required").max(200),
6207
+ clientEmail: import_zod17.z.string().email("Invalid email").max(254),
6208
+ clientPhone: import_zod17.z.string().max(30).optional(),
5939
6209
  // Untrusted: the host's client resolver decides whether to honor it.
5940
- clientId: import_zod13.z.string().optional(),
5941
- notes: import_zod13.z.string().max(1e3).optional(),
6210
+ clientId: import_zod17.z.string().optional(),
6211
+ notes: import_zod17.z.string().max(1e3).optional(),
5942
6212
  // Only the primitive types a form field produces; z.unknown() would accept anything.
5943
- intakeResponses: import_zod13.z.record(
5944
- import_zod13.z.string(),
5945
- import_zod13.z.union([import_zod13.z.string().max(1e3), import_zod13.z.number(), import_zod13.z.boolean(), import_zod13.z.null()])
6213
+ intakeResponses: import_zod17.z.record(
6214
+ import_zod17.z.string(),
6215
+ import_zod17.z.union([import_zod17.z.string().max(1e3), import_zod17.z.number(), import_zod17.z.boolean(), import_zod17.z.null()])
5946
6216
  ).optional(),
5947
- paymentId: import_zod13.z.string().optional(),
5948
- couponCode: import_zod13.z.string().min(1).max(50).transform((value) => value.trim().toUpperCase()).optional(),
5949
- packagePurchaseId: import_zod13.z.string().optional(),
6217
+ paymentId: import_zod17.z.string().optional(),
6218
+ couponCode: import_zod17.z.string().min(1).max(50).transform((value) => value.trim().toUpperCase()).optional(),
6219
+ packagePurchaseId: import_zod17.z.string().optional(),
5950
6220
  // Names the organization, so the calendar and type lookups can run against its own database.
5951
- orgSlug: import_zod13.z.string().min(1).max(100).optional()
6221
+ orgSlug: import_zod17.z.string().min(1).max(100).optional()
5952
6222
  });
5953
6223
 
5954
6224
  // src/booking/finalize-booking.ts
5955
- var import_scheduling50 = require("@ordinatio/scheduling");
6225
+ var import_scheduling54 = require("@ordinatio/scheduling");
5956
6226
  async function finalizeBooking(host, cors, db, organizationId, body, appointment, appointmentType, discountCents, appliedCouponCode, noShowApprovalOverride, clientId) {
5957
6227
  const logActivity = (action, description, metadata) => host.activity.log(db, { action, description, metadata });
5958
6228
  if (noShowApprovalOverride && appointment.status === "CONFIRMED") {
@@ -5995,7 +6265,7 @@ async function finalizeBooking(host, cors, db, organizationId, body, appointment
5995
6265
  AND ("expiresAt" IS NULL OR "expiresAt" > NOW())
5996
6266
  `;
5997
6267
  if (counted === 0) {
5998
- await (0, import_scheduling50.cancelAppointment)(db, {
6268
+ await (0, import_scheduling54.cancelAppointment)(db, {
5999
6269
  appointmentId: appointment.id,
6000
6270
  cancelledBy: "system",
6001
6271
  reason: "Coupon exhausted by concurrent booking during payment"
@@ -6014,7 +6284,7 @@ async function finalizeBooking(host, cors, db, organizationId, body, appointment
6014
6284
  }
6015
6285
  let meetingUrl = null;
6016
6286
  if (appointmentType.locationType === "VIRTUAL") {
6017
- meetingUrl = (0, import_scheduling50.buildJitsiUrl)(appointment.id);
6287
+ meetingUrl = (0, import_scheduling54.buildJitsiUrl)(appointment.id);
6018
6288
  try {
6019
6289
  await db.appointment.update({ where: { id: appointment.id }, data: { meetingUrl } });
6020
6290
  } catch (meetingError) {
@@ -6039,7 +6309,7 @@ async function finalizeBooking(host, cors, db, organizationId, body, appointment
6039
6309
  const baseUrl = host.appUrl();
6040
6310
  if (!baseUrl) throw new Error("The host must supply appUrl() to book appointments");
6041
6311
  try {
6042
- await (0, import_scheduling50.scheduleNotifications)(db, appointment.id, baseUrl);
6312
+ await (0, import_scheduling54.scheduleNotifications)(db, appointment.id, baseUrl, { brand: Boolean(host.brand) });
6043
6313
  } catch (error) {
6044
6314
  await logActivity(
6045
6315
  "scheduling.notification_schedule_failed",
@@ -6048,7 +6318,7 @@ async function finalizeBooking(host, cors, db, organizationId, body, appointment
6048
6318
  );
6049
6319
  }
6050
6320
  try {
6051
- await (0, import_scheduling50.queueStaffNotification)(db, appointment.id, "CONFIRMATION", baseUrl);
6321
+ await (0, import_scheduling54.queueStaffNotification)(db, appointment.id, "CONFIRMATION", baseUrl, { brand: Boolean(host.brand) });
6052
6322
  } catch (error) {
6053
6323
  await logActivity(
6054
6324
  "scheduling.staff_notification_failed",
@@ -6102,7 +6372,7 @@ async function finalizeBooking(host, cors, db, organizationId, body, appointment
6102
6372
  }
6103
6373
 
6104
6374
  // src/booking/refund-blocked-booking.ts
6105
- var import_scheduling51 = require("@ordinatio/scheduling");
6375
+ var import_scheduling55 = require("@ordinatio/scheduling");
6106
6376
  var REFUND_ON_CODES = ["SCHED_301", "SCHED_307", "SCHED_802", "SCHED_800", "SCHED_801", "SCHED_201", "SCHED_100", "SCHED_200", "SCHED_102"];
6107
6377
  function isBlockedBookingCode(code) {
6108
6378
  return code !== void 0 && REFUND_ON_CODES.includes(code);
@@ -6117,12 +6387,12 @@ async function respondToBlockedPaidBooking(db, payments, cors, paymentId, blocke
6117
6387
  try {
6118
6388
  existingBooking = await db.appointment.findFirst({ where: { paymentId }, select: { id: true } });
6119
6389
  } catch (lookupError) {
6120
- const err = (0, import_scheduling51.schedError)("SCHED_409", { paymentId, blockerCode, error: lookupError });
6390
+ const err = (0, import_scheduling55.schedError)("SCHED_409", { paymentId, blockerCode, error: lookupError });
6121
6391
  console.error("[booking] Could not check for an existing booking before refunding:", err.ref, lookupError);
6122
6392
  return corsJson(cors, { error: "We could not confirm your booking just now. Please try again.", ref: err.ref }, { status: 503 });
6123
6393
  }
6124
6394
  if (existingBooking) {
6125
- const err = (0, import_scheduling51.schedError)("SCHED_404", { paymentId, existingAppointmentId: existingBooking.id });
6395
+ const err = (0, import_scheduling55.schedError)("SCHED_404", { paymentId, existingAppointmentId: existingBooking.id });
6126
6396
  return corsJson(
6127
6397
  cors,
6128
6398
  { error: "This payment has already been used for a booking", ref: err.ref, code: "PAYMENT_ALREADY_USED" },
@@ -6249,7 +6519,7 @@ async function resolveBookingContext(host, cors, body) {
6249
6519
  }
6250
6520
 
6251
6521
  // src/booking/verify-booking-payment.ts
6252
- var import_scheduling52 = require("@ordinatio/scheduling");
6522
+ var import_scheduling56 = require("@ordinatio/scheduling");
6253
6523
  async function verifyBookingPayment(db, payments, cors, organizationId, appointmentType, body) {
6254
6524
  const refuse = (status, payload) => ({
6255
6525
  ok: false,
@@ -6266,7 +6536,7 @@ async function verifyBookingPayment(db, payments, cors, organizationId, appointm
6266
6536
  select: { id: true }
6267
6537
  });
6268
6538
  if (existingBooking) {
6269
- const err = (0, import_scheduling52.schedError)("SCHED_404", { paymentId: body.paymentId, existingAppointmentId: existingBooking.id });
6539
+ const err = (0, import_scheduling56.schedError)("SCHED_404", { paymentId: body.paymentId, existingAppointmentId: existingBooking.id });
6270
6540
  return refuse(409, {
6271
6541
  error: "This payment has already been used for a booking",
6272
6542
  ref: err.ref,
@@ -6276,7 +6546,7 @@ async function verifyBookingPayment(db, payments, cors, organizationId, appointm
6276
6546
  const metaTypeId = payment.metadata?.typeId;
6277
6547
  const metaClientEmail = payment.metadata?.clientEmail;
6278
6548
  if (metaTypeId !== body.typeId || metaClientEmail && metaClientEmail.toLowerCase() !== body.clientEmail.toLowerCase()) {
6279
- const err = (0, import_scheduling52.schedError)("SCHED_405", {
6549
+ const err = (0, import_scheduling56.schedError)("SCHED_405", {
6280
6550
  paymentId: body.paymentId,
6281
6551
  metaTypeId,
6282
6552
  bodyTypeId: body.typeId,
@@ -6306,7 +6576,7 @@ async function verifyBookingPayment(db, payments, cors, organizationId, appointm
6306
6576
  }
6307
6577
  }
6308
6578
  if (payment.amount === void 0) {
6309
- const err = (0, import_scheduling52.schedError)("SCHED_405", { paymentId: body.paymentId, currency: payment.currency });
6579
+ const err = (0, import_scheduling56.schedError)("SCHED_405", { paymentId: body.paymentId, currency: payment.currency });
6310
6580
  return refuse(400, { error: "Payment currency is not supported", ref: err.ref });
6311
6581
  }
6312
6582
  if (Math.round(payment.amount * 100) !== expectedCents) {
@@ -6347,7 +6617,7 @@ async function bookAppointmentRoute(request, { host }) {
6347
6617
  } catch (linkError) {
6348
6618
  console.error("[booking] client link lookup failed \u2014 saving the appointment unlinked:", linkError instanceof Error ? linkError.message : linkError);
6349
6619
  }
6350
- const appointment = await (0, import_scheduling53.bookAppointment)(db, {
6620
+ const appointment = await (0, import_scheduling57.bookAppointment)(db, {
6351
6621
  calendarId: body.calendarId,
6352
6622
  preferredCalendarId: body.preferredCalendarId,
6353
6623
  typeId: body.typeId,
@@ -6409,8 +6679,466 @@ async function bookAppointmentRoute(request, { host }) {
6409
6679
  }
6410
6680
  }
6411
6681
 
6682
+ // src/handlers/waitlist-offer.ts
6683
+ async function getWaitlistOfferRoute(request, { host }) {
6684
+ const cors = host.cors(request);
6685
+ if (!host.waitlist) return corsJson(cors, NOT_ENABLED2, { status: 501 });
6686
+ try {
6687
+ const limited = await host.rateLimit(`waitlist-offer:${clientAddress(request)}`, "AVAILABILITY");
6688
+ if (limited) return limited;
6689
+ const url = new URL(request.url);
6690
+ const token = url.searchParams.get("token");
6691
+ if (!token) return corsJson(cors, { error: "token is required" }, { status: 400 });
6692
+ const context = await resolveWaitlistDb(host, url.searchParams.get("orgSlug"));
6693
+ if (!context) return corsJson(cors, { error: "No open offer for this link", code: "SCHED_1106" }, { status: 404 });
6694
+ const offer = await (0, import_scheduling58.getWaitlistOffer)(context.db, token);
6695
+ return corsJson(cors, {
6696
+ typeName: offer.typeName,
6697
+ calendarName: offer.calendarName,
6698
+ startTime: offer.startTime,
6699
+ timezone: offer.timezone,
6700
+ expiresAt: offer.expiresAt,
6701
+ clientName: offer.clientName
6702
+ });
6703
+ } catch (error) {
6704
+ return waitlistErrorResponse(cors, error, "Failed to read the offer");
6705
+ }
6706
+ }
6707
+ var AcceptSchema = import_zod18.z.object({ token: import_zod18.z.string().min(1).max(200), orgSlug: import_zod18.z.string().min(1).max(100).optional() });
6708
+ async function acceptWaitlistOfferRoute(request, context) {
6709
+ const { host } = context;
6710
+ const cors = host.cors(request);
6711
+ const settings = host.waitlist;
6712
+ if (!settings) return corsJson(cors, NOT_ENABLED2, { status: 501 });
6713
+ let entryId = null;
6714
+ let db = null;
6715
+ let booked = false;
6716
+ try {
6717
+ const limited = await host.rateLimit(`waitlist-accept:${clientAddress(request)}`, "GENERAL");
6718
+ if (limited) return limited;
6719
+ const parsed = AcceptSchema.safeParse(await request.json().catch(() => null));
6720
+ if (!parsed.success) return corsJson(cors, { error: "token is required" }, { status: 400 });
6721
+ db = await resolveWaitlistDb(host, parsed.data.orgSlug);
6722
+ if (!db) return corsJson(cors, { error: "No open offer for this link", code: "SCHED_1106" }, { status: 404 });
6723
+ const offer = await (0, import_scheduling58.beginWaitlistAccept)(db.db, parsed.data.token);
6724
+ entryId = offer.entryId;
6725
+ const headers = new Headers(request.headers);
6726
+ headers.set("content-type", "application/json");
6727
+ const response = await bookAppointmentRoute(
6728
+ new Request(request.url, {
6729
+ method: "POST",
6730
+ headers,
6731
+ body: JSON.stringify({
6732
+ calendarId: offer.calendarId,
6733
+ typeId: offer.typeId,
6734
+ startTime: offer.startTime.toISOString(),
6735
+ timezone: offer.timezone,
6736
+ clientName: offer.clientName,
6737
+ clientEmail: offer.clientEmail,
6738
+ ...offer.clientPhone ? { clientPhone: offer.clientPhone } : {},
6739
+ ...parsed.data.orgSlug ? { orgSlug: parsed.data.orgSlug } : {}
6740
+ })
6741
+ }),
6742
+ context
6743
+ );
6744
+ if (response.status === 201) {
6745
+ booked = true;
6746
+ const appointment = await response.clone().json().catch(() => null);
6747
+ try {
6748
+ await (0, import_scheduling58.completeWaitlistAccept)(db.db, entryId, appointment?.id ?? "");
6749
+ } catch (completeError) {
6750
+ console.error("[waitlist] the booking was saved but its waitlist entry could not be marked booked:", completeError);
6751
+ }
6752
+ return response;
6753
+ }
6754
+ const saved = await (0, import_scheduling58.settleBookedAccept)(db.db, entryId);
6755
+ if (saved) return corsJson(cors, saved, { status: 201 });
6756
+ await endFailedAccept(host, settings, db.db, entryId, releaseFor(response.status));
6757
+ return response;
6758
+ } catch (error) {
6759
+ if (entryId && db && !booked) {
6760
+ try {
6761
+ const saved = await (0, import_scheduling58.settleBookedAccept)(db.db, entryId);
6762
+ if (saved) return corsJson(cors, saved, { status: 201 });
6763
+ await endFailedAccept(host, settings, db.db, entryId, "KEEP_OFFER");
6764
+ } catch (releaseError) {
6765
+ console.error("[waitlist] could not settle or release an accept:", releaseError);
6766
+ }
6767
+ }
6768
+ return waitlistErrorResponse(cors, error, "Failed to accept the offer");
6769
+ }
6770
+ }
6771
+ function releaseFor(status) {
6772
+ if (status === 409) return "BACK_IN_LINE";
6773
+ if (status === 429 || status >= 500) return "KEEP_OFFER";
6774
+ return "DROP";
6775
+ }
6776
+ async function endFailedAccept(host, settings, db, entryId, outcome) {
6777
+ const freed = await (0, import_scheduling58.releaseWaitlistAccept)(db, entryId, outcome);
6778
+ if (freed) await offerAndAnnounce(host, settings, db, freed);
6779
+ }
6780
+
6781
+ // src/handlers/admin-waitlist.ts
6782
+ var import_scheduling59 = require("@ordinatio/scheduling");
6783
+ var STATUSES2 = /* @__PURE__ */ new Set(["WAITING", "OFFERED", "ACCEPTING", "BOOKED", "EXPIRED"]);
6784
+ async function listWaitlistEntries(request, { host }) {
6785
+ try {
6786
+ const guard = await requireWaitlistAdmin(host, request, { action: "admin-waitlist-list", tier: "GENERAL" });
6787
+ if (guard instanceof Response) return guard;
6788
+ const status = new URL(request.url).searchParams.get("status");
6789
+ if (status && !STATUSES2.has(status)) {
6790
+ return Response.json({ error: `status must be one of ${[...STATUSES2].join(", ")}` }, { status: 400 });
6791
+ }
6792
+ const { limit, offset } = parsePagination(request);
6793
+ const { entries, total } = await (0, import_scheduling59.listWaitlist)(guard.staff.db, {
6794
+ organizationId: guard.staff.organizationId,
6795
+ status: status ?? void 0,
6796
+ limit,
6797
+ offset
6798
+ });
6799
+ return Response.json({ entries, total, hasMore: offset + entries.length < total });
6800
+ } catch (error) {
6801
+ return webhookErrorResponse(error, "Failed to list the waitlist");
6802
+ }
6803
+ }
6804
+ async function removeWaitlistEntryRoute(request, { host, params }) {
6805
+ try {
6806
+ const guard = await requireWaitlistAdmin(host, request, { action: "admin-waitlist-remove", tier: "DELETE" });
6807
+ if (guard instanceof Response) return guard;
6808
+ const { db, organizationId } = guard.staff;
6809
+ const freed = await (0, import_scheduling59.removeWaitlistEntry)(db, organizationId, params.id);
6810
+ await host.activity.log(db, {
6811
+ action: "scheduling.waitlist_entry_removed",
6812
+ description: "removed someone from the waitlist",
6813
+ metadata: { entryId: params.id }
6814
+ });
6815
+ if (freed) await offerAndAnnounce(host, guard.settings, db, freed);
6816
+ return Response.json({ success: true });
6817
+ } catch (error) {
6818
+ return webhookErrorResponse(error, "Failed to remove the waitlist entry");
6819
+ }
6820
+ }
6821
+
6822
+ // src/handlers/internal-waitlist-sweep.ts
6823
+ var import_scheduling60 = require("@ordinatio/scheduling");
6824
+ async function sweepWaitlist(request, { host }) {
6825
+ if (!await host.internalAccess.isAuthorized(request)) return Response.json({ error: "Unauthorized" }, { status: 401 });
6826
+ if (!host.waitlist) return Response.json(NOT_ENABLED2, { status: 501 });
6827
+ const limited = await host.rateLimit(`${request.headers.get("x-forwarded-for") ?? "worker"}:waitlist-sweep`, "INTERNAL");
6828
+ if (limited) return limited;
6829
+ try {
6830
+ const db = await host.publicDb();
6831
+ const { lapsed, released, booked } = await (0, import_scheduling60.expireWaitlistOffers)(db);
6832
+ let reoffered = 0;
6833
+ for (const slot of lapsed) {
6834
+ if (await offerAndAnnounce(host, host.waitlist, db, slot)) reoffered++;
6835
+ }
6836
+ return Response.json({ lapsed: lapsed.length, released, booked, reoffered });
6837
+ } catch (error) {
6838
+ console.error("[scheduling-api] waitlist sweep failed:", error);
6839
+ return Response.json({ error: "Waitlist sweep failed" }, { status: 500 });
6840
+ }
6841
+ }
6842
+
6843
+ // src/handlers/routing-forms-public.ts
6844
+ var import_scheduling62 = require("@ordinatio/scheduling");
6845
+
6846
+ // src/routing/routing-http.ts
6847
+ var import_scheduling61 = require("@ordinatio/scheduling");
6848
+ var NOT_ENABLED3 = { error: "Routing forms are not enabled on this server" };
6849
+ var STATUS_BY_CODE2 = { SCHED_1201: 404, SCHED_1202: 409, SCHED_1204: 409 };
6850
+ function routingErrorResponse(error, fallback, cors) {
6851
+ const send = (payload, status) => cors ? corsJson(cors, payload, { status }) : Response.json(payload, { status });
6852
+ if (error instanceof import_scheduling61.SchedulingValidationError || error instanceof import_scheduling61.SchedulingNotFoundError) {
6853
+ const details = error.context.errors;
6854
+ const status = STATUS_BY_CODE2[error.code] ?? 400;
6855
+ return send({ error: error.message, code: error.code, ref: getErrorRef(error), ...details ? { details } : {} }, status);
6856
+ }
6857
+ console.error("[routing-forms]", error);
6858
+ return send({ error: fallback }, 500);
6859
+ }
6860
+ async function requireRoutingAdmin(host, request, options) {
6861
+ const staff = await requireAdminStaff(host, request, options);
6862
+ if (staff instanceof Response) return staff;
6863
+ if (!host.routingForms) return Response.json(NOT_ENABLED3, { status: 501 });
6864
+ return staff;
6865
+ }
6866
+
6867
+ // src/handlers/routing-forms-public.ts
6868
+ async function resolveOrganization(host, orgSlug) {
6869
+ if (!orgSlug) return { db: await host.publicDb(), organizationId: null };
6870
+ return host.organizations.resolve({ orgSlug, logPrefix: "[routing-forms]" });
6871
+ }
6872
+ async function getRoutingFormRoute(request, { host }) {
6873
+ const cors = host.cors(request);
6874
+ if (!host.routingForms) return corsJson(cors, NOT_ENABLED3, { status: 501 });
6875
+ try {
6876
+ const limited = await host.rateLimit(`routing-form:${clientAddress(request)}`, "AVAILABILITY");
6877
+ if (limited) return limited;
6878
+ const url = new URL(request.url);
6879
+ const slug = url.searchParams.get("slug");
6880
+ if (!slug) return corsJson(cors, { error: "slug is required" }, { status: 400 });
6881
+ const context = await resolveOrganization(host, url.searchParams.get("orgSlug"));
6882
+ if (!context) return corsJson(cors, { error: "Routing form not found", code: "SCHED_1201" }, { status: 404 });
6883
+ return corsJson(cors, await (0, import_scheduling62.getPublicRoutingForm)(context.db, { organizationId: context.organizationId, slug }));
6884
+ } catch (error) {
6885
+ return routingErrorResponse(error, "Failed to read the form", cors);
6886
+ }
6887
+ }
6888
+ async function routeClientRoute(request, { host }) {
6889
+ const cors = host.cors(request);
6890
+ if (!host.routingForms) return corsJson(cors, NOT_ENABLED3, { status: 501 });
6891
+ try {
6892
+ const limited = await host.rateLimit(`routing-route:${clientAddress(request)}`, "AVAILABILITY");
6893
+ if (limited) return limited;
6894
+ const parsed = RouteClientSchema.safeParse(await request.json().catch(() => null));
6895
+ if (!parsed.success) {
6896
+ const details = parsed.error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message }));
6897
+ return corsJson(cors, { error: "Validation failed", details }, { status: 400 });
6898
+ }
6899
+ const context = await resolveOrganization(host, parsed.data.orgSlug);
6900
+ if (!context) return corsJson(cors, { error: "Routing form not found", code: "SCHED_1201" }, { status: 404 });
6901
+ const outcome = await (0, import_scheduling62.routeClient)(context.db, { organizationId: context.organizationId, slug: parsed.data.slug, answers: parsed.data.answers });
6902
+ return corsJson(cors, outcome);
6903
+ } catch (error) {
6904
+ return routingErrorResponse(error, "Failed to route the answers", cors);
6905
+ }
6906
+ }
6907
+
6908
+ // src/handlers/admin-routing-forms.ts
6909
+ var import_scheduling63 = require("@ordinatio/scheduling");
6910
+ async function listRoutingFormsRoute(request, { host }) {
6911
+ try {
6912
+ const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-list", tier: "GENERAL" });
6913
+ if (staff instanceof Response) return staff;
6914
+ return Response.json({ forms: await (0, import_scheduling63.listRoutingForms)(staff.db, staff.organizationId) });
6915
+ } catch (error) {
6916
+ return routingErrorResponse(error, "Failed to list the forms");
6917
+ }
6918
+ }
6919
+ async function createRoutingFormRoute(request, { host }) {
6920
+ try {
6921
+ const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-create", tier: "CREATE" });
6922
+ if (staff instanceof Response) return staff;
6923
+ const validation = await parseBody(request, CreateRoutingFormSchema);
6924
+ if (!validation.success) return validation.response;
6925
+ const form = await (0, import_scheduling63.createRoutingForm)(staff.db, staff.organizationId, validation.data);
6926
+ await host.activity.log(staff.db, {
6927
+ action: "scheduling.routing_form_created",
6928
+ description: `created routing form "${form.name}"`,
6929
+ metadata: { formId: form.id, slug: form.slug }
6930
+ });
6931
+ return Response.json({ form }, { status: 201 });
6932
+ } catch (error) {
6933
+ return routingErrorResponse(error, "Failed to save the form");
6934
+ }
6935
+ }
6936
+
6937
+ // src/handlers/brand-public.ts
6938
+ var import_scheduling64 = require("@ordinatio/scheduling");
6939
+ async function getBrandRoute(request, { host }) {
6940
+ const cors = host.cors(request);
6941
+ if (!host.brand) return corsJson(cors, NOT_ENABLED, { status: 501 });
6942
+ try {
6943
+ const limited = await host.rateLimit(`brand:${clientAddress(request)}`, "AVAILABILITY");
6944
+ if (limited) return limited;
6945
+ const orgSlug = new URL(request.url).searchParams.get("orgSlug");
6946
+ const context = orgSlug ? await host.organizations.resolve({ orgSlug, logPrefix: "[brand]" }) : { db: await host.publicDb(), organizationId: null };
6947
+ const brand = context ? await (0, import_scheduling64.getPublicBrand)(context.db, context.organizationId) : null;
6948
+ if (!brand) return corsJson(cors, { error: "No brand set" }, { status: 404 });
6949
+ return corsJson(cors, { brand }, { headers: WIDGET_CACHE_HEADERS });
6950
+ } catch (error) {
6951
+ return brandErrorResponse(error, "Failed to read the brand", cors);
6952
+ }
6953
+ }
6954
+
6955
+ // src/handlers/admin-brand.ts
6956
+ var import_scheduling65 = require("@ordinatio/scheduling");
6957
+ async function getBrandAdminRoute(request, { host }) {
6958
+ try {
6959
+ const staff = await requireBrandAdmin(host, request, { action: "admin-brand-get", tier: "GENERAL" });
6960
+ if (staff instanceof Response) return staff;
6961
+ return Response.json({ brand: await (0, import_scheduling65.getBrand)(staff.db, staff.organizationId) });
6962
+ } catch (error) {
6963
+ return brandErrorResponse(error, "Failed to read the brand");
6964
+ }
6965
+ }
6966
+ async function saveBrandRoute(request, { host }) {
6967
+ try {
6968
+ const staff = await requireBrandAdmin(host, request, { action: "admin-brand-save", tier: "UPDATE" });
6969
+ if (staff instanceof Response) return staff;
6970
+ const validation = await parseBody(request, BrandSchema);
6971
+ if (!validation.success) return validation.response;
6972
+ const verifier = host.brand?.isSendingDomainVerified;
6973
+ const brand = await (0, import_scheduling65.saveBrand)(staff.db, staff.organizationId, validation.data, {
6974
+ isSendingDomainVerified: verifier ? async (domain) => Boolean(await verifier.call(host.brand, domain, staff.organizationId)) : void 0
6975
+ });
6976
+ await host.activity.log(staff.db, {
6977
+ action: "scheduling.brand_saved",
6978
+ description: "changed the brand",
6979
+ metadata: { displayName: brand.displayName, sendsFromOwnDomain: Boolean(brand.fromEmail) }
6980
+ });
6981
+ return Response.json({ brand });
6982
+ } catch (error) {
6983
+ return brandErrorResponse(error, "Failed to save the brand");
6984
+ }
6985
+ }
6986
+ async function deleteBrandRoute(request, { host }) {
6987
+ try {
6988
+ const staff = await requireBrandAdmin(host, request, { action: "admin-brand-delete", tier: "DELETE" });
6989
+ if (staff instanceof Response) return staff;
6990
+ const removed = await (0, import_scheduling65.deleteBrand)(staff.db, staff.organizationId);
6991
+ if (removed) await host.activity.log(staff.db, { action: "scheduling.brand_removed", description: "removed the brand", metadata: {} });
6992
+ return Response.json({ removed });
6993
+ } catch (error) {
6994
+ return brandErrorResponse(error, "Failed to remove the brand");
6995
+ }
6996
+ }
6997
+
6998
+ // src/handlers/admin-privacy.ts
6999
+ var import_scheduling67 = require("@ordinatio/scheduling");
7000
+
7001
+ // src/privacy/privacy-http.ts
7002
+ var import_scheduling66 = require("@ordinatio/scheduling");
7003
+ var NOT_ENABLED4 = { error: "Data export and erasure are not enabled on this server" };
7004
+ var STATUS_BY_CODE3 = { SCHED_1300: 400, SCHED_1301: 409 };
7005
+ function privacyErrorResponse(error, fallback) {
7006
+ if (error instanceof import_scheduling66.SchedulingValidationError) {
7007
+ return Response.json(
7008
+ { error: error.message, code: error.code, ref: getErrorRef(error), details: error.context },
7009
+ { status: STATUS_BY_CODE3[error.code] ?? 400 }
7010
+ );
7011
+ }
7012
+ console.error("[privacy]", error);
7013
+ return Response.json({ error: fallback }, { status: 500 });
7014
+ }
7015
+ async function requirePrivacyAdmin(host, request, options) {
7016
+ const staff = await requireAdminStaff(host, request, options);
7017
+ if (staff instanceof Response) return staff;
7018
+ if (!host.privacy) return Response.json(NOT_ENABLED4, { status: 501 });
7019
+ return staff;
7020
+ }
7021
+
7022
+ // src/handlers/admin-privacy.ts
7023
+ async function logQuietly(host, db, entry) {
7024
+ try {
7025
+ await host.activity.log(db, entry);
7026
+ } catch (error) {
7027
+ console.error("[privacy] could not write the activity entry:", error);
7028
+ }
7029
+ }
7030
+ async function exportPrivacyRoute(request, { host }) {
7031
+ try {
7032
+ const staff = await requirePrivacyAdmin(host, request, { action: "admin-privacy-export", tier: "CREATE" });
7033
+ if (staff instanceof Response) return staff;
7034
+ const validation = await parseBody(request, PrivacySubjectSchema);
7035
+ if (!validation.success) return validation.response;
7036
+ const result = await (0, import_scheduling67.exportSubject)(staff.db, { organizationId: staff.organizationId, requestedBy: staff.userId, ...validation.data });
7037
+ await logQuietly(host, staff.db, {
7038
+ action: "scheduling.privacy_export",
7039
+ description: "exported the data held about one person",
7040
+ metadata: { counts: result.counts }
7041
+ });
7042
+ const stamp = result.exportedAt.slice(0, 10);
7043
+ return new Response(JSON.stringify(result, null, 2), {
7044
+ status: 200,
7045
+ headers: {
7046
+ "content-type": "application/json; charset=utf-8",
7047
+ "content-disposition": `attachment; filename="scheduling-data-export-${stamp}.json"`,
7048
+ "cache-control": "no-store"
7049
+ }
7050
+ });
7051
+ } catch (error) {
7052
+ return privacyErrorResponse(error, "Failed to export the data");
7053
+ }
7054
+ }
7055
+ async function erasePrivacyRoute(request, { host }) {
7056
+ try {
7057
+ const staff = await requirePrivacyAdmin(host, request, { action: "admin-privacy-erase", tier: "DELETE" });
7058
+ if (staff instanceof Response) return staff;
7059
+ const validation = await parseBody(request, PrivacyEraseSchema);
7060
+ if (!validation.success) return validation.response;
7061
+ const { confirm, ...subject2 } = validation.data;
7062
+ if (confirm !== true) {
7063
+ const preview = await (0, import_scheduling67.previewErase)(staff.db, { organizationId: staff.organizationId, ...subject2 });
7064
+ return Response.json({ erased: false, preview }, { headers: { "cache-control": "no-store" } });
7065
+ }
7066
+ const { counts } = await (0, import_scheduling67.eraseSubject)(staff.db, { organizationId: staff.organizationId, requestedBy: staff.userId, ...subject2 });
7067
+ await logQuietly(host, staff.db, {
7068
+ action: "scheduling.privacy_erase",
7069
+ description: "erased the data held about one person",
7070
+ metadata: { counts }
7071
+ });
7072
+ return Response.json({ erased: true, counts }, { headers: { "cache-control": "no-store" } });
7073
+ } catch (error) {
7074
+ return privacyErrorResponse(error, "Failed to erase the data");
7075
+ }
7076
+ }
7077
+ async function listPrivacyRequestsRoute(request, { host }) {
7078
+ try {
7079
+ const staff = await requirePrivacyAdmin(host, request, { action: "admin-privacy-requests", tier: "GENERAL" });
7080
+ if (staff instanceof Response) return staff;
7081
+ const { limit, offset } = parsePagination(request);
7082
+ const { requests, total } = await (0, import_scheduling67.listPrivacyRequests)(staff.db, { organizationId: staff.organizationId, limit, offset });
7083
+ return Response.json({ requests, total, hasMore: offset + requests.length < total });
7084
+ } catch (error) {
7085
+ return privacyErrorResponse(error, "Failed to list the requests");
7086
+ }
7087
+ }
7088
+
7089
+ // src/handlers/admin-routing-form-detail.ts
7090
+ var import_scheduling68 = require("@ordinatio/scheduling");
7091
+ async function getRoutingFormAdminRoute(request, { host, params }) {
7092
+ try {
7093
+ const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-get", tier: "GENERAL" });
7094
+ if (staff instanceof Response) return staff;
7095
+ return Response.json({ form: await (0, import_scheduling68.getRoutingForm)(staff.db, staff.organizationId, params.id) });
7096
+ } catch (error) {
7097
+ return routingErrorResponse(error, "Failed to read the form");
7098
+ }
7099
+ }
7100
+ async function updateRoutingFormRoute(request, { host, params }) {
7101
+ try {
7102
+ const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-update", tier: "UPDATE" });
7103
+ if (staff instanceof Response) return staff;
7104
+ const validation = await parseBody(request, UpdateRoutingFormSchema);
7105
+ if (!validation.success) return validation.response;
7106
+ const form = await (0, import_scheduling68.updateRoutingForm)(staff.db, staff.organizationId, params.id, validation.data);
7107
+ await host.activity.log(staff.db, {
7108
+ action: "scheduling.routing_form_updated",
7109
+ description: `changed routing form "${form.name}"`,
7110
+ metadata: { formId: form.id, fields: Object.keys(validation.data) }
7111
+ });
7112
+ return Response.json({ form });
7113
+ } catch (error) {
7114
+ return routingErrorResponse(error, "Failed to save the form");
7115
+ }
7116
+ }
7117
+ async function deleteRoutingFormRoute(request, { host, params }) {
7118
+ try {
7119
+ const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-delete", tier: "DELETE" });
7120
+ if (staff instanceof Response) return staff;
7121
+ await (0, import_scheduling68.deleteRoutingForm)(staff.db, staff.organizationId, params.id);
7122
+ await host.activity.log(staff.db, { action: "scheduling.routing_form_deleted", description: "removed a routing form", metadata: { formId: params.id } });
7123
+ return Response.json({ success: true });
7124
+ } catch (error) {
7125
+ return routingErrorResponse(error, "Failed to remove the form");
7126
+ }
7127
+ }
7128
+ async function testRoutingFormRoute(request, { host, params }) {
7129
+ try {
7130
+ const staff = await requireRoutingAdmin(host, request, { action: "admin-routing-forms-test", tier: "GENERAL" });
7131
+ if (staff instanceof Response) return staff;
7132
+ const validation = await parseBody(request, TestRoutingFormSchema);
7133
+ if (!validation.success) return validation.response;
7134
+ return Response.json(await (0, import_scheduling68.testRoutingForm)(staff.db, { organizationId: staff.organizationId, id: params.id, answers: validation.data.answers }));
7135
+ } catch (error) {
7136
+ return routingErrorResponse(error, "Failed to test the form");
7137
+ }
7138
+ }
7139
+
6412
7140
  // src/handlers/appointments-list.ts
6413
- var import_scheduling54 = require("@ordinatio/scheduling");
7141
+ var import_scheduling69 = require("@ordinatio/scheduling");
6414
7142
  var DEFAULT_LIMIT2 = 50;
6415
7143
  var MAX_LIMIT2 = 500;
6416
7144
  async function listAppointments(request, { host }) {
@@ -6422,7 +7150,7 @@ async function listAppointments(request, { host }) {
6422
7150
  const clientId = params.get("clientId") || void 0;
6423
7151
  const status = params.get("status") || void 0;
6424
7152
  if (!clientId && !staff.isAdmin) {
6425
- const err = (0, import_scheduling54.schedError)("SCHED_900", { role: staff.role });
7153
+ const err = (0, import_scheduling69.schedError)("SCHED_900", { role: staff.role });
6426
7154
  return Response.json({ error: "Forbidden", ref: err.ref }, { status: 403 });
6427
7155
  }
6428
7156
  const startDate = params.get("startDate");
@@ -6502,7 +7230,7 @@ async function listAppointments(request, { host }) {
6502
7230
  }
6503
7231
 
6504
7232
  // src/handlers/availability.ts
6505
- var import_scheduling55 = require("@ordinatio/scheduling");
7233
+ var import_scheduling70 = require("@ordinatio/scheduling");
6506
7234
 
6507
7235
  // src/availability/picked-day.ts
6508
7236
  var CALENDAR_DAY_SHAPE = /^\d{4}-\d{2}-\d{2}$/;
@@ -6615,7 +7343,7 @@ async function getAvailability(request, { host }) {
6615
7343
  const cached = await readCache(host.availabilityCache, calendarId, cacheDay, typeDuration);
6616
7344
  if (cached != null) return corsJson(cors, cached);
6617
7345
  }
6618
- const result = await (0, import_scheduling55.getAvailableSlots)(db, {
7346
+ const result = await (0, import_scheduling70.getAvailableSlots)(db, {
6619
7347
  calendarId,
6620
7348
  date: dateObj,
6621
7349
  localDate: pickedDay,
@@ -6636,7 +7364,7 @@ async function getAvailability(request, { host }) {
6636
7364
  }
6637
7365
 
6638
7366
  // src/handlers/availability-team.ts
6639
- var import_scheduling56 = require("@ordinatio/scheduling");
7367
+ var import_scheduling71 = require("@ordinatio/scheduling");
6640
7368
 
6641
7369
  // src/loaders/team-type.ts
6642
7370
  async function loadTeamType(db, typeId, organizationId) {
@@ -6684,7 +7412,7 @@ async function getTeamAvailability(request, { host }) {
6684
7412
  { status: 400 }
6685
7413
  );
6686
7414
  }
6687
- const result = await (0, import_scheduling56.getTeamAvailableSlots)(db, {
7415
+ const result = await (0, import_scheduling71.getTeamAvailableSlots)(db, {
6688
7416
  typeId,
6689
7417
  date: dateObj,
6690
7418
  localDate: pickedCalendarDay(date),
@@ -6707,7 +7435,7 @@ async function getTeamAvailability(request, { host }) {
6707
7435
  }
6708
7436
 
6709
7437
  // src/handlers/appointment-types.ts
6710
- var import_scheduling57 = require("@ordinatio/scheduling");
7438
+ var import_scheduling72 = require("@ordinatio/scheduling");
6711
7439
 
6712
7440
  // src/loaders/types-by-calendar.ts
6713
7441
  function loadTypesByCalendarSlug(cache, db, organizationId, calendarSlug) {
@@ -6781,19 +7509,19 @@ async function getAppointmentTypes(request, { host }) {
6781
7509
  }));
6782
7510
  return corsJson(cors, response, { headers: WIDGET_CACHE_HEADERS });
6783
7511
  } catch (error) {
6784
- const diagnostic = (0, import_scheduling57.schedError)("SCHED_202", { error });
7512
+ const diagnostic = (0, import_scheduling72.schedError)("SCHED_202", { error });
6785
7513
  console.error("[appointment-types]", diagnostic.ref, error);
6786
7514
  return corsJson(cors, { error: "Failed to fetch appointment types", ref: diagnostic.ref }, { status: 500 });
6787
7515
  }
6788
7516
  }
6789
7517
 
6790
7518
  // src/handlers/coupons-validate.ts
6791
- var import_zod14 = require("zod");
6792
- var import_scheduling58 = require("@ordinatio/scheduling");
6793
- var Schema4 = import_zod14.z.object({
6794
- code: import_zod14.z.string().min(1).max(50).transform((v) => v.trim().toUpperCase()),
6795
- calendarId: import_zod14.z.string().min(1),
6796
- price: import_zod14.z.number().positive()
7519
+ var import_zod19 = require("zod");
7520
+ var import_scheduling73 = require("@ordinatio/scheduling");
7521
+ var Schema4 = import_zod19.z.object({
7522
+ code: import_zod19.z.string().min(1).max(50).transform((v) => v.trim().toUpperCase()),
7523
+ calendarId: import_zod19.z.string().min(1),
7524
+ price: import_zod19.z.number().positive()
6797
7525
  });
6798
7526
  function calculateDiscount(price, discountType, discountValue) {
6799
7527
  if (discountType === "PERCENTAGE") {
@@ -6851,7 +7579,7 @@ async function validateCoupon(request, { host }) {
6851
7579
  { headers: host.cors(request) }
6852
7580
  );
6853
7581
  } catch (error) {
6854
- const err = (0, import_scheduling58.schedError)("SCHED_408", {
7582
+ const err = (0, import_scheduling73.schedError)("SCHED_408", {
6855
7583
  calendarId,
6856
7584
  error: error instanceof Error ? error.message : String(error)
6857
7585
  });
@@ -6906,7 +7634,7 @@ async function getPublicCalendar(request, { host }) {
6906
7634
  }
6907
7635
 
6908
7636
  // src/handlers/type.ts
6909
- var import_scheduling59 = require("@ordinatio/scheduling");
7637
+ var import_scheduling74 = require("@ordinatio/scheduling");
6910
7638
 
6911
7639
  // src/loaders/type-by-slug.ts
6912
7640
  function loadTypeBySlug(cache, db, organizationId, slug) {
@@ -7011,14 +7739,14 @@ async function getType(request, { host }) {
7011
7739
  { headers: WIDGET_CACHE_HEADERS }
7012
7740
  );
7013
7741
  } catch (error) {
7014
- const diagnostic = (0, import_scheduling59.schedError)("SCHED_207", { error });
7742
+ const diagnostic = (0, import_scheduling74.schedError)("SCHED_207", { error });
7015
7743
  console.error("[type]", diagnostic.ref, error);
7016
7744
  return corsJson(cors, { error: "Failed to fetch appointment type", ref: diagnostic.ref }, { status: 500 });
7017
7745
  }
7018
7746
  }
7019
7747
 
7020
7748
  // src/handlers/widget-init.ts
7021
- var import_scheduling60 = require("@ordinatio/scheduling");
7749
+ var import_scheduling75 = require("@ordinatio/scheduling");
7022
7750
 
7023
7751
  // src/loaders/widget-data.ts
7024
7752
  function loadWidgetData(cache, db, organizationId, calendarSlug) {
@@ -7107,7 +7835,7 @@ async function getWidgetInit(request, { host }) {
7107
7835
  }));
7108
7836
  return corsJson(cors, { calendar, types: sanitizedTypes }, { headers: WIDGET_CACHE_HEADERS });
7109
7837
  } catch (error) {
7110
- const diagnostic = (0, import_scheduling60.schedError)("SCHED_206", { error });
7838
+ const diagnostic = (0, import_scheduling75.schedError)("SCHED_206", { error });
7111
7839
  console.error("[widget-init]", diagnostic.ref, error);
7112
7840
  return corsJson(cors, { error: "Failed to load widget data", ref: diagnostic.ref }, { status: 500 });
7113
7841
  }
@@ -7124,6 +7852,13 @@ var ROUTES = [
7124
7852
  { method: "GET", path: "/availability", public: true, handler: getAvailability },
7125
7853
  { method: "GET", path: "/availability/team", public: true, handler: getTeamAvailability },
7126
7854
  { method: "POST", path: "/appointments", public: true, handler: bookAppointmentRoute },
7855
+ { method: "GET", path: "/routing-forms", public: true, handler: getRoutingFormRoute },
7856
+ { method: "POST", path: "/routing-forms/route", public: true, handler: routeClientRoute },
7857
+ { method: "GET", path: "/brand", public: true, handler: getBrandRoute },
7858
+ { method: "POST", path: "/waitlist", public: true, handler: joinWaitlistRoute },
7859
+ { method: "DELETE", path: "/waitlist", public: true, handler: leaveWaitlistRoute },
7860
+ { method: "GET", path: "/waitlist/offer", public: true, handler: getWaitlistOfferRoute },
7861
+ { method: "POST", path: "/waitlist/offer/accept", public: true, handler: acceptWaitlistOfferRoute },
7127
7862
  { method: "GET", path: "/appointments", public: false, handler: listAppointments },
7128
7863
  { method: "GET", path: "/appointments/capacity", public: true, handler: getCapacity },
7129
7864
  { method: "GET", path: "/appointments/ical", public: false, handler: getAppointmentIcal },
@@ -7165,6 +7900,21 @@ var ROUTES = [
7165
7900
  { method: "GET", path: "/admin/webhooks/[id]/deliveries", public: false, handler: listWebhookEndpointDeliveries },
7166
7901
  { method: "POST", path: "/admin/webhook-deliveries/[id]/retry", public: false, handler: retryWebhook },
7167
7902
  { method: "POST", path: "/internal/webhooks/deliver", public: false, handler: deliverWebhooks },
7903
+ { method: "GET", path: "/admin/routing-forms", public: false, handler: listRoutingFormsRoute },
7904
+ { method: "POST", path: "/admin/routing-forms", public: false, handler: createRoutingFormRoute },
7905
+ { method: "GET", path: "/admin/routing-forms/[id]", public: false, handler: getRoutingFormAdminRoute },
7906
+ { method: "PUT", path: "/admin/routing-forms/[id]", public: false, handler: updateRoutingFormRoute },
7907
+ { method: "DELETE", path: "/admin/routing-forms/[id]", public: false, handler: deleteRoutingFormRoute },
7908
+ { method: "POST", path: "/admin/routing-forms/[id]/test", public: false, handler: testRoutingFormRoute },
7909
+ { method: "GET", path: "/admin/brand", public: false, handler: getBrandAdminRoute },
7910
+ { method: "PUT", path: "/admin/brand", public: false, handler: saveBrandRoute },
7911
+ { method: "DELETE", path: "/admin/brand", public: false, handler: deleteBrandRoute },
7912
+ { method: "POST", path: "/admin/privacy/export", public: false, handler: exportPrivacyRoute },
7913
+ { method: "POST", path: "/admin/privacy/erase", public: false, handler: erasePrivacyRoute },
7914
+ { method: "GET", path: "/admin/privacy/requests", public: false, handler: listPrivacyRequestsRoute },
7915
+ { method: "GET", path: "/admin/waitlist", public: false, handler: listWaitlistEntries },
7916
+ { method: "DELETE", path: "/admin/waitlist/[id]", public: false, handler: removeWaitlistEntryRoute },
7917
+ { method: "POST", path: "/internal/waitlist/sweep", public: false, handler: sweepWaitlist },
7168
7918
  { method: "GET", path: "/analytics", public: false, handler: getAnalytics },
7169
7919
  { method: "GET", path: "/admin/types", public: false, handler: listTypes },
7170
7920
  { method: "POST", path: "/admin/types", public: false, handler: createType },
@@ -7243,6 +7993,11 @@ function createSchedulingApi(config) {
7243
7993
  tenantDb: config.host.tenantDb?.bind(config.host) ?? (() => config.host.publicDb()),
7244
7994
  events: { emit: async () => void 0 },
7245
7995
  webhooks: config.host.webhooks ?? null,
7996
+ waitlist: config.host.waitlist ?? null,
7997
+ routingForms: config.host.routingForms ?? null,
7998
+ privacy: config.host.privacy ?? null,
7999
+ brand: config.host.brand ?? null,
8000
+ icalUidDomain: config.host.icalUidDomain?.bind(config.host) ?? (() => null),
7246
8001
  calendarSync: config.host.calendarSync ?? { upsertEvent: async () => ({}), removeEvent: async () => void 0 },
7247
8002
  activity: config.host.activity ?? { log: async () => void 0, isOpen: async () => false, resolve: async () => void 0 },
7248
8003
  appUrl: config.host.appUrl?.bind(config.host) ?? (() => null),
@@ -7268,7 +8023,7 @@ function createSchedulingApi(config) {
7268
8023
  jsonNull: config.host.jsonNull?.bind(config.host) ?? (() => null),
7269
8024
  cors: config.host.cors?.bind(config.host) ?? defaultCorsHeaders
7270
8025
  };
7271
- host.events = withWebhooks(config.host.events ?? { emit: async () => void 0 }, host);
8026
+ host.events = withWebhooks(withWaitlist(config.host.events ?? { emit: async () => void 0 }, host), host);
7272
8027
  async function handle(request) {
7273
8028
  const { pathname } = new URL(request.url);
7274
8029
  if (pathname !== basePath && !pathname.startsWith(`${basePath}/`)) return notFound();
@@ -7423,6 +8178,7 @@ function createNodeListener(api, options) {
7423
8178
  BalanceRequestInProgressError,
7424
8179
  BookingPageConfigSchema,
7425
8180
  BookingPageThemeSchema,
8181
+ BrandSchema,
7426
8182
  CancelRecurringScopeSchema,
7427
8183
  CancelRecurringSeriesSchema,
7428
8184
  CreateAppointmentCategorySchema,
@@ -7433,6 +8189,7 @@ function createNodeListener(api, options) {
7433
8189
  CreateNotificationTemplateSchema,
7434
8190
  CreatePackagePurchaseSchema,
7435
8191
  CreateRecurringSeriesSchema,
8192
+ CreateRoutingFormSchema,
7436
8193
  CreateServicePackageSchema,
7437
8194
  CreateWebhookEndpointSchema,
7438
8195
  DEFAULT_CANCELLATION_POLICY,
@@ -7441,13 +8198,17 @@ function createNodeListener(api, options) {
7441
8198
  NotificationChannelSchema,
7442
8199
  NotificationTriggerSchema,
7443
8200
  OrgCancellationPolicySchema,
8201
+ PrivacyEraseSchema,
8202
+ PrivacySubjectSchema,
7444
8203
  RETRYABLE_REFUND_STATUSES,
7445
8204
  RecurringFrequencySchema,
7446
8205
  RefundOutcomePersistenceError,
8206
+ RouteClientSchema,
7447
8207
  SLOT_GRANULARITY_DEFAULT,
7448
8208
  SLOT_GRANULARITY_OPTIONS,
7449
8209
  TERMINAL_SUCCESS_STATUSES,
7450
8210
  TOKEN_TTL_DAYS,
8211
+ TestRoutingFormSchema,
7451
8212
  UnverifiedPaymentError,
7452
8213
  UpdateAppointmentCategorySchema,
7453
8214
  UpdateAppointmentTypeSchema,
@@ -7456,6 +8217,7 @@ function createNodeListener(api, options) {
7456
8217
  UpdateCouponCodeSchema,
7457
8218
  UpdateNotificationTemplateSchema,
7458
8219
  UpdatePackagePurchaseSchema,
8220
+ UpdateRoutingFormSchema,
7459
8221
  UpdateServicePackageSchema,
7460
8222
  attemptLegRefund,
7461
8223
  createBalanceIntent,