@ordinatio/scheduling-api 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +87 -1
- package/dist/{chunk-IAQBLF5F.mjs → chunk-MQO3VRMU.mjs} +59 -1
- package/dist/index.d.mts +63 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.js +991 -229
- package/dist/index.mjs +776 -59
- package/dist/schemas/index.d.mts +55 -1
- package/dist/schemas/index.d.ts +55 -1
- package/dist/schemas/index.js +65 -0
- package/dist/schemas/index.mjs +15 -1
- package/package.json +3 -3
package/dist/index.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
|
|
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
|
|
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
|
|
450
|
+
return import_scheduling3.SCHEDULING_ERRORS[code]?.httpStatus ?? 400;
|
|
365
451
|
}
|
|
366
452
|
|
|
367
453
|
// src/manage/cancel-via-token.ts
|
|
368
|
-
var
|
|
454
|
+
var import_scheduling5 = require("@ordinatio/scheduling");
|
|
369
455
|
|
|
370
456
|
// src/calendar-sync.ts
|
|
371
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
539
|
+
await (0, import_scheduling5.queueStaffNotification)(db, appointment.id, "CANCELLATION", baseUrl, { brand: Boolean(host.brand) });
|
|
454
540
|
} catch (staffError) {
|
|
455
|
-
const diagnostic = (0,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
668
|
+
var import_scheduling9 = require("@ordinatio/scheduling");
|
|
583
669
|
|
|
584
670
|
// src/manage/reschedule-via-token.ts
|
|
585
|
-
var
|
|
671
|
+
var import_scheduling8 = require("@ordinatio/scheduling");
|
|
586
672
|
async function rescheduleViaToken(host, db, organizationId, appointment, newTime) {
|
|
587
|
-
const result = await (0,
|
|
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,
|
|
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,
|
|
703
|
+
await (0, import_scheduling8.scheduleNotifications)(db, appointment.id, baseUrl, { excludeTriggers: ["CONFIRMATION"], brand: Boolean(host.brand) });
|
|
618
704
|
} catch (error) {
|
|
619
|
-
const diagnostic = (0,
|
|
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,
|
|
710
|
+
await (0, import_scheduling8.queueStaffNotification)(db, appointment.id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
|
|
625
711
|
} catch (staffError) {
|
|
626
|
-
const diagnostic = (0,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
1569
|
+
await (0, import_scheduling16.queueImmediateNotification)(db, id, "RESCHEDULE", baseUrl, { brand: Boolean(host.brand) });
|
|
1484
1570
|
try {
|
|
1485
|
-
await (0,
|
|
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,
|
|
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
|
|
1529
|
-
if (error instanceof
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
2839
|
+
var import_scheduling27 = require("@ordinatio/scheduling");
|
|
2754
2840
|
function refundFailureResponse({ error, id, stripeMayHaveMoved }) {
|
|
2755
|
-
const diagnostic = (0,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
3916
|
+
var import_scheduling31 = require("@ordinatio/scheduling");
|
|
3780
3917
|
|
|
3781
3918
|
// src/recurring/series-side-effects.ts
|
|
3782
|
-
var
|
|
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,
|
|
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
|
-
|
|
3860
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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 >
|
|
3954
|
-
const err = (0,
|
|
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,
|
|
3958
|
-
const err = (0,
|
|
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,
|
|
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
|
|
4204
|
+
var import_scheduling33 = require("@ordinatio/scheduling");
|
|
4069
4205
|
|
|
4070
4206
|
// src/recurring/series-cancel-side-effects.ts
|
|
4071
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
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 ${
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
5171
|
+
var import_scheduling41 = require("@ordinatio/scheduling");
|
|
5005
5172
|
|
|
5006
5173
|
// src/availability/override-helpers.ts
|
|
5007
|
-
var
|
|
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,
|
|
5015
|
-
const blockEnd = (0,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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}
|
|
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,
|
|
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,
|
|
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
|
|
5943
|
+
var import_scheduling48 = require("@ordinatio/scheduling");
|
|
5777
5944
|
|
|
5778
5945
|
// src/webhooks/require-webhooks.ts
|
|
5779
|
-
var
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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/
|
|
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
|
|
5930
|
-
var BookingSchema =
|
|
5931
|
-
calendarId:
|
|
5932
|
-
preferredCalendarId:
|
|
5933
|
-
typeId:
|
|
5934
|
-
startTime:
|
|
5935
|
-
timezone:
|
|
5936
|
-
clientName:
|
|
5937
|
-
clientEmail:
|
|
5938
|
-
clientPhone:
|
|
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:
|
|
5941
|
-
notes:
|
|
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:
|
|
5944
|
-
|
|
5945
|
-
|
|
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:
|
|
5948
|
-
couponCode:
|
|
5949
|
-
packagePurchaseId:
|
|
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:
|
|
6221
|
+
orgSlug: import_zod17.z.string().min(1).max(100).optional()
|
|
5952
6222
|
});
|
|
5953
6223
|
|
|
5954
6224
|
// src/booking/finalize-booking.ts
|
|
5955
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
6792
|
-
var
|
|
6793
|
-
var Schema4 =
|
|
6794
|
-
code:
|
|
6795
|
-
calendarId:
|
|
6796
|
-
price:
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|