riverbankcms 0.2.0 → 0.4.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.
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { z } from "zod";
3
-
4
2
  //#region ../core/src/result.ts
5
3
  function ok(value) {
6
4
  return {
@@ -46,6 +44,7 @@ const unsafeAsEntryId = (id) => id;
46
44
  const unsafeAsPreviewEntryId = (id) => id;
47
45
  const unsafeAsEventInstanceId = (id) => id;
48
46
  const unsafeAsSiteId = (id) => id;
47
+ const unsafeAsApiKeyId = (id) => id;
49
48
  const unsafeAsSiteRouteId = (id) => id;
50
49
  const unsafeAsPageId = (id) => id;
51
50
  const unsafeAsPageSurfaceId = (id) => id;
@@ -155,6 +154,7 @@ const unsafeAsEncounterRecordId = (id) => id;
155
154
  const unsafeAsRecordEntryId = (id) => id;
156
155
  const unsafeAsProtectedPayloadKeyVersion = (id) => id;
157
156
  const parseSiteId = (value) => parseUuidBrand(value, unsafeAsSiteId);
157
+ const parseApiKeyId = (value) => parseUuidBrand(value, unsafeAsApiKeyId);
158
158
  const parseSiteRouteId = (value) => parseUuidBrand(value, unsafeAsSiteRouteId);
159
159
  const parsePageId = (value) => parseUuidBrand(value, unsafeAsPageId);
160
160
  const parsePageSurfaceId = (value) => parseUuidBrand(value, unsafeAsPageSurfaceId);
@@ -609,6 +609,7 @@ function formatCoursePriceDisplayLabel(display, options) {
609
609
  case "exact": return `Course (${formatCurrencyAmount(display.amount, display.currency, options)})`;
610
610
  case "from": return `Course (From ${formatCurrencyAmount(display.amount, display.currency, options)})`;
611
611
  case "range": return `Course (${formatCurrencyAmount(display.minAmount, display.currency, options)} - ${formatCurrencyAmount(display.maxAmount, display.currency, options)})`;
612
+ case "mixed": return display.paidToAmount > display.paidFromAmount ? `Course (Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)} - ${formatCurrencyAmount(display.paidToAmount, display.currency, options)})` : `Course (Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)})`;
612
613
  default: return assertNever(display);
613
614
  }
614
615
  }
@@ -639,18 +640,36 @@ function formatEventPricing(pricing, options = {}) {
639
640
 
640
641
  //#endregion
641
642
  //#region ../core/src/event-page-surface-subject.ts
643
+ function createEventSeriesPageSurfaceSubject(eventSeriesId, bookingScope) {
644
+ return bookingScope === "whole_series" ? {
645
+ kind: "eventSeries",
646
+ eventSeriesId,
647
+ bookingScope,
648
+ isWholeSeriesBooking: true
649
+ } : {
650
+ kind: "eventSeries",
651
+ eventSeriesId,
652
+ bookingScope
653
+ };
654
+ }
642
655
  function parseEventPageSurfaceSubject(value) {
643
- if (!isRecord$8(value)) return err({ kind: "notObject" });
656
+ if (!isRecord$6(value)) return err({ kind: "notObject" });
644
657
  switch (value.kind) {
645
658
  case "eventSeries": {
646
659
  const eventSeriesId = parseEventSeriesId(value.eventSeriesId);
647
- return eventSeriesId.ok ? ok({
648
- kind: "eventSeries",
649
- eventSeriesId: eventSeriesId.value
650
- }) : err({
651
- kind: "invalidEventSeriesId",
652
- error: eventSeriesId.error
653
- });
660
+ if (!eventSeriesId.ok) {
661
+ return err({
662
+ kind: "invalidEventSeriesId",
663
+ error: eventSeriesId.error
664
+ });
665
+ }
666
+ if (value.bookingScope !== "single_occurrence" && value.bookingScope !== "whole_series") {
667
+ return err({
668
+ kind: "invalidEventSeriesBookingScope",
669
+ bookingScope: value.bookingScope
670
+ });
671
+ }
672
+ return ok(createEventSeriesPageSurfaceSubject(eventSeriesId.value, value.bookingScope));
654
673
  }
655
674
  case "eventOffering": {
656
675
  const eventOfferingId = parseEventOfferingId(value.eventOfferingId);
@@ -692,7 +711,7 @@ function parseEventPageSurfaceSubject(value) {
692
711
  function toEventPageSurfaceSubjectWire(subject) {
693
712
  return subject;
694
713
  }
695
- function isRecord$8(value) {
714
+ function isRecord$6(value) {
696
715
  return typeof value === "object" && value !== null && !Array.isArray(value);
697
716
  }
698
717
 
@@ -2233,7 +2252,7 @@ function reconcileParticipantRoster(input) {
2233
2252
  }
2234
2253
  function planParticipantSubjectResolution(input) {
2235
2254
  const existingParticipantIdByEmail = new Map(input.existingParticipantsByEmail.map((participant) => [participant.email, participant.participantId]));
2236
- const duplicateSlotKeysByEmail = new Map(input.projection.issues.filter((issue$3) => issue$3.kind === "duplicate_email_in_booking_context").map((issue$3) => [issue$3.email, issue$3.slotKeys]));
2255
+ const duplicateSlotKeysByEmail = new Map(input.projection.issues.filter((issue) => issue.kind === "duplicate_email_in_booking_context").map((issue) => [issue.email, issue.slotKeys]));
2237
2256
  return input.projection.slots.map((slot) => {
2238
2257
  if (slot.email === null) {
2239
2258
  return {
@@ -2699,7 +2718,7 @@ const knownSocialPlatforms = [
2699
2718
  "x",
2700
2719
  "youtube"
2701
2720
  ];
2702
- function isRecord$7(value) {
2721
+ function isRecord$5(value) {
2703
2722
  return typeof value === "object" && value !== null && !Array.isArray(value);
2704
2723
  }
2705
2724
  function trimmedOptionalString(value) {
@@ -2709,7 +2728,7 @@ function trimmedOptionalString(value) {
2709
2728
  return trimmed.length > 0 ? trimmed : null;
2710
2729
  }
2711
2730
  function parsePublicContactDetails(value) {
2712
- if (!isRecord$7(value)) {
2731
+ if (!isRecord$5(value)) {
2713
2732
  return err({
2714
2733
  code: "invalid_shape",
2715
2734
  path: "contact",
@@ -2805,7 +2824,7 @@ function minutesSinceMidnight(value) {
2805
2824
  }
2806
2825
  function parseOpeningHoursDay(value, expectedWeekday, index) {
2807
2826
  const path = `openingHours.days.${index}`;
2808
- if (!isRecord$7(value) || value.weekday !== expectedWeekday) {
2827
+ if (!isRecord$5(value) || value.weekday !== expectedWeekday) {
2809
2828
  return err({
2810
2829
  code: "invalid_weekdays",
2811
2830
  path,
@@ -2826,7 +2845,7 @@ function parseOpeningHoursDay(value, expectedWeekday, index) {
2826
2845
  const intervals = [];
2827
2846
  for (const [intervalIndex, rawInterval] of value.intervals.entries()) {
2828
2847
  const intervalPath = `${path}.intervals.${intervalIndex}`;
2829
- if (!isRecord$7(rawInterval)) {
2848
+ if (!isRecord$5(rawInterval)) {
2830
2849
  return err({
2831
2850
  code: "invalid_interval",
2832
2851
  path: intervalPath,
@@ -2876,7 +2895,7 @@ function parseOpeningHoursDay(value, expectedWeekday, index) {
2876
2895
  });
2877
2896
  }
2878
2897
  function parseWeeklyOpeningHours(value) {
2879
- if (!isRecord$7(value) || !Array.isArray(value.days) || value.days.length !== publicProfileWeekdays.length) {
2898
+ if (!isRecord$5(value) || !Array.isArray(value.days) || value.days.length !== publicProfileWeekdays.length) {
2880
2899
  return err({
2881
2900
  code: "invalid_weekdays",
2882
2901
  path: "openingHours.days",
@@ -2956,7 +2975,7 @@ function parsePublicSocialLinks(value) {
2956
2975
  const ids = new Set();
2957
2976
  for (const [index, rawLink] of value.entries()) {
2958
2977
  const path = `socialLinks.${index}`;
2959
- if (!isRecord$7(rawLink)) {
2978
+ if (!isRecord$5(rawLink)) {
2960
2979
  return err({
2961
2980
  code: "invalid_social_link",
2962
2981
  path,
@@ -3003,7 +3022,7 @@ function parsePublicSocialLinks(value) {
3003
3022
  return ok(links);
3004
3023
  }
3005
3024
  function parsePublicProfile(value) {
3006
- if (!isRecord$7(value)) {
3025
+ if (!isRecord$5(value)) {
3007
3026
  return err({
3008
3027
  code: "invalid_shape",
3009
3028
  path: "",
@@ -3032,7 +3051,7 @@ function emptyPublicProfile() {
3032
3051
 
3033
3052
  //#endregion
3034
3053
  //#region ../core/src/rich-text.ts
3035
- function isRecord$6(value) {
3054
+ function isRecord$4(value) {
3036
3055
  return typeof value === "object" && value !== null && !Array.isArray(value);
3037
3056
  }
3038
3057
  function hasOnlyKeys(value, allowed) {
@@ -3054,10 +3073,10 @@ function isDenseArrayOf(value, predicate) {
3054
3073
  return true;
3055
3074
  }
3056
3075
  function isTipTapMark(value) {
3057
- return isRecord$6(value) && hasOnlyKeys(value, markKeys) && typeof value.type === "string" && value.type.length > 0 && (value.attrs === undefined || isRecord$6(value.attrs));
3076
+ return isRecord$4(value) && hasOnlyKeys(value, markKeys) && typeof value.type === "string" && value.type.length > 0 && (value.attrs === undefined || isRecord$4(value.attrs));
3058
3077
  }
3059
3078
  function isTipTapNode(value) {
3060
- if (!isRecord$6(value) || !hasOnlyKeys(value, nodeKeys) || typeof value.type !== "string" || value.type.length === 0 || value.type === "doc" || value.attrs !== undefined && !isRecord$6(value.attrs) || value.marks !== undefined && (!Array.isArray(value.marks) || !isDenseArrayOf(value.marks, isTipTapMark)) || value.content !== undefined && (!Array.isArray(value.content) || !isDenseArrayOf(value.content, isTipTapNode))) {
3079
+ if (!isRecord$4(value) || !hasOnlyKeys(value, nodeKeys) || typeof value.type !== "string" || value.type.length === 0 || value.type === "doc" || value.attrs !== undefined && !isRecord$4(value.attrs) || value.marks !== undefined && (!Array.isArray(value.marks) || !isDenseArrayOf(value.marks, isTipTapMark)) || value.content !== undefined && (!Array.isArray(value.content) || !isDenseArrayOf(value.content, isTipTapNode))) {
3061
3080
  return false;
3062
3081
  }
3063
3082
  if (value.type === "text") {
@@ -3066,7 +3085,7 @@ function isTipTapNode(value) {
3066
3085
  return value.text === undefined;
3067
3086
  }
3068
3087
  function isCanonicalRichTextDoc(value) {
3069
- return isRecord$6(value) && hasOnlyKeys(value, rootKeys) && value.type === "doc" && Array.isArray(value.content) && isDenseArrayOf(value.content, isTipTapNode);
3088
+ return isRecord$4(value) && hasOnlyKeys(value, rootKeys) && value.type === "doc" && Array.isArray(value.content) && isDenseArrayOf(value.content, isTipTapNode);
3070
3089
  }
3071
3090
 
3072
3091
  //#endregion
@@ -3074,7 +3093,7 @@ function isCanonicalRichTextDoc(value) {
3074
3093
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
3075
3094
  const MAILCHIMP_AUDIENCE_ID_PATTERN = /^[A-Za-z0-9_-]{1,100}$/u;
3076
3095
  const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
3077
- function isRecord$5(value) {
3096
+ function isRecord$3(value) {
3078
3097
  return typeof value === "object" && value !== null && !Array.isArray(value);
3079
3098
  }
3080
3099
  function parseTrimmedString(value) {
@@ -3102,7 +3121,7 @@ function parseNewsletterSignupDestinationProvider(value) {
3102
3121
  return value === "riverbank" || value === "mailchimp" ? value : null;
3103
3122
  }
3104
3123
  function parseNewsletterSignupAuthoringDestinationValue(value) {
3105
- if (!isRecord$5(value) || typeof value.tag !== "string") {
3124
+ if (!isRecord$3(value) || typeof value.tag !== "string") {
3106
3125
  return err({ kind: "invalid-destination-shape" });
3107
3126
  }
3108
3127
  switch (value.tag) {
@@ -3148,7 +3167,7 @@ function parseNewsletterSignupAuthoringDestinationValue(value) {
3148
3167
 
3149
3168
  //#endregion
3150
3169
  //#region ../core/src/footer-engagement.ts
3151
- function isRecord$4(value) {
3170
+ function isRecord$2(value) {
3152
3171
  return typeof value === "object" && value !== null && !Array.isArray(value);
3153
3172
  }
3154
3173
  function invalid(path, message) {
@@ -3182,7 +3201,7 @@ function parseEngagementBase(value, base, path) {
3182
3201
  }
3183
3202
  const treatment = parseTreatment(value.treatment, `${path}.treatment`);
3184
3203
  if (!treatment.ok) return treatment;
3185
- if (!isRecord$4(value.source)) {
3204
+ if (!isRecord$2(value.source)) {
3186
3205
  return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
3187
3206
  }
3188
3207
  const intro = parseBoundedText(value.source.intro, `${path}.source.intro`, {
@@ -3248,7 +3267,7 @@ function parseFooterEngagementSectionValue(value, sectionBase, path) {
3248
3267
  const base = parseEngagementBase(value, sectionBase, path);
3249
3268
  if (!base.ok) return base;
3250
3269
  const source = value.source;
3251
- if (!isRecord$4(source)) {
3270
+ if (!isRecord$2(source)) {
3252
3271
  return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
3253
3272
  }
3254
3273
  switch (source.kind) {
@@ -3363,7 +3382,7 @@ function unsafeAsFooterModuleId(value) {
3363
3382
  function parseFooterModuleId(value) {
3364
3383
  return parseSectionId(value, "sectionId");
3365
3384
  }
3366
- function isRecord$3(value) {
3385
+ function isRecord$1(value) {
3367
3386
  return typeof value === "object" && value !== null && !Array.isArray(value);
3368
3387
  }
3369
3388
  function isRichText(value) {
@@ -3393,7 +3412,7 @@ function parseHeading(value, path) {
3393
3412
  }
3394
3413
  function parseFooterSection(value, index) {
3395
3414
  const path = `sections.${index}`;
3396
- if (!isRecord$3(value)) {
3415
+ if (!isRecord$1(value)) {
3397
3416
  return err({
3398
3417
  code: "invalid_section",
3399
3418
  path,
@@ -3428,7 +3447,7 @@ function parseFooterSection(value, index) {
3428
3447
  }
3429
3448
  break;
3430
3449
  case "navigation":
3431
- if (isRecord$3(value.source) && value.source.kind === "menu-role" && value.source.role === "main") {
3450
+ if (isRecord$1(value.source) && value.source.kind === "menu-role" && value.source.role === "main") {
3432
3451
  return ok({
3433
3452
  ...base,
3434
3453
  kind: "navigation",
@@ -3441,7 +3460,7 @@ function parseFooterSection(value, index) {
3441
3460
  }
3442
3461
  break;
3443
3462
  case "legal-navigation":
3444
- if (isRecord$3(value.source) && value.source.kind === "menu-role" && value.source.role === "legal-policy") {
3463
+ if (isRecord$1(value.source) && value.source.kind === "menu-role" && value.source.role === "legal-policy") {
3445
3464
  return ok({
3446
3465
  ...base,
3447
3466
  kind: "legal-navigation",
@@ -3454,7 +3473,7 @@ function parseFooterSection(value, index) {
3454
3473
  }
3455
3474
  break;
3456
3475
  case "business-address":
3457
- if (isRecord$3(value.source) && value.source.kind === "business-address") {
3476
+ if (isRecord$1(value.source) && value.source.kind === "business-address") {
3458
3477
  return ok({
3459
3478
  ...base,
3460
3479
  kind: "business-address",
@@ -3466,7 +3485,7 @@ function parseFooterSection(value, index) {
3466
3485
  case "contact":
3467
3486
  case "opening-hours":
3468
3487
  case "social-links":
3469
- if (isRecord$3(value.source) && value.source.kind === "public-profile") {
3488
+ if (isRecord$1(value.source) && value.source.kind === "public-profile") {
3470
3489
  return ok({
3471
3490
  ...base,
3472
3491
  kind: value.kind,
@@ -3518,7 +3537,7 @@ function validateFooterComposition(composition) {
3518
3537
  return ok(composition);
3519
3538
  }
3520
3539
  function parseFooterCompositionV1(value) {
3521
- if (!isRecord$3(value)) {
3540
+ if (!isRecord$1(value)) {
3522
3541
  return err({
3523
3542
  code: "invalid_shape",
3524
3543
  path: "",
@@ -3551,7 +3570,7 @@ function parseFooterCompositionV1(value) {
3551
3570
  });
3552
3571
  }
3553
3572
  function parseLegacyPlacementSection(value, index, selectedIds) {
3554
- if (!isRecord$3(value)) {
3573
+ if (!isRecord$1(value)) {
3555
3574
  return err({
3556
3575
  code: "invalid_section",
3557
3576
  path: `modules.${index}`,
@@ -3559,7 +3578,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
3559
3578
  });
3560
3579
  }
3561
3580
  const selected = typeof value.id === "string" && selectedIds.has(value.id);
3562
- const source = isRecord$3(value.source) ? value.source : null;
3581
+ const source = isRecord$1(value.source) ? value.source : null;
3563
3582
  const mapped = value.kind === "navigation" && source?.kind === "menu-role" && source.role === "footer" ? {
3564
3583
  ...value,
3565
3584
  source: {
@@ -3578,7 +3597,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
3578
3597
  * It is intentionally not called by the runtime parser.
3579
3598
  */
3580
3599
  function upgradePlacementFooterCompositionV1(value) {
3581
- if (!isRecord$3(value) || value.schemaVersion !== 1 || !Array.isArray(value.modules) || !Array.isArray(value.placements)) {
3600
+ if (!isRecord$1(value) || value.schemaVersion !== 1 || !Array.isArray(value.modules) || !Array.isArray(value.placements)) {
3582
3601
  return err({
3583
3602
  code: "invalid_shape",
3584
3603
  path: "",
@@ -3587,7 +3606,7 @@ function upgradePlacementFooterCompositionV1(value) {
3587
3606
  }
3588
3607
  const selectedIds = new Set();
3589
3608
  for (const [index, placement] of value.placements.entries()) {
3590
- if (!isRecord$3(placement) || typeof placement.moduleId !== "string") {
3609
+ if (!isRecord$1(placement) || typeof placement.moduleId !== "string") {
3591
3610
  return err({
3592
3611
  code: "invalid_shape",
3593
3612
  path: `placements.${index}`,
@@ -3802,7 +3821,7 @@ const defaultSections = (legacy, options) => {
3802
3821
  ];
3803
3822
  };
3804
3823
  function normalizeLegacyFooterContentToV1(value, options = {}) {
3805
- if (!isRecord$3(value)) {
3824
+ if (!isRecord$1(value)) {
3806
3825
  return err({
3807
3826
  code: "invalid_shape",
3808
3827
  path: "",
@@ -3887,11 +3906,11 @@ const PRIORITY_IDS = new Set([
3887
3906
  "emailaddress"
3888
3907
  ]);
3889
3908
  const MAX_COLUMNS = 5;
3890
- function isRecord$2(value) {
3909
+ function isRecord(value) {
3891
3910
  return typeof value === "object" && value !== null && !Array.isArray(value);
3892
3911
  }
3893
3912
  function isSchemaField(value) {
3894
- return isRecord$2(value) && typeof value.id === "string";
3913
+ return isRecord(value) && typeof value.id === "string";
3895
3914
  }
3896
3915
  function selectSubmissionPreviewFields(schemaFields, submissions) {
3897
3916
  const fields = Array.isArray(schemaFields) ? schemaFields.filter(isSchemaField) : [];
@@ -3984,7 +4003,7 @@ function valueIsMeaningful(value) {
3984
4003
  if (value === null || value === undefined) return false;
3985
4004
  if (typeof value === "string") return value.trim().length > 0;
3986
4005
  if (Array.isArray(value)) return value.length > 0;
3987
- if (isRecord$2(value)) return Object.keys(value).length > 0;
4006
+ if (isRecord(value)) return Object.keys(value).length > 0;
3988
4007
  return true;
3989
4008
  }
3990
4009
  function orderSubmissionFields(schemaFields, data) {
@@ -4473,6 +4492,82 @@ function sortStableJsonValue(value) {
4473
4492
  throw new TypeError(`Stable JSON does not support ${typeof value}`);
4474
4493
  }
4475
4494
 
4495
+ //#endregion
4496
+ //#region ../core/src/secret.ts
4497
+ /**
4498
+ * Credential values that must remain inert when they cross diagnostics,
4499
+ * logging, or serialization boundaries.
4500
+ *
4501
+ * The value is deliberately held behind a non-enumerable capability rather
4502
+ * than exposed as a property. The capability uses a global symbol so a Secret
4503
+ * created by another bundled copy of core remains consumable at an approved
4504
+ * `exposeSecret` boundary.
4505
+ */
4506
+ const SECRET_KINDS = [
4507
+ "management-key",
4508
+ "content-key",
4509
+ "preview-key",
4510
+ "webhook-secret",
4511
+ "oauth-access-token",
4512
+ "oauth-refresh-token",
4513
+ "preview-session-refresh-token",
4514
+ "oauth-authorization-code",
4515
+ "oauth-pkce-verifier",
4516
+ "registrar-epp-code"
4517
+ ];
4518
+ const secretValueSymbol = Symbol.for("riverbank.secret.value");
4519
+ const SECRET_INSPECT_SYMBOL = Symbol.for("nodejs.util.inspect.custom");
4520
+ const MANAGEMENT_KEY_PREFIX = "bld_mgmt_sk_";
4521
+ function placeholderFor(kind) {
4522
+ return `[riverbank:redacted:${kind}]`;
4523
+ }
4524
+ /** Create an opaque secret after a caller has parsed/validated its boundary. */
4525
+ function createSecret(kind, value) {
4526
+ return createSecretInternal(kind, value);
4527
+ }
4528
+ /** Construct a Secret for an already validated kind/value pair. */
4529
+ function createSecretInternal(kind, value) {
4530
+ const placeholder = placeholderFor(kind);
4531
+ const secret = {
4532
+ kind: "secret",
4533
+ secretType: kind,
4534
+ toString: () => placeholder,
4535
+ toJSON: () => placeholder,
4536
+ [SECRET_INSPECT_SYMBOL]: () => placeholder
4537
+ };
4538
+ Object.defineProperty(secret, secretValueSymbol, {
4539
+ configurable: false,
4540
+ enumerable: false,
4541
+ value: () => value,
4542
+ writable: false
4543
+ });
4544
+ return Object.freeze(secret);
4545
+ }
4546
+ /**
4547
+ * Parse an untrusted management API key before constructing its Secret value.
4548
+ *
4549
+ * The Secret type protects an already validated value from accidental
4550
+ * disclosure; this boundary is what validates the value's wire format.
4551
+ */
4552
+ function parseManagementKey(raw) {
4553
+ if (typeof raw !== "string" || !raw.startsWith(MANAGEMENT_KEY_PREFIX) || raw.length === MANAGEMENT_KEY_PREFIX.length) {
4554
+ return err({ kind: "invalid-management-key" });
4555
+ }
4556
+ return ok(createSecretInternal("management-key", raw));
4557
+ }
4558
+ /**
4559
+ * The only generic plaintext exposure operation. Keep calls at named I/O
4560
+ * boundaries; do not pass exposed strings through domain/application layers.
4561
+ */
4562
+ function exposeSecret(secret) {
4563
+ const getValue = secret[secretValueSymbol];
4564
+ if (typeof getValue !== "function") return failInvalidSecret();
4565
+ return getValue();
4566
+ }
4567
+ function failInvalidSecret() {
4568
+ throw new TypeError("Invalid Riverbank secret");
4569
+ }
4570
+
4476
4571
  //#endregion
4477
4572
  //#region ../core/src/site-route-target.ts
4478
4573
  const SITE_ROUTE_NODE_TYPES = [
@@ -4600,885 +4695,675 @@ function disabledSelection() {
4600
4695
  }
4601
4696
 
4602
4697
  //#endregion
4603
- //#region ../api/src/sdk-event-payment-terms.ts
4604
- const supportedKinds = new Set([
4605
- "upfront",
4606
- "deferred_manual",
4607
- "flexible_balance"
4608
- ]);
4609
- function issue$2(path, message) {
4698
+ //#region ../core/src/event-series-behaviour.ts
4699
+ const EVENT_PRESET_VALUES = [
4700
+ "event",
4701
+ "class",
4702
+ "course",
4703
+ "term",
4704
+ "workshop",
4705
+ "retreat",
4706
+ "performance"
4707
+ ];
4708
+ const ORDINARY_EVENT_PRESET_VALUES = [
4709
+ "event",
4710
+ "class",
4711
+ "workshop",
4712
+ "retreat",
4713
+ "performance"
4714
+ ];
4715
+ const WHOLE_SERIES_EVENT_PRESET_VALUES = ["course", "term"];
4716
+ const SURFACE_EVENT_PRESET_VALUES = [
4717
+ "class",
4718
+ "course",
4719
+ "term",
4720
+ "workshop",
4721
+ "retreat",
4722
+ "performance"
4723
+ ];
4724
+ const DEFAULT_EVENT_TYPE_PRESETS = [
4725
+ "event",
4726
+ "class",
4727
+ "course",
4728
+ "term",
4729
+ "workshop",
4730
+ "retreat"
4731
+ ];
4732
+ const EVENT_SERIES_BEHAVIOURS = [
4733
+ "class",
4734
+ "course",
4735
+ "workshop",
4736
+ "retreat",
4737
+ "performance"
4738
+ ];
4739
+ /** Keeps Term as Event vocabulary without introducing a second booking engine. */
4740
+ function eventPresetForVerticalPreset(preset) {
4741
+ return preset.eventPresetOverride ?? preset.canonicalBehaviour;
4742
+ }
4743
+
4744
+ //#endregion
4745
+ //#region ../core/src/booking-vertical-preset.ts
4746
+ const BOOKING_ENGINE_BY_BEHAVIOUR = {
4747
+ class: "event_series",
4748
+ workshop: "event_series",
4749
+ course: "event_series",
4750
+ retreat: "event_series",
4751
+ performance: "event_series",
4752
+ appointment: "appointment_service"
4753
+ };
4754
+ const BOOKING_DEFAULTS_BY_BEHAVIOUR = {
4755
+ class: {
4756
+ passes: "on",
4757
+ memberships: "on"
4758
+ },
4759
+ workshop: {
4760
+ passes: "off",
4761
+ memberships: "off"
4762
+ },
4763
+ course: {
4764
+ passes: "off",
4765
+ memberships: "strict"
4766
+ },
4767
+ retreat: {
4768
+ passes: "off",
4769
+ memberships: "off"
4770
+ },
4771
+ performance: {
4772
+ passes: "off",
4773
+ memberships: "off"
4774
+ },
4775
+ appointment: {
4776
+ passes: "off",
4777
+ memberships: "off"
4778
+ }
4779
+ };
4780
+ function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability = "onboarding") {
4610
4781
  return {
4611
- path,
4612
- message
4782
+ id,
4783
+ verticalId,
4784
+ canonicalBehaviour,
4785
+ engine: BOOKING_ENGINE_BY_BEHAVIOUR[canonicalBehaviour],
4786
+ availability,
4787
+ label,
4788
+ pluralLabel,
4789
+ description
4613
4790
  };
4614
4791
  }
4615
- function isRecord$1(value) {
4616
- return typeof value === "object" && value !== null && !Array.isArray(value);
4617
- }
4618
- function isWholeNumber$1(value) {
4619
- return typeof value === "number" && Number.isInteger(value);
4620
- }
4621
- function isPositiveWholeNumber(value) {
4622
- return isWholeNumber$1(value) && value > 0;
4623
- }
4624
- function isDepositRefundability(value) {
4625
- return value === "refundable" || value === "non_refundable";
4792
+
4793
+ //#endregion
4794
+ //#region ../core/src/booking-vertical-definitions.ts
4795
+ const VERTICAL_DEFINITIONS = [
4796
+ {
4797
+ id: "yoga_wellness",
4798
+ label: "Yoga, meditation or mindfulness",
4799
+ description: "Yoga studios, wellness teachers, and movement practitioners."
4800
+ },
4801
+ {
4802
+ id: "fitness_pt",
4803
+ label: "Fitness and personal training",
4804
+ description: "Personal trainers, group training, and fitness programmes."
4805
+ },
4806
+ {
4807
+ id: "dance_school",
4808
+ label: "Dance school",
4809
+ description: "Dance classes, courses or terms, workshops, private lessons, and shows."
4810
+ },
4811
+ {
4812
+ id: "physiotherapy_physical_health",
4813
+ label: "Physiotherapy & physical health",
4814
+ description: "Physiotherapists, chiropractors, osteopaths, sports therapists, and rehabilitation practitioners."
4815
+ },
4816
+ {
4817
+ id: "massage_bodywork",
4818
+ label: "Massage & bodywork",
4819
+ description: "Massage therapists, bodyworkers, and somatic practitioners."
4820
+ },
4821
+ {
4822
+ id: "therapist_practitioner",
4823
+ label: "Therapist or practitioner",
4824
+ description: "Appointments, sessions, consultations, and occasional group work."
4825
+ },
4826
+ {
4827
+ id: "beauty_spa",
4828
+ label: "Beauty or spa",
4829
+ description: "Treatment-led businesses with optional workshops."
4830
+ },
4831
+ {
4832
+ id: "consultant_coach",
4833
+ label: "Consultant or coach",
4834
+ description: "One-to-one calls, workshops, courses, and retreats."
4835
+ },
4836
+ {
4837
+ id: "training_provider",
4838
+ label: "Training provider",
4839
+ description: "Courses, workshops, classes, appointments, and residential training."
4840
+ },
4841
+ {
4842
+ id: "retreat_centre",
4843
+ label: "Retreat centre",
4844
+ description: "Residential retreats, workshops, classes, courses, and private sessions."
4845
+ },
4846
+ {
4847
+ id: "music_school",
4848
+ label: "Music teacher or school",
4849
+ description: "Lessons, classes, terms, workshops, and performances."
4850
+ },
4851
+ {
4852
+ id: "voice_singing",
4853
+ label: "Voice & Singing",
4854
+ description: "Singing lessons, choirs, voice circles, workshops, and performances."
4855
+ },
4856
+ {
4857
+ id: "performing_arts_school",
4858
+ label: "Theatre or performing arts school",
4859
+ description: "Classes, terms, workshops, performances, and lessons."
4860
+ },
4861
+ {
4862
+ id: "community_wellbeing",
4863
+ label: "Community or wellbeing centre",
4864
+ description: "Groups, workshops, courses, appointments, and retreats."
4865
+ },
4866
+ {
4867
+ id: "venue_event_space",
4868
+ label: "Venue or event space",
4869
+ description: "Room, resource, or access bookings. Depends on the future capacity-booking domain."
4870
+ }
4871
+ ];
4872
+ const VERTICAL_IDS = VERTICAL_DEFINITIONS.map(({ id }) => id);
4873
+
4874
+ //#endregion
4875
+ //#region ../core/src/booking-vertical-catalog.ts
4876
+ const VERTICAL_BOOKING_PRESETS = [
4877
+ verticalBookingPreset("yoga_wellness.classes", "yoga_wellness", "class", "Class", "Classes", "Regular scheduled classes and group sessions."),
4878
+ verticalBookingPreset("yoga_wellness.workshops", "yoga_wellness", "workshop", "Workshop", "Workshops", "One-off workshops, day retreats, or special sessions."),
4879
+ verticalBookingPreset("yoga_wellness.courses", "yoga_wellness", "course", "Course", "Courses", "Fixed-schedule courses or multi-session groups."),
4880
+ verticalBookingPreset("yoga_wellness.retreats", "yoga_wellness", "retreat", "Retreat", "Retreats", "Multi-day or residential retreats and immersive experiences."),
4881
+ verticalBookingPreset("yoga_wellness.appointments", "yoga_wellness", "appointment", "Appointment", "Appointments", "Private sessions, consultations, or one-to-one support."),
4882
+ verticalBookingPreset("fitness_pt.group_training", "fitness_pt", "class", "Group training", "Group training", "Regular scheduled classes and group sessions."),
4883
+ verticalBookingPreset("fitness_pt.personal_training", "fitness_pt", "appointment", "Personal training", "Personal training", "One-to-one sessions booked from your availability."),
4884
+ verticalBookingPreset("fitness_pt.programmes", "fitness_pt", "course", "Programme", "Programmes", "Multi-session programmes like bootcamps or courses."),
4885
+ verticalBookingPreset("fitness_pt.workshops", "fitness_pt", "workshop", "Workshop", "Workshops", "One-off workshops, events, or special sessions."),
4886
+ verticalBookingPreset("fitness_pt.retreats", "fitness_pt", "retreat", "Retreat", "Retreats", "Multi-day or residential fitness experiences."),
4887
+ verticalBookingPreset("dance_school.classes", "dance_school", "class", "Class", "Classes", "Regular group classes and weekly sessions."),
4888
+ {
4889
+ ...verticalBookingPreset("dance_school.terms", "dance_school", "course", "Term", "Terms", "Multi-week term blocks or fixed-date course groups."),
4890
+ eventPresetOverride: "term"
4891
+ },
4892
+ verticalBookingPreset("dance_school.courses", "dance_school", "course", "Course", "Courses", "Fixed-schedule courses or multi-session groups.", "settings"),
4893
+ verticalBookingPreset("dance_school.workshops", "dance_school", "workshop", "Workshop", "Workshops", "One-off workshops, masterclasses, or intensives."),
4894
+ verticalBookingPreset("dance_school.private_lessons", "dance_school", "appointment", "Private lesson", "Private lessons", "One-to-one lessons or coaching sessions."),
4895
+ verticalBookingPreset("dance_school.shows", "dance_school", "performance", "Show", "Shows", "Ticketed performances, showcases, and recitals."),
4896
+ verticalBookingPreset("physiotherapy_physical_health.appointments", "physiotherapy_physical_health", "appointment", "Appointment", "Appointments", "Assessments, treatments, follow-ups, and rehabilitation sessions."),
4897
+ verticalBookingPreset("physiotherapy_physical_health.rehab_classes", "physiotherapy_physical_health", "class", "Rehab class", "Rehab classes", "Regular rehabilitation, mobility, strength, or recovery classes."),
4898
+ verticalBookingPreset("physiotherapy_physical_health.workshops", "physiotherapy_physical_health", "workshop", "Workshop", "Workshops", "One-off workshops, talks, or practical group sessions."),
4899
+ verticalBookingPreset("massage_bodywork.treatments", "massage_bodywork", "appointment", "Treatment", "Treatments", "Massage treatments, bodywork sessions, and one-to-one appointments."),
4900
+ verticalBookingPreset("massage_bodywork.group_sessions", "massage_bodywork", "class", "Group session", "Group sessions", "Regular bodywork, somatic movement, or self-massage sessions."),
4901
+ verticalBookingPreset("massage_bodywork.workshops", "massage_bodywork", "workshop", "Workshop", "Workshops", "One-off bodywork, self-massage, or somatic practice workshops."),
4902
+ verticalBookingPreset("therapist_practitioner.appointments", "therapist_practitioner", "appointment", "Appointment", "Appointments", "Sessions, consultations, treatments, or assessments."),
4903
+ verticalBookingPreset("therapist_practitioner.workshops", "therapist_practitioner", "workshop", "Workshop", "Workshops", "One-off group workshops, talks, or wellbeing sessions."),
4904
+ verticalBookingPreset("therapist_practitioner.retreats", "therapist_practitioner", "retreat", "Retreat", "Retreats", "Multi-day retreats or immersive group experiences."),
4905
+ verticalBookingPreset("beauty_spa.appointments", "beauty_spa", "appointment", "Appointment", "Appointments", "Treatments, consultations, services, and sessions."),
4906
+ verticalBookingPreset("beauty_spa.workshops", "beauty_spa", "workshop", "Workshop", "Workshops", "Skincare workshops, makeup lessons, demos, or group events."),
4907
+ verticalBookingPreset("consultant_coach.appointments", "consultant_coach", "appointment", "Appointment", "Appointments", "One-to-one calls, consultations, reviews, or coaching sessions."),
4908
+ verticalBookingPreset("consultant_coach.workshops", "consultant_coach", "workshop", "Workshop", "Workshops", "One-off group sessions, webinars, or strategy days."),
4909
+ verticalBookingPreset("consultant_coach.courses", "consultant_coach", "course", "Course", "Courses", "Fixed-schedule cohorts or multi-session group programmes."),
4910
+ verticalBookingPreset("consultant_coach.retreats", "consultant_coach", "retreat", "Retreat", "Retreats", "Multi-day coaching retreats or immersive experiences."),
4911
+ verticalBookingPreset("training_provider.courses", "training_provider", "course", "Course", "Courses", "Fixed-schedule courses, cohorts, and multi-session training."),
4912
+ verticalBookingPreset("training_provider.workshops", "training_provider", "workshop", "Workshop", "Workshops", "One-off workshops, masterclasses, and short training sessions."),
4913
+ verticalBookingPreset("training_provider.appointments", "training_provider", "appointment", "Appointment", "Appointments", "Consultations, assessments, mentoring, or office hours."),
4914
+ verticalBookingPreset("training_provider.classes", "training_provider", "class", "Class", "Classes", "Regular classes, sessions, or groups.", "settings"),
4915
+ verticalBookingPreset("training_provider.residentials", "training_provider", "retreat", "Residential", "Residentials", "Residential courses or multi-day training experiences.", "settings"),
4916
+ verticalBookingPreset("retreat_centre.retreats", "retreat_centre", "retreat", "Retreat", "Retreats", "Multi-day or residential retreats with richer booking needs."),
4917
+ verticalBookingPreset("retreat_centre.workshops", "retreat_centre", "workshop", "Workshop", "Workshops", "One-day retreats, ceremonies, workshops, or special events."),
4918
+ verticalBookingPreset("retreat_centre.classes", "retreat_centre", "class", "Class", "Classes", "Regular sessions like guided meditations or group practices."),
4919
+ verticalBookingPreset("retreat_centre.courses", "retreat_centre", "course", "Course", "Courses", "Fixed-schedule programmes or multi-session experiences."),
4920
+ verticalBookingPreset("retreat_centre.appointments", "retreat_centre", "appointment", "Appointment", "Appointments", "Private sessions, therapies, consultations, or add-ons."),
4921
+ verticalBookingPreset("music_school.lessons", "music_school", "appointment", "Lesson", "Lessons", "One-to-one lessons, consultations, or coaching sessions."),
4922
+ verticalBookingPreset("music_school.classes", "music_school", "class", "Class", "Classes", "Regular group classes, ensembles, or recurring sessions."),
4923
+ {
4924
+ ...verticalBookingPreset("music_school.terms", "music_school", "course", "Term", "Terms", "Multi-week term blocks or fixed-date group courses."),
4925
+ eventPresetOverride: "term"
4926
+ },
4927
+ verticalBookingPreset("music_school.workshops", "music_school", "workshop", "Workshop", "Workshops", "One-off workshops, masterclasses, or intensives."),
4928
+ verticalBookingPreset("music_school.performances", "music_school", "performance", "Performance", "Performances", "Concerts, recitals, showcases, or ticketed performances."),
4929
+ verticalBookingPreset("voice_singing.lessons", "voice_singing", "appointment", "Voice session", "Voice sessions", "One-to-one singing lessons, voice coaching, or private practice sessions."),
4930
+ verticalBookingPreset("voice_singing.classes", "voice_singing", "class", "Ongoing weekly session", "Ongoing weekly sessions", "Regular choirs, voice circles, community singing, or group practice."),
4931
+ {
4932
+ ...verticalBookingPreset("voice_singing.terms", "voice_singing", "course", "Singing term", "Singing terms", "Multi-week choir terms, voice courses, or fixed-date group programmes."),
4933
+ eventPresetOverride: "term"
4934
+ },
4935
+ verticalBookingPreset("voice_singing.workshops", "voice_singing", "workshop", "Workshop", "Workshops", "One-off singing workshops, voice journeys, or devotional singing sessions."),
4936
+ verticalBookingPreset("voice_singing.performances", "voice_singing", "performance", "Performance", "Performances", "Choir concerts, sharing events, recitals, or community performances."),
4937
+ verticalBookingPreset("performing_arts_school.classes", "performing_arts_school", "class", "Class", "Classes", "Regular group classes, rehearsals, or weekly sessions."),
4938
+ {
4939
+ ...verticalBookingPreset("performing_arts_school.terms", "performing_arts_school", "course", "Term", "Terms", "Multi-week term blocks or fixed-date course groups."),
4940
+ eventPresetOverride: "term"
4941
+ },
4942
+ verticalBookingPreset("performing_arts_school.workshops", "performing_arts_school", "workshop", "Workshop", "Workshops", "One-off workshops, masterclasses, or intensives."),
4943
+ verticalBookingPreset("performing_arts_school.performances", "performing_arts_school", "performance", "Performance", "Performances", "Shows, showcases, productions, or ticketed performances."),
4944
+ verticalBookingPreset("performing_arts_school.lessons", "performing_arts_school", "appointment", "Lesson", "Lessons", "One-to-one lessons, coaching, or audition prep."),
4945
+ verticalBookingPreset("community_wellbeing.classes", "community_wellbeing", "class", "Class", "Classes", "Regular groups, sessions, practices, or weekly activities."),
4946
+ verticalBookingPreset("community_wellbeing.workshops", "community_wellbeing", "workshop", "Workshop", "Workshops", "One-off workshops, talks, events, or gatherings."),
4947
+ verticalBookingPreset("community_wellbeing.courses", "community_wellbeing", "course", "Course", "Courses", "Fixed-schedule courses, study groups, or multi-session programmes."),
4948
+ verticalBookingPreset("community_wellbeing.appointments", "community_wellbeing", "appointment", "Appointment", "Appointments", "One-to-one appointments, consultations, or support sessions."),
4949
+ verticalBookingPreset("community_wellbeing.retreats", "community_wellbeing", "retreat", "Retreat", "Retreats", "Multi-day or residential retreats."),
4950
+ verticalBookingPreset("venue_event_space.resource_hire", "venue_event_space", "appointment", "Hire slot", "Hire slots", "Future room, venue, and resource hire bookings.", "future")
4951
+ ];
4952
+
4953
+ //#endregion
4954
+ //#region ../core/src/booking-vertical-config.ts
4955
+ const DEFAULT_ONBOARDING_PRESET_ID_BY_VERTICAL = {
4956
+ yoga_wellness: null,
4957
+ fitness_pt: null,
4958
+ dance_school: null,
4959
+ physiotherapy_physical_health: "physiotherapy_physical_health.appointments",
4960
+ massage_bodywork: "massage_bodywork.treatments",
4961
+ therapist_practitioner: null,
4962
+ beauty_spa: null,
4963
+ consultant_coach: null,
4964
+ training_provider: null,
4965
+ retreat_centre: null,
4966
+ music_school: null,
4967
+ voice_singing: null,
4968
+ performing_arts_school: null,
4969
+ community_wellbeing: null,
4970
+ venue_event_space: null
4971
+ };
4972
+ const VERTICAL_ID_SET = new Set(VERTICAL_DEFINITIONS.map(({ id }) => id));
4973
+ const VERTICAL_PRESET_ID_SET = new Set(VERTICAL_BOOKING_PRESETS.map((presetDefinition) => presetDefinition.id));
4974
+ function parseVerticalId(value) {
4975
+ return typeof value === "string" && VERTICAL_ID_SET.has(value) ? value : null;
4626
4976
  }
4627
- function isSupportedOptionKind(value) {
4628
- return typeof value === "string" && supportedKinds.has(value);
4977
+ function parseVerticalPresetId(value) {
4978
+ return typeof value === "string" && VERTICAL_PRESET_ID_SET.has(value) ? value : null;
4629
4979
  }
4630
- function validateFlexibleBalanceDueDateRule(raw, path) {
4631
- if (!isRecord$1(raw)) {
4632
- return [issue$2(path, "Flexible balance dueDateRule must be an object")];
4980
+ function getVerticalDefinition(verticalId) {
4981
+ const definition = VERTICAL_DEFINITIONS.find((vertical) => vertical.id === verticalId);
4982
+ if (definition === undefined) {
4983
+ throw new Error(`Unhandled vertical id: ${verticalId}`);
4633
4984
  }
4634
- if (raw.kind === "relative_to_start_date") {
4635
- if (!isPositiveWholeNumber(raw.daysBeforeStart)) {
4636
- return [issue$2([...path, "daysBeforeStart"], "Flexible balance daysBeforeStart must be a positive integer")];
4637
- }
4638
- return {
4639
- kind: "relative_to_start_date",
4640
- daysBeforeStart: raw.daysBeforeStart
4641
- };
4642
- }
4643
- if (raw.kind === "fixed_calendar_date") {
4644
- if (typeof raw.dueAt !== "string" || !Number.isFinite(Date.parse(raw.dueAt))) {
4645
- return [issue$2([...path, "dueAt"], "Flexible balance fixed dueAt must be an ISO datetime")];
4646
- }
4647
- return {
4648
- kind: "fixed_calendar_date",
4649
- dueAt: raw.dueAt
4650
- };
4651
- }
4652
- return [issue$2([...path, "kind"], "Flexible balance dueDateRule kind must be relative_to_start_date or fixed_calendar_date")];
4985
+ return definition;
4653
4986
  }
4654
- function validateFlexibleBalanceConfig(raw, path) {
4655
- if (!isRecord$1(raw)) {
4656
- return [issue$2(path, "Flexible balance config must be an object")];
4657
- }
4658
- const issues = [];
4659
- const upfrontAmountCents = raw.upfrontAmountCents;
4660
- const depositRefundability = raw.depositRefundability;
4661
- let validatedUpfrontAmountCents = null;
4662
- let validatedDepositRefundability = null;
4663
- if (!isWholeNumber$1(upfrontAmountCents) || upfrontAmountCents < 0) {
4664
- issues.push(issue$2([...path, "upfrontAmountCents"], "Flexible balance upfrontAmountCents must be a non-negative integer"));
4665
- } else {
4666
- validatedUpfrontAmountCents = upfrontAmountCents;
4667
- }
4668
- if (!isDepositRefundability(depositRefundability)) {
4669
- issues.push(issue$2([...path, "depositRefundability"], "Flexible balance depositRefundability must be refundable or non_refundable"));
4670
- } else {
4671
- validatedDepositRefundability = depositRefundability;
4672
- }
4673
- const dueDateRule = validateFlexibleBalanceDueDateRule(raw.dueDateRule, [...path, "dueDateRule"]);
4674
- if (Array.isArray(dueDateRule)) {
4675
- issues.push(...dueDateRule);
4676
- }
4677
- if (issues.length > 0 || Array.isArray(dueDateRule) || validatedUpfrontAmountCents === null || validatedDepositRefundability === null) {
4678
- return issues;
4987
+ function getVerticalPresetDefinition(presetId) {
4988
+ const definition = VERTICAL_BOOKING_PRESETS.find((presetDefinition) => presetDefinition.id === presetId);
4989
+ if (definition === undefined) {
4990
+ throw new Error(`Unhandled vertical preset id: ${presetId}`);
4679
4991
  }
4992
+ return definition;
4993
+ }
4994
+ function getDefaultOnboardingPresetForVertical(verticalId) {
4995
+ const presetId = DEFAULT_ONBOARDING_PRESET_ID_BY_VERTICAL[verticalId];
4996
+ return presetId ? getVerticalPresetDefinition(presetId) : null;
4997
+ }
4998
+ function createConfiguredSiteVerticalBookingConfig(verticalId, selectedPresetIds) {
4680
4999
  return {
4681
- upfrontAmountCents: validatedUpfrontAmountCents,
4682
- depositRefundability: validatedDepositRefundability,
4683
- dueDateRule
5000
+ verticalId,
5001
+ selectedPresetIds
4684
5002
  };
4685
5003
  }
4686
- function validateCollectionOption(raw, index) {
4687
- const path = ["allowedOptions", String(index)];
4688
- if (!isRecord$1(raw)) {
4689
- return [issue$2(path, "Payment option must be an object")];
5004
+ function toNonEmptyVerticalPresetIds(presetIds) {
5005
+ const firstPresetId = presetIds[0];
5006
+ return firstPresetId ? [firstPresetId, ...presetIds.slice(1)] : null;
5007
+ }
5008
+ /**
5009
+ * Construct a configured booking selection only when the supplied preset IDs
5010
+ * form a valid selection for the vertical. Invalid, cross-vertical, future,
5011
+ * duplicate, or empty selections return null.
5012
+ */
5013
+ function createSiteVerticalBookingConfig(verticalId, selectedPresetIds) {
5014
+ const resolved = resolveSiteVerticalBookingConfig({
5015
+ verticalId,
5016
+ selectedPresetIds
5017
+ });
5018
+ return resolved.kind === "configured" ? resolved.config : null;
5019
+ }
5020
+ const DEFAULT_SITE_VERTICAL_BOOKING_CONFIG = {
5021
+ verticalId: null,
5022
+ selectedPresetIds: []
5023
+ };
5024
+ function appendUniqueIssue(issues, issue) {
5025
+ return issues.some((existing) => existing.code === issue.code) ? issues : [...issues, issue];
5026
+ }
5027
+ function collectRawConfigIssues(rawConfig, config) {
5028
+ const rawPresetValue = rawConfig.selectedPresetIds;
5029
+ const rawPresetIds = Array.isArray(rawPresetValue) ? rawPresetValue : [];
5030
+ let issues = [];
5031
+ if (rawConfig.verticalId != null && config.verticalId === null) {
5032
+ issues = appendUniqueIssue(issues, {
5033
+ code: "unknown_vertical",
5034
+ rawValue: rawConfig.verticalId
5035
+ });
4690
5036
  }
4691
- if (raw.kind === "instalment_plan") {
4692
- return [issue$2([...path, "kind"], "Event instalment plans are not supported yet. Use upfront, deferred_manual, or flexible_balance.")];
5037
+ if (config.verticalId === null && config.selectedPresetIds.length > 0) {
5038
+ issues = appendUniqueIssue(issues, { code: "missing_vertical" });
4693
5039
  }
4694
- if (!isSupportedOptionKind(raw.kind)) {
4695
- return [issue$2([...path, "kind"], "Payment option kind must be upfront, deferred_manual, or flexible_balance")];
5040
+ if (rawPresetValue !== undefined && !Array.isArray(rawPresetValue) || rawPresetIds.some((presetId) => parseVerticalPresetId(presetId) === null)) {
5041
+ issues = appendUniqueIssue(issues, {
5042
+ code: "unknown_preset",
5043
+ rawValue: rawPresetValue
5044
+ });
4696
5045
  }
4697
- switch (raw.kind) {
4698
- case "upfront":
4699
- case "deferred_manual": return { kind: raw.kind };
4700
- case "flexible_balance": {
4701
- const config = validateFlexibleBalanceConfig(raw.config, [...path, "config"]);
4702
- if (Array.isArray(config)) {
4703
- return config;
4704
- }
4705
- return {
4706
- kind: "flexible_balance",
4707
- config
4708
- };
4709
- }
5046
+ if (rawConfig.verticalId != null && config.selectedPresetIds.length === 0) {
5047
+ issues = appendUniqueIssue(issues, { code: "empty_selected_presets" });
4710
5048
  }
5049
+ return issues;
4711
5050
  }
4712
- function optionCombinationIssues(options) {
4713
- const issues = [];
4714
- const seenKinds = new Set();
4715
- options.forEach((option, index) => {
4716
- if (seenKinds.has(option.kind)) {
4717
- issues.push(issue$2([
4718
- "allowedOptions",
4719
- String(index),
4720
- "kind"
4721
- ], `Duplicate payment option kind "${option.kind}"`));
5051
+ function collectPresetConfigIssues(config) {
5052
+ const seenPresetIds = new Set();
5053
+ const seenBehaviours = new Set();
5054
+ let issues = [];
5055
+ for (const presetId of config.selectedPresetIds) {
5056
+ const presetDefinition = getVerticalPresetDefinition(presetId);
5057
+ if (seenPresetIds.has(presetId)) {
5058
+ issues = appendUniqueIssue(issues, { code: "duplicate_preset_id" });
4722
5059
  }
4723
- seenKinds.add(option.kind);
4724
- });
4725
- if (options.length > 2) {
4726
- issues.push(issue$2(["allowedOptions"], "Event payment terms can offer one option, or a choice between upfront and one alternative option"));
4727
- }
4728
- if (options.length === 2 && (!seenKinds.has("upfront") || !seenKinds.has("deferred_manual") && !seenKinds.has("flexible_balance"))) {
4729
- issues.push(issue$2(["allowedOptions"], "Event payment terms with two options must combine upfront with deferred_manual or flexible_balance"));
5060
+ seenPresetIds.add(presetId);
5061
+ if (config.verticalId !== null && presetDefinition.verticalId !== config.verticalId) {
5062
+ issues = appendUniqueIssue(issues, { code: "preset_vertical_mismatch" });
5063
+ }
5064
+ if (presetDefinition.availability === "future") {
5065
+ issues = appendUniqueIssue(issues, { code: "future_preset_selected" });
5066
+ }
5067
+ if (seenBehaviours.has(presetDefinition.canonicalBehaviour)) {
5068
+ issues = appendUniqueIssue(issues, { code: "duplicate_canonical_behaviour" });
5069
+ }
5070
+ seenBehaviours.add(presetDefinition.canonicalBehaviour);
4730
5071
  }
4731
5072
  return issues;
4732
5073
  }
4733
- function classifySdkEventPaymentTerms(rawInput) {
4734
- if (rawInput === undefined || rawInput === null) {
4735
- return {
4736
- ok: true,
4737
- paymentTerms: undefined
4738
- };
5074
+ function getSiteVerticalConfigIssues(rawConfig, config) {
5075
+ let issues = collectRawConfigIssues(rawConfig, config);
5076
+ for (const issue of collectPresetConfigIssues(config)) {
5077
+ issues = appendUniqueIssue(issues, issue);
4739
5078
  }
4740
- if (!isRecord$1(rawInput)) {
5079
+ return issues;
5080
+ }
5081
+ function resolveSiteVerticalBookingConfig(rawConfig) {
5082
+ if (rawConfig == null) {
4741
5083
  return {
4742
- ok: false,
4743
- issues: [issue$2([], "Event paymentTerms must be an object")]
5084
+ kind: "unconfigured",
5085
+ config: DEFAULT_SITE_VERTICAL_BOOKING_CONFIG
4744
5086
  };
4745
5087
  }
4746
- if (!Array.isArray(rawInput.allowedOptions)) {
5088
+ const verticalId = parseVerticalId(rawConfig.verticalId);
5089
+ const rawPresetIds = Array.isArray(rawConfig.selectedPresetIds) ? rawConfig.selectedPresetIds : [];
5090
+ const selectedPresetIds = rawPresetIds.map(parseVerticalPresetId).filter((presetId) => Boolean(presetId));
5091
+ const config = {
5092
+ verticalId,
5093
+ selectedPresetIds
5094
+ };
5095
+ if (rawConfig.verticalId == null && selectedPresetIds.length === 0) {
4747
5096
  return {
4748
- ok: false,
4749
- issues: [issue$2(["allowedOptions"], "Event paymentTerms.allowedOptions is required")]
5097
+ kind: "unconfigured",
5098
+ config: DEFAULT_SITE_VERTICAL_BOOKING_CONFIG
4750
5099
  };
4751
5100
  }
4752
- if (rawInput.allowedOptions.length === 0) {
4753
- return {
4754
- ok: false,
4755
- issues: [issue$2(["allowedOptions"], "Event paymentTerms.allowedOptions must include at least one option")]
4756
- };
4757
- }
4758
- const options = [];
4759
- const issues = [];
4760
- rawInput.allowedOptions.forEach((option, index) => {
4761
- const validated = validateCollectionOption(option, index);
4762
- if (Array.isArray(validated)) {
4763
- issues.push(...validated);
4764
- return;
4765
- }
4766
- options.push(validated);
4767
- });
4768
- issues.push(...optionCombinationIssues(options));
4769
- if (issues.length > 0) {
5101
+ const issues = getSiteVerticalConfigIssues(rawConfig, config);
5102
+ if (issues.length > 0) return {
5103
+ kind: "invalid",
5104
+ config,
5105
+ issues
5106
+ };
5107
+ const nonEmptyPresetIds = toNonEmptyVerticalPresetIds(selectedPresetIds);
5108
+ const configuredConfig = verticalId !== null && nonEmptyPresetIds !== null ? createConfiguredSiteVerticalBookingConfig(verticalId, nonEmptyPresetIds) : null;
5109
+ if (configuredConfig === null) {
4770
5110
  return {
4771
- ok: false,
4772
- issues
5111
+ kind: "invalid",
5112
+ config,
5113
+ issues: [{ code: "empty_selected_presets" }]
4773
5114
  };
4774
5115
  }
4775
5116
  return {
4776
- ok: true,
4777
- paymentTerms: { allowedOptions: options }
5117
+ kind: "configured",
5118
+ config: configuredConfig,
5119
+ selectedPresets: configuredConfig.selectedPresetIds.map(getVerticalPresetDefinition)
4778
5120
  };
4779
5121
  }
4780
5122
 
4781
5123
  //#endregion
4782
- //#region ../api/src/sdk-event-pricing.ts
4783
- const identifierRegex = /^[a-z0-9][a-z0-9-]*$/;
4784
- const currencyRegex = /^[A-Z]{3}$/;
4785
- const isoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T.+/;
4786
- function issue$1(path, message) {
4787
- return {
4788
- path,
4789
- message
4790
- };
4791
- }
4792
- function isRecord(value) {
4793
- return typeof value === "object" && value !== null && !Array.isArray(value);
5124
+ //#region ../core/src/booking-verticals.ts
5125
+ const CANONICAL_BOOKING_BEHAVIOURS = [
5126
+ "class",
5127
+ "workshop",
5128
+ "course",
5129
+ "retreat",
5130
+ "performance",
5131
+ "appointment"
5132
+ ];
5133
+ const BOOKING_ENGINES = ["event_series", "appointment_service"];
5134
+ const VERTICAL_PRESET_AVAILABILITIES = [
5135
+ "onboarding",
5136
+ "settings",
5137
+ "future"
5138
+ ];
5139
+ function getVerticalPresetsForVertical(verticalId, options) {
5140
+ const availability = options?.availability;
5141
+ return VERTICAL_BOOKING_PRESETS.filter((presetDefinition) => {
5142
+ if (presetDefinition.verticalId !== verticalId) return false;
5143
+ if (!availability) return true;
5144
+ if (availability === "selectable") return presetDefinition.availability !== "future";
5145
+ return presetDefinition.availability === availability;
5146
+ });
4794
5147
  }
4795
- function isCurrency(value) {
4796
- return typeof value === "string" && currencyRegex.test(value);
5148
+ function getOnboardingPresetsForVertical(verticalId) {
5149
+ return getVerticalPresetsForVertical(verticalId, { availability: "onboarding" });
4797
5150
  }
4798
- function isWholeNumber(value) {
4799
- return typeof value === "number" && Number.isInteger(value);
5151
+ function isEventSeriesCanonicalBehaviour(behaviour) {
5152
+ return EVENT_SERIES_BEHAVIOURS.includes(behaviour);
4800
5153
  }
4801
- function isWholeMinuteIsoDateTime(value) {
4802
- if (!isoDateTimeRegex.test(value)) return false;
4803
- const epochMs = Date.parse(value);
4804
- return Number.isFinite(epochMs) && epochMs % 6e4 === 0;
5154
+ function isEventSeriesVerticalPresetDefinition(presetDefinition) {
5155
+ return isEventSeriesCanonicalBehaviour(presetDefinition.canonicalBehaviour);
4805
5156
  }
4806
- function validateTicketType(raw, index, currency) {
4807
- const path = ["ticketTypes", String(index)];
4808
- if (!isRecord(raw)) return [issue$1(path, "Ticket type must be an object")];
4809
- const issues = [];
4810
- const identifier = raw.identifier;
4811
- if (typeof identifier !== "string" || !identifierRegex.test(identifier)) {
4812
- issues.push(issue$1([...path, "identifier"], "Ticket identifier must use lowercase letters, numbers, and hyphens"));
4813
- }
4814
- const name = raw.name;
4815
- if (typeof name !== "string" || name.trim().length === 0) {
4816
- issues.push(issue$1([...path, "name"], "Ticket name is required"));
4817
- }
4818
- const priceAmount = raw.priceAmount;
4819
- if (!isWholeNumber(priceAmount) || priceAmount < 0) {
4820
- issues.push(issue$1([...path, "priceAmount"], "Ticket priceAmount must be a non-negative integer"));
4821
- }
4822
- const priceCurrency = raw.priceCurrency;
4823
- if (priceCurrency !== undefined && priceCurrency !== null && priceCurrency !== currency) {
4824
- issues.push(issue$1([...path, "priceCurrency"], "Ticket priceCurrency must match pricing.priceCurrency"));
4825
- }
4826
- const sortOrder = raw.sortOrder;
4827
- if (sortOrder !== undefined && (!isWholeNumber(sortOrder) || sortOrder < 0)) {
4828
- issues.push(issue$1([...path, "sortOrder"], "Ticket sortOrder must be a non-negative integer"));
4829
- }
4830
- const capacityLimit = raw.capacityLimit;
4831
- if (capacityLimit !== undefined && capacityLimit !== null && (!isWholeNumber(capacityLimit) || capacityLimit < 1)) {
4832
- issues.push(issue$1([...path, "capacityLimit"], "Ticket capacityLimit must be a positive integer"));
4833
- }
4834
- const attendanceScope = raw.attendanceScope;
4835
- if (attendanceScope !== undefined && attendanceScope !== "in_person" && attendanceScope !== "online" && attendanceScope !== "both") {
4836
- issues.push(issue$1([...path, "attendanceScope"], "Ticket attendanceScope must be in_person, online, or both"));
4837
- }
4838
- const saleStartsAt = raw.saleStartsAt;
4839
- if (saleStartsAt !== undefined && saleStartsAt !== null && (typeof saleStartsAt !== "string" || !isWholeMinuteIsoDateTime(saleStartsAt))) {
4840
- issues.push(issue$1([...path, "saleStartsAt"], "Ticket saleStartsAt must be an ISO datetime aligned to a whole minute"));
4841
- }
4842
- const saleEndsAt = raw.saleEndsAt;
4843
- if (saleEndsAt !== undefined && saleEndsAt !== null && (typeof saleEndsAt !== "string" || !isWholeMinuteIsoDateTime(saleEndsAt))) {
4844
- issues.push(issue$1([...path, "saleEndsAt"], "Ticket saleEndsAt must be an ISO datetime aligned to a whole minute"));
5157
+ function validateEventSeriesVerticalPreset(input) {
5158
+ const presetId = parseVerticalPresetId(input.verticalPresetId);
5159
+ if (!presetId) {
5160
+ return {
5161
+ ok: false,
5162
+ error: "unknown_vertical_preset"
5163
+ };
4845
5164
  }
4846
- if (typeof saleStartsAt === "string" && typeof saleEndsAt === "string" && Date.parse(saleEndsAt) <= Date.parse(saleStartsAt)) {
4847
- issues.push(issue$1([...path, "saleEndsAt"], "Ticket saleEndsAt must be after saleStartsAt"));
5165
+ const presetDefinition = getVerticalPresetDefinition(presetId);
5166
+ if (!isEventSeriesVerticalPresetDefinition(presetDefinition)) {
5167
+ return {
5168
+ ok: false,
5169
+ error: "non_event_series_preset"
5170
+ };
4848
5171
  }
4849
- const cmsTicketTypeId = raw.cmsTicketTypeId;
4850
- if (cmsTicketTypeId !== undefined && cmsTicketTypeId !== null && (typeof cmsTicketTypeId !== "string" || cmsTicketTypeId.length === 0)) {
4851
- issues.push(issue$1([...path, "cmsTicketTypeId"], "Ticket cmsTicketTypeId must be a non-empty string"));
5172
+ if (eventPresetForVerticalPreset(presetDefinition) !== input.canonicalPreset) {
5173
+ return {
5174
+ ok: false,
5175
+ error: "canonical_behaviour_mismatch"
5176
+ };
4852
5177
  }
4853
- if (issues.length > 0) return issues;
4854
5178
  return {
4855
- identifier,
4856
- ...cmsTicketTypeId ? { cmsTicketTypeId } : {},
4857
- name,
4858
- description: typeof raw.description === "string" ? raw.description : null,
4859
- priceAmount,
4860
- isActive: typeof raw.isActive === "boolean" ? raw.isActive : true,
4861
- sortOrder: isWholeNumber(sortOrder) ? sortOrder : index,
4862
- capacityLimit: isWholeNumber(capacityLimit) ? capacityLimit : null,
4863
- attendanceScope: attendanceScope === "in_person" || attendanceScope === "online" || attendanceScope === "both" ? attendanceScope : "both",
4864
- saleStartsAt: typeof saleStartsAt === "string" ? saleStartsAt : null,
4865
- saleEndsAt: typeof saleEndsAt === "string" ? saleEndsAt : null
5179
+ ok: true,
5180
+ preset: presetDefinition
4866
5181
  };
4867
5182
  }
4868
- function validateTicketTypes(rawTicketTypes, currency) {
4869
- if (!Array.isArray(rawTicketTypes) || rawTicketTypes.length === 0) {
4870
- return [issue$1(["ticketTypes"], "Ticket-type pricing requires at least one ticket type")];
4871
- }
4872
- const issues = [];
4873
- const ticketTypes = [];
4874
- const seenIdentifiers = new Set();
4875
- rawTicketTypes.forEach((rawTicketType, index) => {
4876
- const rawIdentifier = isRecord(rawTicketType) ? rawTicketType.identifier : undefined;
4877
- const hasValidIdentifier = typeof rawIdentifier === "string" && identifierRegex.test(rawIdentifier);
4878
- const isDuplicateIdentifier = hasValidIdentifier && seenIdentifiers.has(rawIdentifier);
4879
- if (isDuplicateIdentifier) {
4880
- issues.push(issue$1([
4881
- "ticketTypes",
4882
- String(index),
4883
- "identifier"
4884
- ], `Duplicate ticket identifier "${rawIdentifier}"`));
4885
- } else if (hasValidIdentifier) {
4886
- seenIdentifiers.add(rawIdentifier);
4887
- }
4888
- const parsed = validateTicketType(rawTicketType, index, currency);
4889
- if (Array.isArray(parsed)) {
4890
- issues.push(...parsed);
4891
- return;
4892
- }
4893
- if (isDuplicateIdentifier) {
4894
- return;
4895
- }
4896
- ticketTypes.push(parsed);
4897
- });
4898
- if (issues.length > 0) return issues;
4899
- return sortSdkEventTicketTypes(ticketTypes);
4900
- }
4901
- function isPricingIssueArray(value) {
4902
- const first = value[0];
4903
- return first !== undefined && "message" in first;
4904
- }
4905
- function sortSdkEventTicketTypes(ticketTypes) {
4906
- return [...ticketTypes].sort((left, right) => {
4907
- const leftOrder = left.sortOrder ?? Number.MAX_SAFE_INTEGER;
4908
- const rightOrder = right.sortOrder ?? Number.MAX_SAFE_INTEGER;
4909
- if (leftOrder !== rightOrder) return leftOrder - rightOrder;
4910
- const identifierCompare = left.identifier.localeCompare(right.identifier);
4911
- if (identifierCompare !== 0) return identifierCompare;
4912
- return left.name.localeCompare(right.name);
5183
+ function getSelectedCanonicalBehaviours(presetIds) {
5184
+ return presetIds.map((presetId) => getVerticalPresetDefinition(presetId).canonicalBehaviour);
5185
+ }
5186
+ function getSelectedPresetForBehaviour(input) {
5187
+ return input.presetIds.map(getVerticalPresetDefinition).find((presetDefinition) => presetDefinition.canonicalBehaviour === input.canonicalBehaviour) ?? null;
5188
+ }
5189
+ function getVerticalPresetSelectionOptions(config) {
5190
+ const resolved = resolveSiteVerticalBookingConfig(config);
5191
+ if (resolved.kind !== "configured") return [];
5192
+ const selectedIds = new Set(resolved.config.selectedPresetIds);
5193
+ const selectedBehaviours = new Set(resolved.selectedPresets.map((presetDefinition) => presetDefinition.canonicalBehaviour));
5194
+ return getVerticalPresetsForVertical(resolved.config.verticalId, { availability: "selectable" }).flatMap((presetDefinition) => {
5195
+ const checked = selectedIds.has(presetDefinition.id);
5196
+ if (!checked && selectedBehaviours.has(presetDefinition.canonicalBehaviour)) return [];
5197
+ return [{
5198
+ preset: presetDefinition,
5199
+ checked,
5200
+ disabledReason: checked && selectedIds.size === 1 ? "last_selected_preset" : null
5201
+ }];
4913
5202
  });
4914
5203
  }
4915
- function suggestSdkEventTicketIdentifier(name, usedIdentifiers) {
4916
- const base = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "ticket";
4917
- let candidate = base;
4918
- let suffix = 2;
4919
- while (usedIdentifiers.has(candidate)) {
4920
- candidate = `${base}-${suffix}`;
4921
- suffix += 1;
5204
+ function setVerticalPresetSelected(config, presetId, selected) {
5205
+ const resolved = resolveSiteVerticalBookingConfig(config);
5206
+ const presetDefinition = getVerticalPresetDefinition(presetId);
5207
+ const verticalId = resolved.config.verticalId;
5208
+ if (!verticalId) {
5209
+ return {
5210
+ ok: false,
5211
+ error: "unconfigured",
5212
+ config: DEFAULT_SITE_VERTICAL_BOOKING_CONFIG
5213
+ };
4922
5214
  }
4923
- return candidate;
4924
- }
4925
- function classifySdkEventPricing(rawInput) {
4926
- if (rawInput === undefined || rawInput === null) {
5215
+ const fallbackConfig = resolved.kind === "configured" ? resolved.config : DEFAULT_SITE_VERTICAL_BOOKING_CONFIG;
5216
+ if (presetDefinition.verticalId !== verticalId) {
4927
5217
  return {
4928
- ok: true,
4929
- pricing: { kind: "free" }
5218
+ ok: false,
5219
+ error: "cross_vertical_preset",
5220
+ config: fallbackConfig
4930
5221
  };
4931
5222
  }
4932
- if (!isRecord(rawInput)) {
5223
+ if (presetDefinition.availability === "future") {
4933
5224
  return {
4934
5225
  ok: false,
4935
- issues: [issue$1([], "Pricing must be an object")]
5226
+ error: "future_preset",
5227
+ config: fallbackConfig
4936
5228
  };
4937
5229
  }
4938
- const record = rawInput;
4939
- const kind = typeof record.kind === "string" ? record.kind : record.pricingMode === "ticket_types" ? "ticket_types" : record.pricingMode === "fixed_price" ? "fixed_price" : record.pricingMode === "free" ? "free" : record.requiresPayment || typeof record.priceAmount === "number" && record.priceAmount > 0 ? "fixed_price" : "free";
4940
- const acceptsPasses = typeof record.acceptsPasses === "boolean" ? record.acceptsPasses : undefined;
4941
- const acceptsMemberships = typeof record.acceptsMemberships === "boolean" ? record.acceptsMemberships : undefined;
4942
- const priceDisplay = typeof record.priceDisplay === "string" ? record.priceDisplay : null;
4943
- switch (kind) {
4944
- case "free": return {
4945
- ok: true,
4946
- pricing: {
4947
- kind: "free",
4948
- priceDisplay,
4949
- ...acceptsPasses !== undefined ? { acceptsPasses } : {},
4950
- ...acceptsMemberships !== undefined ? { acceptsMemberships } : {}
4951
- }
5230
+ if (resolved.kind !== "configured") {
5231
+ return {
5232
+ ok: false,
5233
+ error: "unconfigured",
5234
+ config: DEFAULT_SITE_VERTICAL_BOOKING_CONFIG
4952
5235
  };
4953
- case "fixed_price": {
4954
- const issues = [];
4955
- const priceAmount = record.priceAmount;
4956
- if (!isWholeNumber(priceAmount) || priceAmount < 0) {
4957
- issues.push(issue$1(["priceAmount"], "Fixed-price events require a non-negative integer priceAmount"));
4958
- }
4959
- const rawPriceCurrency = record.priceCurrency;
4960
- const priceCurrency = rawPriceCurrency === undefined || rawPriceCurrency === null ? "USD" : isCurrency(rawPriceCurrency) ? rawPriceCurrency : null;
4961
- if (rawPriceCurrency !== undefined && rawPriceCurrency !== null && !isCurrency(rawPriceCurrency)) {
4962
- issues.push(issue$1(["priceCurrency"], "priceCurrency must be a 3-letter uppercase currency code"));
4963
- }
4964
- if (issues.length > 0) return {
4965
- ok: false,
4966
- issues
4967
- };
4968
- if (!isWholeNumber(priceAmount) || priceCurrency === null) {
4969
- return {
4970
- ok: false,
4971
- issues: [issue$1(["priceAmount"], "Invalid fixed-price event pricing")]
4972
- };
4973
- }
4974
- return {
4975
- ok: true,
4976
- pricing: {
4977
- kind: "fixed_price",
4978
- priceAmount,
4979
- priceCurrency,
4980
- priceDisplay,
4981
- ...acceptsPasses !== undefined ? { acceptsPasses } : {},
4982
- ...acceptsMemberships !== undefined ? { acceptsMemberships } : {}
4983
- }
4984
- };
4985
- }
4986
- case "ticket_types": {
4987
- const priceCurrency = record.priceCurrency;
4988
- if (!isCurrency(priceCurrency)) {
4989
- return {
4990
- ok: false,
4991
- issues: [issue$1(["priceCurrency"], "Ticket-type pricing requires a 3-letter uppercase priceCurrency")]
4992
- };
4993
- }
4994
- const ticketTypes = record.ticketTypes === undefined || record.ticketTypes === null ? [] : validateTicketTypes(record.ticketTypes, priceCurrency);
4995
- if (isPricingIssueArray(ticketTypes)) {
4996
- return {
4997
- ok: false,
4998
- issues: ticketTypes
4999
- };
5000
- }
5236
+ }
5237
+ const selectedIds = new Set(resolved.config.selectedPresetIds);
5238
+ if (!selected) {
5239
+ if (selectedIds.size <= 1) {
5001
5240
  return {
5002
- ok: true,
5003
- pricing: {
5004
- kind: "ticket_types",
5005
- priceCurrency,
5006
- priceDisplay,
5007
- ...acceptsPasses !== undefined ? { acceptsPasses } : {},
5008
- ...acceptsMemberships !== undefined ? { acceptsMemberships } : {},
5009
- ticketTypes
5010
- }
5241
+ ok: false,
5242
+ error: "last_selected_preset",
5243
+ config: resolved.config
5011
5244
  };
5012
5245
  }
5013
- default: return {
5014
- ok: false,
5015
- issues: [issue$1(["kind"], "Pricing kind must be free, fixed_price, or ticket_types")]
5016
- };
5017
- }
5018
- }
5019
-
5020
- //#endregion
5021
- //#region ../api/src/sdk-event-schedule.ts
5022
- const timeRegex = /^([01]\d|2[0-3]):([0-5]\d)(:([0-5]\d))?$/;
5023
- const explicitOffsetDateTimeRegex = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?(\.\d{1,3})?(Z|[+-]([01]\d|2[0-3]):[0-5]\d)$/;
5024
- const localDateTimeRegex = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?(\.\d{1,3})?$/;
5025
- function issue(path, message) {
5026
- return {
5027
- path,
5028
- message
5029
- };
5030
- }
5031
- function isValidDateParts(year, month, day) {
5032
- const date = new Date(Date.UTC(year, month - 1, day));
5033
- return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
5034
- }
5035
- function isZeroFraction(fraction) {
5036
- return fraction === undefined || /^\.0+$/.test(fraction);
5037
- }
5038
- function datePart(year, month, day) {
5039
- return `${year}-${month}-${day}`;
5040
- }
5041
- function timePart(hours, minutes, seconds = "00") {
5042
- return `${hours}:${minutes}:${seconds}`;
5043
- }
5044
- function parseSdkEventDateTime(value) {
5045
- const explicitMatch = explicitOffsetDateTimeRegex.exec(value);
5046
- if (explicitMatch) {
5047
- const [, yearRaw$1, monthRaw$1, dayRaw$1, hours$1, minutes$1, seconds$1 = "00", fraction$1] = explicitMatch;
5048
- if (!yearRaw$1 || !monthRaw$1 || !dayRaw$1 || !hours$1 || !minutes$1) return null;
5049
- const year$1 = Number(yearRaw$1);
5050
- const month$1 = Number(monthRaw$1);
5051
- const day$1 = Number(dayRaw$1);
5052
- if (!isValidDateParts(year$1, month$1, day$1) || !isZeroFraction(fraction$1)) return null;
5053
- const epochMs$1 = Date.parse(value);
5054
- if (!Number.isFinite(epochMs$1)) return null;
5246
+ selectedIds.delete(presetId);
5247
+ const nextConfig = createSiteVerticalBookingConfig(verticalId, orderPresetIdsForVertical(verticalId, selectedIds));
5055
5248
  return {
5056
- epochMs: epochMs$1,
5057
- date: datePart(yearRaw$1, monthRaw$1, dayRaw$1),
5058
- time: timePart(hours$1, minutes$1, seconds$1),
5059
- iso: new Date(epochMs$1).toISOString()
5249
+ ok: true,
5250
+ config: nextConfig ?? DEFAULT_SITE_VERTICAL_BOOKING_CONFIG
5060
5251
  };
5061
5252
  }
5062
- const localMatch = localDateTimeRegex.exec(value);
5063
- if (!localMatch) return null;
5064
- const [, yearRaw, monthRaw, dayRaw, hours, minutes, seconds = "00", fraction] = localMatch;
5065
- if (!yearRaw || !monthRaw || !dayRaw || !hours || !minutes) return null;
5066
- const year = Number(yearRaw);
5067
- const month = Number(monthRaw);
5068
- const day = Number(dayRaw);
5069
- if (!isValidDateParts(year, month, day) || !isZeroFraction(fraction)) return null;
5070
- const epochMs = Date.UTC(year, month - 1, day, Number(hours), Number(minutes), Number(seconds));
5071
- return {
5072
- epochMs,
5073
- date: datePart(yearRaw, monthRaw, dayRaw),
5074
- time: timePart(hours, minutes, seconds),
5075
- iso: new Date(epochMs).toISOString()
5076
- };
5077
- }
5078
- function durationMinutesBetween(startsAt, endsAt) {
5079
- if (endsAt.epochMs <= startsAt.epochMs) return null;
5080
- const durationMs = endsAt.epochMs - startsAt.epochMs;
5081
- if (durationMs % 6e4 !== 0) return null;
5082
- return durationMs / 6e4;
5083
- }
5084
- function exceedsMaxDuration(durationMinutes, options) {
5085
- return options.maxDurationMinutes !== undefined && durationMinutes > options.maxDurationMinutes;
5086
- }
5087
- function maxDurationMessage(options) {
5088
- return options.maxDurationDays === undefined ? "Duration is above the allowed maximum" : `Duration must be ${options.maxDurationDays} days or less`;
5089
- }
5090
- function classifyCanonicalSchedule(input, options) {
5091
- const issues = [];
5092
- if (options.requireCanonicalFields && input.recurrenceRule === undefined) {
5093
- issues.push(issue(["recurrenceRule"], "recurrenceRule is required"));
5094
- }
5095
- if (input.defaultStartTime !== undefined && input.defaultStartTime !== null && !timeRegex.test(input.defaultStartTime)) {
5096
- issues.push(issue(["defaultStartTime"], "Invalid time format (HH:MM or HH:MM:SS)"));
5097
- }
5098
- if (options.requireCanonicalFields && !input.defaultStartTime) {
5099
- issues.push(issue(["defaultStartTime"], "defaultStartTime is required"));
5100
- }
5101
- if (options.requireCanonicalFields && input.durationMinutes === undefined) {
5102
- issues.push(issue(["durationMinutes"], "durationMinutes is required"));
5103
- }
5104
- if (input.durationMinutes !== undefined && input.durationMinutes !== null) {
5105
- if (input.durationMinutes < 1) {
5106
- issues.push(issue(["durationMinutes"], "durationMinutes must be at least 1"));
5107
- } else if (exceedsMaxDuration(input.durationMinutes, options)) {
5108
- issues.push(issue(["durationMinutes"], maxDurationMessage(options)));
5253
+ for (const existingPresetId of selectedIds) {
5254
+ const existingPreset = getVerticalPresetDefinition(existingPresetId);
5255
+ if (existingPreset.canonicalBehaviour === presetDefinition.canonicalBehaviour) {
5256
+ selectedIds.delete(existingPresetId);
5109
5257
  }
5110
5258
  }
5111
- if (issues.length > 0) return {
5112
- ok: false,
5113
- issues
5114
- };
5259
+ selectedIds.add(presetId);
5115
5260
  return {
5116
5261
  ok: true,
5117
- kind: "canonical",
5118
- canonical: {
5119
- recurrenceRule: input.recurrenceRule ?? null,
5120
- startDate: input.startDate,
5121
- timezone: input.timezone,
5122
- defaultStartTime: input.defaultStartTime ?? "09:00:00",
5123
- durationMinutes: input.durationMinutes ?? 60
5124
- }
5262
+ config: createSiteVerticalBookingConfig(verticalId, orderPresetIdsForVertical(verticalId, selectedIds)) ?? resolved.config
5125
5263
  };
5126
5264
  }
5127
- function classifySdkEventSchedule(input, options = {}) {
5128
- const issues = [];
5129
- const hasStartsAt = input.startsAt !== undefined && input.startsAt !== null;
5130
- const hasEndsAt = input.endsAt !== undefined && input.endsAt !== null;
5131
- const hasDateTimeDuration = hasStartsAt && input.durationMinutes !== undefined && input.durationMinutes !== null;
5132
- const hasCanonicalStart = input.startDate !== undefined || input.defaultStartTime !== undefined;
5133
- if (!hasStartsAt && !hasEndsAt) {
5134
- return classifyCanonicalSchedule(input, options);
5135
- }
5136
- if (hasStartsAt && hasCanonicalStart) {
5137
- issues.push(issue(["startsAt"], "Use either startsAt or startDate/defaultStartTime, not both"));
5138
- }
5139
- if (input.recurrenceRule) {
5140
- issues.push(issue(["recurrenceRule"], "startsAt schedules are only supported for one-off events"));
5141
- }
5142
- if (!hasStartsAt) {
5143
- issues.push(issue(["startsAt"], "startsAt is required when endsAt is provided"));
5144
- }
5145
- if (!hasEndsAt && !hasDateTimeDuration) {
5146
- issues.push(issue(["endsAt"], "Provide either endsAt or durationMinutes with startsAt"));
5147
- }
5148
- if (hasEndsAt && hasDateTimeDuration) {
5149
- issues.push(issue(["endsAt"], "Use either endsAt or durationMinutes with startsAt, not both"));
5150
- }
5151
- const startsAt = typeof input.startsAt === "string" ? parseSdkEventDateTime(input.startsAt) : null;
5152
- const endsAt = typeof input.endsAt === "string" ? parseSdkEventDateTime(input.endsAt) : null;
5153
- if (hasStartsAt && !startsAt) {
5154
- issues.push(issue(["startsAt"], "Invalid datetime format. Use ISO datetime format, e.g. 2026-06-01T10:00:00Z"));
5155
- }
5156
- if (hasEndsAt && !endsAt) {
5157
- issues.push(issue(["endsAt"], "Invalid datetime format. Use ISO datetime format, e.g. 2026-06-07T10:00:00Z"));
5158
- }
5159
- if (startsAt && endsAt) {
5160
- const durationMinutes = durationMinutesBetween(startsAt, endsAt);
5161
- if (durationMinutes === null) {
5162
- issues.push(issue(["endsAt"], "endsAt must be after startsAt and align to a whole minute"));
5163
- } else if (exceedsMaxDuration(durationMinutes, options)) {
5164
- issues.push(issue(["endsAt"], maxDurationMessage(options)));
5165
- }
5166
- if (issues.length === 0 && durationMinutes !== null) {
5265
+ function setVerticalPresetsSelected(config, presetIds) {
5266
+ const resolved = resolveSiteVerticalBookingConfig(config);
5267
+ const verticalId = resolved.config.verticalId;
5268
+ if (!verticalId) {
5269
+ return {
5270
+ ok: false,
5271
+ error: "unconfigured",
5272
+ config: DEFAULT_SITE_VERTICAL_BOOKING_CONFIG
5273
+ };
5274
+ }
5275
+ const fallbackConfig = resolved.kind === "configured" ? resolved.config : DEFAULT_SITE_VERTICAL_BOOKING_CONFIG;
5276
+ if (presetIds.length === 0) {
5277
+ return {
5278
+ ok: false,
5279
+ error: "last_selected_preset",
5280
+ config: fallbackConfig
5281
+ };
5282
+ }
5283
+ const presetByBehaviour = new Map();
5284
+ for (const presetId of presetIds) {
5285
+ const presetDefinition = getVerticalPresetDefinition(presetId);
5286
+ if (presetDefinition.verticalId !== verticalId) {
5167
5287
  return {
5168
- ok: true,
5169
- kind: "start-end",
5170
- canonical: {
5171
- recurrenceRule: null,
5172
- startDate: startsAt.date,
5173
- timezone: input.timezone,
5174
- defaultStartTime: startsAt.time,
5175
- durationMinutes
5176
- },
5177
- startsAtIso: startsAt.iso,
5178
- endsAtIso: endsAt.iso
5288
+ ok: false,
5289
+ error: "cross_vertical_preset",
5290
+ config: fallbackConfig
5179
5291
  };
5180
5292
  }
5181
- }
5182
- if (startsAt && hasDateTimeDuration && input.durationMinutes !== undefined && input.durationMinutes !== null) {
5183
- if (input.durationMinutes < 1) {
5184
- issues.push(issue(["durationMinutes"], "durationMinutes must be at least 1"));
5185
- } else if (exceedsMaxDuration(input.durationMinutes, options)) {
5186
- issues.push(issue(["durationMinutes"], maxDurationMessage(options)));
5187
- }
5188
- if (issues.length === 0) {
5189
- const endsAtMs = startsAt.epochMs + input.durationMinutes * 6e4;
5293
+ if (presetDefinition.availability === "future") {
5190
5294
  return {
5191
- ok: true,
5192
- kind: "start-duration",
5193
- canonical: {
5194
- recurrenceRule: null,
5195
- startDate: startsAt.date,
5196
- timezone: input.timezone,
5197
- defaultStartTime: startsAt.time,
5198
- durationMinutes: input.durationMinutes
5199
- },
5200
- startsAtIso: startsAt.iso,
5201
- endsAtIso: new Date(endsAtMs).toISOString()
5295
+ ok: false,
5296
+ error: "future_preset",
5297
+ config: fallbackConfig
5202
5298
  };
5203
5299
  }
5300
+ presetByBehaviour.set(presetDefinition.canonicalBehaviour, presetId);
5204
5301
  }
5205
- return {
5206
- ok: false,
5207
- issues
5208
- };
5209
- }
5210
-
5211
- //#endregion
5212
- //#region ../api/src/sdk-event-upsert.ts
5213
- const sdkEventOccurrenceWireSchema = z.object({
5214
- startsAt: z.string(),
5215
- endsAt: z.string(),
5216
- timeZone: z.string()
5217
- });
5218
- const sdkEventOccurrenceInputSchema = z.object({
5219
- startsAt: z.string().datetime({ offset: true }),
5220
- endsAt: z.string().datetime({ offset: true }),
5221
- timeZone: z.string().min(1).refine((value) => parseBookingTimeZone(value).ok, "Invalid IANA timezone")
5222
- }).refine(({ startsAt, endsAt }) => Date.parse(endsAt) > Date.parse(startsAt), {
5223
- path: ["endsAt"],
5224
- message: "endsAt must be after startsAt"
5225
- });
5226
- const sdkEventUpsertOptionalFieldSchemas = {
5227
- path: z.string().startsWith("/", "path must start with /").nullable().optional(),
5228
- status: z.enum([
5229
- "draft",
5230
- "active",
5231
- "archived",
5232
- "cancelled",
5233
- "completed"
5234
- ]).optional(),
5235
- visibility: z.enum([
5236
- "private",
5237
- "public",
5238
- "unlisted"
5239
- ]).optional(),
5240
- preset: z.enum([
5241
- "event",
5242
- "class",
5243
- "course",
5244
- "workshop",
5245
- "retreat",
5246
- "performance"
5247
- ]).optional(),
5248
- bookingScope: z.enum(["single_occurrence", "whole_series"]).optional(),
5249
- occurrences: z.array(sdkEventOccurrenceWireSchema).optional(),
5250
- capacity: z.number().int().positive().nullable().optional()
5251
- };
5252
- function timingIssue(path, message) {
5253
- return {
5254
- path,
5255
- message
5256
- };
5257
- }
5258
- function parseExplicitOccurrence(occurrence, index) {
5259
- const startsAt = parseUtcIsoDateTimeString(occurrence.startsAt);
5260
- const endsAt = parseUtcIsoDateTimeString(occurrence.endsAt);
5261
- const timeZone = parseBookingTimeZone(occurrence.timeZone);
5262
- const path = ["occurrences", String(index)];
5263
- const issues = [];
5264
- if (!startsAt.ok) issues.push(timingIssue([...path, "startsAt"], "startsAt must be a valid offset-aware ISO datetime"));
5265
- if (!endsAt.ok) issues.push(timingIssue([...path, "endsAt"], "endsAt must be a valid offset-aware ISO datetime"));
5266
- if (!timeZone.ok) issues.push(timingIssue([...path, "timeZone"], "Invalid IANA timezone"));
5267
- if (!startsAt.ok || !endsAt.ok || !timeZone.ok) return {
5268
- ok: false,
5269
- issues
5270
- };
5271
- if (Date.parse(endsAt.value) <= Date.parse(startsAt.value)) {
5302
+ if (resolved.kind !== "configured") {
5272
5303
  return {
5273
5304
  ok: false,
5274
- issues: [timingIssue([...path, "endsAt"], "endsAt must be after startsAt")]
5305
+ error: "unconfigured",
5306
+ config: DEFAULT_SITE_VERTICAL_BOOKING_CONFIG
5275
5307
  };
5276
5308
  }
5309
+ const nextConfig = createSiteVerticalBookingConfig(verticalId, orderPresetIdsForVertical(verticalId, new Set(presetByBehaviour.values())));
5277
5310
  return {
5278
5311
  ok: true,
5279
- value: {
5280
- startsAt: startsAt.value,
5281
- endsAt: endsAt.value,
5282
- timeZone: timeZone.value
5283
- }
5312
+ config: nextConfig ?? resolved.config
5284
5313
  };
5285
5314
  }
5286
- function classifySdkEventTiming(input, options = {}) {
5287
- if (input.occurrences && input.occurrences.length > 0) {
5288
- if (input.bookingScope === undefined) {
5289
- return {
5290
- ok: false,
5291
- issues: [timingIssue(["bookingScope"], "bookingScope is required when occurrences are provided")]
5292
- };
5293
- }
5294
- const occurrences = [];
5295
- const starts = new Set();
5296
- let timeZone;
5297
- const issues = [];
5298
- input.occurrences.forEach((occurrence, index) => {
5299
- const parsed = parseExplicitOccurrence(occurrence, index);
5300
- if (!parsed.ok) {
5301
- issues.push(...parsed.issues);
5302
- return;
5303
- }
5304
- if (starts.has(parsed.value.startsAt)) {
5305
- issues.push(timingIssue([
5306
- "occurrences",
5307
- String(index),
5308
- "startsAt"
5309
- ], "Occurrence start times must be unique within a series"));
5310
- return;
5311
- }
5312
- if (timeZone && timeZone !== parsed.value.timeZone) {
5313
- issues.push(timingIssue([
5314
- "occurrences",
5315
- String(index),
5316
- "timeZone"
5317
- ], "All event occurrences must use the same timezone"));
5318
- return;
5319
- }
5320
- starts.add(parsed.value.startsAt);
5321
- timeZone ??= parsed.value.timeZone;
5322
- occurrences.push(parsed.value);
5323
- });
5324
- const [first, ...rest] = occurrences;
5325
- if (issues.length > 0 || !first || !timeZone) return {
5326
- ok: false,
5327
- issues
5328
- };
5329
- return {
5330
- ok: true,
5331
- timingAuthority: {
5332
- kind: "explicit_occurrences",
5333
- bookingScope: input.bookingScope,
5334
- occurrences: [first, ...rest],
5335
- timeZone
5336
- }
5337
- };
5338
- }
5339
- if (input.schedule === undefined || typeof input.schedule !== "object" || input.schedule === null || Array.isArray(input.schedule)) {
5315
+ /**
5316
+ * Return a configured selection in the catalog's canonical order.
5317
+ *
5318
+ * This deliberately delegates ordering and selection semantics to the core
5319
+ * authority instead of making dashboard planners maintain a second catalog
5320
+ * ordering implementation. Runtime-invalid values are reported as a closed
5321
+ * result so this pure helper never throws while planning.
5322
+ */
5323
+ function canonicalizeConfiguredBookingVerticalConfig(config) {
5324
+ const resolved = resolveSiteVerticalBookingConfig(config);
5325
+ if (resolved.kind === "invalid") {
5340
5326
  return {
5341
5327
  ok: false,
5342
- issues: [timingIssue(["schedule"], "Either schedule or a non-empty occurrences list is required")]
5328
+ issues: resolved.issues
5343
5329
  };
5344
5330
  }
5345
- const schedule = classifySdkEventSchedule(input.schedule, options);
5346
- return schedule.ok ? {
5347
- ok: true,
5348
- timingAuthority: {
5349
- kind: "authored_schedule",
5350
- schedule: schedule.canonical,
5351
- bookingScope: input.bookingScope
5352
- }
5353
- } : {
5354
- ok: false,
5355
- issues: schedule.issues.map((issue$3) => timingIssue(["schedule", ...issue$3.path], issue$3.message))
5356
- };
5357
- }
5358
- function parseSdkEventUpsertInput(input, options = {}) {
5359
- const runRefs = new Set(input.runRefs);
5360
- const runsByIdentifier = new Map(input.runs.map((run) => [run.identifier, run]));
5361
- if (input.runRefs.length !== runRefs.size || runsByIdentifier.size !== input.runs.length || input.runRefs.length !== input.runs.length || input.runRefs.some((ref) => !runsByIdentifier.has(ref)) || input.runs.some((run) => run.eventRef !== input.identifier)) {
5331
+ if (resolved.kind === "unconfigured") {
5362
5332
  return {
5363
5333
  ok: false,
5364
- issues: [{
5365
- source: "graph",
5366
- path: ["runRefs"],
5367
- message: "Event runRefs and runs must form one complete parent graph",
5368
- issue: { kind: "incoherentParentRunGraph" }
5369
- }]
5334
+ issues: [{ code: "missing_vertical" }]
5370
5335
  };
5371
5336
  }
5372
- const runForRef = (ref) => {
5373
- const run = runsByIdentifier.get(ref);
5374
- if (!run) throw new Error("Unreachable missing Event run after graph validation");
5375
- return run;
5376
- };
5377
- const firstRun = runForRef(input.runRefs[0]);
5378
- const remainingRuns = input.runRefs.slice(1).map(runForRef);
5379
- if (firstRun.bookingScope === "single_occurrence" && remainingRuns.length > 0 || remainingRuns.some((run) => run.bookingScope !== firstRun.bookingScope)) {
5337
+ const selection = setVerticalPresetsSelected(resolved.config, resolved.config.selectedPresetIds);
5338
+ if (!selection.ok) {
5339
+ const issue = selection.error === "cross_vertical_preset" ? { code: "preset_vertical_mismatch" } : selection.error === "future_preset" ? { code: "future_preset_selected" } : selection.error === "last_selected_preset" ? { code: "empty_selected_presets" } : { code: "missing_vertical" };
5380
5340
  return {
5381
5341
  ok: false,
5382
- issues: [{
5383
- source: "graph",
5384
- path: ["runs"],
5385
- message: "An Event must contain one ordinary run or one or more whole-series runs",
5386
- issue: { kind: "incoherentParentRunGraph" }
5387
- }]
5342
+ issues: [issue]
5388
5343
  };
5389
5344
  }
5390
- const firstResult = parseSdkEventRunUpsertInput(firstRun, 0, options);
5391
- const remainingResults = remainingRuns.map((run, index) => parseSdkEventRunUpsertInput(run, index + 1, options));
5392
- const issues = [firstResult, ...remainingResults].flatMap((result) => result.ok ? [] : result.issues);
5393
- if (issues.length > 0) return {
5345
+ const canonical = resolveSiteVerticalBookingConfig(selection.config);
5346
+ return canonical.kind === "configured" ? {
5347
+ ok: true,
5348
+ config: canonical.config
5349
+ } : canonical.kind === "invalid" ? {
5394
5350
  ok: false,
5395
- issues
5396
- };
5397
- if (!firstResult.ok) return {
5351
+ issues: canonical.issues
5352
+ } : {
5398
5353
  ok: false,
5399
- issues: firstResult.issues
5354
+ issues: [{ code: "missing_vertical" }]
5400
5355
  };
5401
- const parsedRemaining = remainingResults.flatMap((result) => result.ok ? [result.value] : []);
5402
- return {
5403
- ok: true,
5404
- value: {
5405
- ...input,
5406
- runs: [firstResult.value, ...parsedRemaining]
5407
- }
5408
- };
5409
- }
5410
- function toSdkEventRunUpsertBody(input) {
5411
- const { timingAuthority, ...common } = input;
5412
- switch (timingAuthority.kind) {
5413
- case "authored_schedule": return {
5414
- ...common,
5415
- ...timingAuthority.bookingScope === undefined ? {} : { bookingScope: timingAuthority.bookingScope },
5416
- schedule: timingAuthority.schedule
5417
- };
5418
- case "explicit_occurrences": return {
5419
- ...common,
5420
- bookingScope: timingAuthority.bookingScope,
5421
- occurrences: timingAuthority.occurrences
5422
- };
5423
- default: return timingAuthority;
5424
- }
5425
5356
  }
5426
- function toSdkEventUpsertBody(input) {
5427
- const [first, ...rest] = input.runs;
5428
- return {
5429
- ...input,
5430
- runs: [toSdkEventRunUpsertBody(first), ...rest.map(toSdkEventRunUpsertBody)]
5431
- };
5357
+ /** Compare configured booking selections by catalog order, not authored array order. */
5358
+ function areConfiguredBookingVerticalConfigsEqual(left, right) {
5359
+ const canonicalLeft = canonicalizeConfiguredBookingVerticalConfig(left);
5360
+ const canonicalRight = canonicalizeConfiguredBookingVerticalConfig(right);
5361
+ if (!canonicalLeft.ok || !canonicalRight.ok) return false;
5362
+ return canonicalLeft.config.verticalId === canonicalRight.config.verticalId && canonicalLeft.config.selectedPresetIds.length === canonicalRight.config.selectedPresetIds.length && canonicalLeft.config.selectedPresetIds.every((presetId, index) => presetId === canonicalRight.config.selectedPresetIds[index]);
5432
5363
  }
5433
- function parseSdkEventRunUpsertInput(input, runIndex, options) {
5434
- const timingResult = classifySdkEventTiming(input, options.schedule);
5435
- const pricingResult = classifySdkEventPricing(input.pricing);
5436
- const paymentTermsResult = classifySdkEventPaymentTerms(input.paymentTerms);
5437
- const issues = [];
5438
- if (timingResult.ok && pricingResult.ok && paymentTermsResult.ok) {
5439
- const { schedule: _schedule, occurrences: _occurrences, bookingScope: _bookingScope, pricing: _pricing, paymentTerms: _paymentTerms, ...common } = input;
5440
- return {
5441
- ok: true,
5442
- value: {
5443
- ...common,
5444
- timingAuthority: timingResult.timingAuthority,
5445
- pricing: pricingResult.pricing,
5446
- paymentTerms: paymentTermsResult.paymentTerms
5447
- }
5448
- };
5449
- }
5450
- if (!timingResult.ok) {
5451
- issues.push(...timingResult.issues.map((timing) => ({
5452
- runIndex,
5453
- source: "timing",
5454
- path: timing.path,
5455
- message: timing.message,
5456
- issue: timing
5457
- })));
5458
- }
5459
- if (!pricingResult.ok) {
5460
- issues.push(...pricingResult.issues.map((pricingIssue) => ({
5461
- runIndex,
5462
- source: "pricing",
5463
- path: pricingIssue.path,
5464
- message: pricingIssue.message,
5465
- issue: pricingIssue
5466
- })));
5467
- }
5468
- if (!paymentTermsResult.ok) {
5469
- issues.push(...paymentTermsResult.issues.map((paymentTermsIssue) => ({
5470
- runIndex,
5471
- source: "paymentTerms",
5472
- path: paymentTermsIssue.path,
5473
- message: paymentTermsIssue.message,
5474
- issue: paymentTermsIssue
5475
- })));
5476
- }
5477
- return {
5478
- ok: false,
5479
- issues
5480
- };
5364
+ function orderPresetIdsForVertical(verticalId, presetIds) {
5365
+ return getVerticalPresetsForVertical(verticalId, { availability: "selectable" }).map((presetDefinition) => presetDefinition.id).filter((id) => presetIds.has(id));
5481
5366
  }
5482
5367
 
5483
5368
  //#endregion
5484
- export { unsafeAsDetectedMimeType as $, getBookingLocalDateKey as A, publicEventPricingCurrency as B, normalizeLocationAddressText as C, formatBookingTime as D, formatBookingDateParts as E, toScheduledBookingDateRange as F, parseMediaAssetId as G, formatCurrencyAmount as H, toScheduledBookingDateTime as I, unsafeAsAppointmentResourceId as J, parsePageId as K, parseEventPageSurfaceSubject as L, getLocalScheduleTime as M, parseUtcIsoDateTimeString as N, formatLocalScheduleDate as O, toBookingTimeZone as P, unsafeAsDemoCapability as Q, formatBookingPriceDisplay as R, isPresent as S, formatBookingDate as T, assertNever as U, publicEventPricingType as V, parseEventSeriesId as W, unsafeAsBookingSessionId as X, unsafeAsBookingRequirementId as Y, unsafeAsDeclaredMimeType as Z, isCanonicalRichTextDoc as _, unsafeAsUploadSessionId as _t, classifySdkEventPricing as a, unsafeAsOccurrenceId as at, mapNonEmptyArray as b, ok as bt, stableJsonStringify as c, unsafeAsPositiveSeconds as ct, parseFooterModuleId as d, unsafeAsSiteFontId as dt, unsafeAsEntitlementId as et, parseMailchimpAudienceId as f, unsafeAsSiteId as ft, parseNewsletterSignupDestinationProvider as g, unsafeAsStoragePath as gt, parseNewsletterSignupAuthoringDestinationValue as h, unsafeAsStorageBucketName as ht, classifySdkEventSchedule as i, unsafeAsMediaAssetId as it, getLocalScheduleDate as j, formatLocalScheduleTime as k, footerNavigationLayouts as l, unsafeAsProductId as lt, parseNewsletterListId as m, unsafeAsStaffMemberId as mt, sdkEventOccurrenceInputSchema as n, unsafeAsEventSeriesId as nt, classifySdkEventPaymentTerms as o, unsafeAsOriginalFilename as ot, parseMailchimpTagName as p, unsafeAsSiteScopedStoragePath as pt, parseSiteId as q, sdkEventUpsertOptionalFieldSchemas as r, unsafeAsFileChecksumSha256 as rt, resolvePlanningDateInput as s, unsafeAsPositiveByteCount as st, classifySdkEventTiming as t, unsafeAsEventOfferingId as tt, parseFooterCompositionV1 as u, unsafeAsSafeDownloadFilename as ut, assertAllowedPublicUrl as v, unsafeAsUserId as vt, toLocationCoordinates as w, hasText as x, fromArray as y, err as yt, formatEventPricing as z };
5369
+ export { parseEventSeriesId as $, normalizeLocationAddressText as A, ok as At, parseUtcIsoDateTimeString as B, parseNewsletterSignupDestinationProvider as C, unsafeAsSiteScopedStoragePath as Ct, mapNonEmptyArray as D, unsafeAsUploadSessionId as Dt, fromArray as E, unsafeAsStoragePath as Et, formatLocalScheduleDate as F, parseEventPageSurfaceSubject as G, toScheduledBookingDateRange as H, formatLocalScheduleTime as I, publicEventPricingCurrency as J, formatBookingPriceDisplay as K, getBookingLocalDateKey as L, formatBookingDate as M, formatBookingDateParts as N, hasText as O, unsafeAsUserId as Ot, formatBookingTime as P, parseApiKeyId as Q, getLocalScheduleDate as R, parseNewsletterSignupAuthoringDestinationValue as S, unsafeAsSiteId as St, assertAllowedPublicUrl as T, unsafeAsStorageBucketName as Tt, toScheduledBookingDateTime as U, toBookingTimeZone as V, parseBookingTimeZone as W, formatCurrencyAmount as X, publicEventPricingType as Y, assertNever as Z, parseFooterCompositionV1 as _, unsafeAsPositiveByteCount as _t, parseVerticalPresetId as a, unsafeAsBookingRequirementId as at, parseMailchimpTagName as b, unsafeAsSafeDownloadFilename as bt, EVENT_PRESET_VALUES as c, unsafeAsDemoCapability as ct, resolvePlanningDateInput as d, unsafeAsEventOfferingId as dt, parseMediaAssetId as et, createSecret as f, unsafeAsEventSeriesId as ft, footerNavigationLayouts as g, unsafeAsOriginalFilename as gt, stableJsonStringify as h, unsafeAsOccurrenceId as ht, parseVerticalId as i, unsafeAsAppointmentResourceId as it, toLocationCoordinates as j, isPresent as k, err as kt, ORDINARY_EVENT_PRESET_VALUES as l, unsafeAsDetectedMimeType as lt, parseManagementKey as m, unsafeAsMediaAssetId as mt, canonicalizeConfiguredBookingVerticalConfig as n, parseSiteId as nt, resolveSiteVerticalBookingConfig as o, unsafeAsBookingSessionId as ot, exposeSecret as p, unsafeAsFileChecksumSha256 as pt, formatEventPricing as q, isEventSeriesVerticalPresetDefinition as r, parseUserId as rt, DEFAULT_EVENT_TYPE_PRESETS as s, unsafeAsDeclaredMimeType as st, areConfiguredBookingVerticalConfigsEqual as t, parsePageId as tt, SURFACE_EVENT_PRESET_VALUES as u, unsafeAsEntitlementId as ut, parseFooterModuleId as v, unsafeAsPositiveSeconds as vt, isCanonicalRichTextDoc as w, unsafeAsStaffMemberId as wt, parseNewsletterListId as x, unsafeAsSiteFontId as xt, parseMailchimpAudienceId as y, unsafeAsProductId as yt, getLocalScheduleTime as z };