riverbankcms 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acceptance/matrix.mjs +173 -176
- package/dist/cli/index.mjs +45677 -45067
- package/dist/cli/init-docs/content/cli-reference.md +12 -17
- package/dist/cli/init-docs/content/content-management.md +42 -33
- package/dist/cli/init-docs/content/workflow-isr-revalidation.md +1 -1
- package/dist/cli/init-docs/content/workflow-preview-mode.md +1 -1
- package/dist/{eventUpsertInputSchema-7QFvBuxF.mjs → eventUpsertInputSchema-CWglzaib.mjs} +34 -40
- package/dist/load-config-CZ9qKC9m.mjs +5 -0
- package/dist/{load-config-DKJkcASy.mjs → load-config-DBGNYfxV.mjs} +1 -1
- package/dist/{plan-DPS1XAby.mjs → plan-DgjlCWxa.mjs} +22 -14
- package/dist/scaffold/index.mjs +2 -2
- package/dist/{style-CeGQc8Oy.mjs → sdk-event-upsert-CS9tY-KW.mjs} +29426 -43432
- package/dist/{sdk-peer-sZF43pDS.mjs → sdk-peer-BjTmH5Mt.mjs} +3 -3
- package/dist/{src-DKm0bLEA.mjs → src-DT7DBZtQ.mjs} +1981 -465
- package/dist/style-D2srLsI5.mjs +14454 -0
- package/dist/testing.mjs +4 -4
- package/package.json +2 -2
- package/dist/load-config-D88VsMlB.mjs +0 -5
- package/dist/sdk-event-upsert-Dy08IXYL.mjs +0 -962
|
@@ -52,7 +52,7 @@ const unsafeAsOccurrenceId = (id) => id;
|
|
|
52
52
|
const unsafeAsEventOfferingId = (id) => id;
|
|
53
53
|
const unsafeAsEventSeriesId = (id) => id;
|
|
54
54
|
const unsafeAsEventPageRouteSlug = (value) => value;
|
|
55
|
-
const
|
|
55
|
+
const unsafeAsEventAttendeeTicketLineId = (id) => id;
|
|
56
56
|
const unsafeAsEventSeriesRegistrationId = (id) => id;
|
|
57
57
|
const unsafeAsEventCategoryId = (id) => id;
|
|
58
58
|
const unsafeAsVenueId = (id) => id;
|
|
@@ -90,8 +90,6 @@ const unsafeAsFormId = (id) => id;
|
|
|
90
90
|
const unsafeAsFormSubmissionId = (id) => id;
|
|
91
91
|
const unsafeAsPassId = (id) => id;
|
|
92
92
|
const unsafeAsMembershipId = (id) => id;
|
|
93
|
-
const unsafeAsCourseId = (id) => id;
|
|
94
|
-
const unsafeAsCourseProgramId = (id) => id;
|
|
95
93
|
const unsafeAsCustomerId = (id) => id;
|
|
96
94
|
const unsafeAsOrderId = (id) => id;
|
|
97
95
|
const unsafeAsOrderItemId = (id) => id;
|
|
@@ -103,6 +101,9 @@ const unsafeAsOrderPaymentPlanInstalmentId = (id) => id;
|
|
|
103
101
|
const unsafeAsManualPaymentOperationId = (id) => id;
|
|
104
102
|
const unsafeAsCheckoutLineId = (id) => id;
|
|
105
103
|
const unsafeAsAttendeeId = (id) => id;
|
|
104
|
+
const unsafeAsEventRegistrationCommandId = (id) => id;
|
|
105
|
+
const unsafeAsEventRegistrationCommandRowId = (id) => id;
|
|
106
|
+
const unsafeAsEventRegistrationProjectionClaimToken = (id) => id;
|
|
106
107
|
const unsafeAsCustomerPassId = (id) => id;
|
|
107
108
|
const unsafeAsCustomerMembershipId = (id) => id;
|
|
108
109
|
const unsafeAsAppointmentId = (id) => id;
|
|
@@ -160,8 +161,7 @@ const parsePageId = (value) => parseUuidBrand(value, unsafeAsPageId);
|
|
|
160
161
|
const parsePageSurfaceId = (value) => parseUuidBrand(value, unsafeAsPageSurfaceId);
|
|
161
162
|
const parseOccurrenceId = (value) => parseUuidBrand(value, unsafeAsOccurrenceId);
|
|
162
163
|
const parseEventOfferingId = (value) => parseUuidBrand(value, unsafeAsEventOfferingId);
|
|
163
|
-
const
|
|
164
|
-
const parseTicketTypeId = (value) => parseUuidBrand(value, unsafeAsTicketTypeId);
|
|
164
|
+
const parseEventAttendeeTicketLineId = (value) => parseUuidBrand(value, unsafeAsEventAttendeeTicketLineId);
|
|
165
165
|
const parsePricingOptionId = (value) => parseUuidBrand(value, unsafeAsPricingOptionId);
|
|
166
166
|
const parseUserId = (value) => parseUuidBrand(value, unsafeAsUserId);
|
|
167
167
|
const parseProductId = (value) => parseUuidBrand(value, unsafeAsProductId);
|
|
@@ -175,7 +175,8 @@ const parseNavigationMenuId = (value) => parseUuidBrand(value, unsafeAsNavigatio
|
|
|
175
175
|
const parseNavigationItemId = (value) => parseUuidBrand(value, unsafeAsNavigationItemId);
|
|
176
176
|
const parsePassId = (value) => parseUuidBrand(value, unsafeAsPassId);
|
|
177
177
|
const parseMembershipId = (value) => parseUuidBrand(value, unsafeAsMembershipId);
|
|
178
|
-
const
|
|
178
|
+
const asRetiredCourseId = (id) => id;
|
|
179
|
+
const parseRetiredCourseId = (value) => parseUuidBrand(value, asRetiredCourseId);
|
|
179
180
|
const parseFormId = (value) => parseUuidBrand(value, unsafeAsFormId);
|
|
180
181
|
const parseAppointmentId = (value) => parseUuidBrand(value, unsafeAsAppointmentId);
|
|
181
182
|
const parseAppointmentServiceId = (value) => parseUuidBrand(value, unsafeAsAppointmentServiceId);
|
|
@@ -183,6 +184,9 @@ const parseAppointmentResourceId = (value) => parseUuidBrand(value, unsafeAsAppo
|
|
|
183
184
|
const parseAppointmentReservationId = (value) => parseUuidBrand(value, unsafeAsAppointmentReservationId);
|
|
184
185
|
const parseAppointmentReservationRunId = (value) => parseUuidBrand(value, unsafeAsAppointmentReservationRunId);
|
|
185
186
|
const parseAttendeeId = (value) => parseUuidBrand(value, unsafeAsAttendeeId);
|
|
187
|
+
const parseEventRegistrationCommandId = (value) => parseUuidBrand(value, unsafeAsEventRegistrationCommandId);
|
|
188
|
+
const parseEventRegistrationCommandRowId = (value) => parseUuidBrand(value, unsafeAsEventRegistrationCommandRowId);
|
|
189
|
+
const parseEventRegistrationProjectionClaimToken = (value) => parseUuidBrand(value, unsafeAsEventRegistrationProjectionClaimToken);
|
|
186
190
|
const parseDatedOfferingCapacityHoldId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingCapacityHoldId);
|
|
187
191
|
const parseDatedOfferingCapacityHoldAuditRef = (value) => parseUuidBrand(value, unsafeAsDatedOfferingCapacityHoldAuditRef);
|
|
188
192
|
const parseDatedOfferingRevocationAuditEventId = (value) => parseUuidBrand(value, unsafeAsDatedOfferingRevocationAuditEventId);
|
|
@@ -475,6 +479,44 @@ function assertNever(value, message = "Unexpected variant") {
|
|
|
475
479
|
throw new Error(`${message}: ${String(value)}`);
|
|
476
480
|
}
|
|
477
481
|
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region ../core/src/event-booking-offer.ts
|
|
484
|
+
const EVENT_PRICING_STRATEGIES = [
|
|
485
|
+
"free",
|
|
486
|
+
"single",
|
|
487
|
+
"tiered",
|
|
488
|
+
"sliding_scale"
|
|
489
|
+
];
|
|
490
|
+
function createEventBookingOffer(input) {
|
|
491
|
+
if (input.bookingRequirement === "none") {
|
|
492
|
+
return input.pricing.kind === "free" ? ok({
|
|
493
|
+
bookingRequirement: "none",
|
|
494
|
+
pricing: input.pricing,
|
|
495
|
+
paymentTerms: input.paymentTerms
|
|
496
|
+
}) : err({ kind: "paid_without_registration" });
|
|
497
|
+
}
|
|
498
|
+
if (input.bookingRequirement === "required") {
|
|
499
|
+
return ok({
|
|
500
|
+
bookingRequirement: "required",
|
|
501
|
+
pricing: input.pricing,
|
|
502
|
+
paymentTerms: input.paymentTerms
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return err({
|
|
506
|
+
kind: "invalid_booking_requirement",
|
|
507
|
+
value: input.bookingRequirement
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
function eventPricingRequiresPayment(pricing) {
|
|
511
|
+
switch (pricing.kind) {
|
|
512
|
+
case "free": return false;
|
|
513
|
+
case "single": return true;
|
|
514
|
+
case "tiered": return pricing.options.some((option) => option.priceAmount > 0);
|
|
515
|
+
case "sliding_scale": return pricing.maximum > 0;
|
|
516
|
+
default: return assertNever(pricing);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
478
520
|
//#endregion
|
|
479
521
|
//#region ../core/src/currency-format.ts
|
|
480
522
|
const CURRENCY_CODE_INPUT_PATTERN = /^[A-Za-z]{3}$/;
|
|
@@ -538,183 +580,6 @@ function formatCurrencyAmount(amountCents, currency, options) {
|
|
|
538
580
|
}
|
|
539
581
|
}
|
|
540
582
|
|
|
541
|
-
//#endregion
|
|
542
|
-
//#region ../core/src/event-pricing.ts
|
|
543
|
-
const EVENT_PRICING_MODES = [
|
|
544
|
-
"single",
|
|
545
|
-
"ticket_types",
|
|
546
|
-
"sliding_scale"
|
|
547
|
-
];
|
|
548
|
-
function isEventPricingMode(value) {
|
|
549
|
-
return typeof value === "string" && EVENT_PRICING_MODES.includes(value);
|
|
550
|
-
}
|
|
551
|
-
function publicEventPricingDisplay(pricing) {
|
|
552
|
-
return pricing.display;
|
|
553
|
-
}
|
|
554
|
-
function publicEventPricingType(pricing) {
|
|
555
|
-
switch (pricing.kind) {
|
|
556
|
-
case "drop_in":
|
|
557
|
-
case "free_registration": return "free";
|
|
558
|
-
case "single_paid":
|
|
559
|
-
case "ticket_types":
|
|
560
|
-
case "sliding_scale":
|
|
561
|
-
case "whole_series": return isFreePublicEventPriceDisplay(pricing.display) ? "free" : "paid";
|
|
562
|
-
case "external": return "external";
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function publicEventPricingCurrency(pricing) {
|
|
566
|
-
switch (pricing.kind) {
|
|
567
|
-
case "drop_in":
|
|
568
|
-
case "free_registration":
|
|
569
|
-
case "single_paid":
|
|
570
|
-
case "ticket_types":
|
|
571
|
-
case "sliding_scale": return pricing.currency;
|
|
572
|
-
case "whole_series": return pricing.display.display.currency;
|
|
573
|
-
case "external": return null;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
function publicEventScalarPrice(pricing) {
|
|
577
|
-
return pricing.kind === "single_paid" ? pricing.amount : null;
|
|
578
|
-
}
|
|
579
|
-
function publicEventPricingMode(pricing) {
|
|
580
|
-
switch (pricing.kind) {
|
|
581
|
-
case "single_paid":
|
|
582
|
-
case "free_registration": return "single";
|
|
583
|
-
case "ticket_types": return "ticket_types";
|
|
584
|
-
case "sliding_scale": return "sliding_scale";
|
|
585
|
-
case "drop_in":
|
|
586
|
-
case "whole_series":
|
|
587
|
-
case "external": return undefined;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
function publicEventTicketTypes(pricing) {
|
|
591
|
-
return pricing.kind === "ticket_types" ? pricing.ticketTypes : [];
|
|
592
|
-
}
|
|
593
|
-
function isFreePublicEventPriceDisplay(display) {
|
|
594
|
-
switch (display.kind) {
|
|
595
|
-
case "free": return true;
|
|
596
|
-
case "whole_series": return display.display.kind === "free";
|
|
597
|
-
case "exact":
|
|
598
|
-
case "from":
|
|
599
|
-
case "range":
|
|
600
|
-
case "mixed":
|
|
601
|
-
case "unavailable":
|
|
602
|
-
case "external": return false;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
function formatCoursePriceDisplayLabel(display, options) {
|
|
606
|
-
switch (display.kind) {
|
|
607
|
-
case "free":
|
|
608
|
-
case "unavailable": return "Course";
|
|
609
|
-
case "exact": return `Course (${formatCurrencyAmount(display.amount, display.currency, options)})`;
|
|
610
|
-
case "from": return `Course (From ${formatCurrencyAmount(display.amount, display.currency, options)})`;
|
|
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)})`;
|
|
613
|
-
default: return assertNever(display);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
/**
|
|
617
|
-
* The single renderer for a precomputed booking price `display` ADT. This is
|
|
618
|
-
* the one place booking price variants map to user-facing strings — every call
|
|
619
|
-
* site (booking entry, events calendar, event/course details, listings, portal)
|
|
620
|
-
* should route through here so labels never drift. Adding a variant to the ADT
|
|
621
|
-
* breaks compilation here (via `assertNever`) until it is handled.
|
|
622
|
-
*/
|
|
623
|
-
function formatBookingPriceDisplay(display, options = {}) {
|
|
624
|
-
switch (display.kind) {
|
|
625
|
-
case "free": return "Free";
|
|
626
|
-
case "exact": return formatCurrencyAmount(display.amount, display.currency, options);
|
|
627
|
-
case "from": return `From ${formatCurrencyAmount(display.amount, display.currency, options)}`;
|
|
628
|
-
case "range": return `${formatCurrencyAmount(display.minAmount, display.currency, options)} - ${formatCurrencyAmount(display.maxAmount, display.currency, options)}`;
|
|
629
|
-
case "mixed": return display.paidToAmount > display.paidFromAmount ? `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)} - ${formatCurrencyAmount(display.paidToAmount, display.currency, options)}` : `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)}`;
|
|
630
|
-
case "unavailable": return "Pricing unavailable";
|
|
631
|
-
case "external": return display.label;
|
|
632
|
-
case "whole_series": return formatCoursePriceDisplayLabel(display.display, options);
|
|
633
|
-
default: return assertNever(display);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
/** Render an event's price directly from its pricing value. */
|
|
637
|
-
function formatEventPricing(pricing, options = {}) {
|
|
638
|
-
return formatBookingPriceDisplay(publicEventPricingDisplay(pricing), options);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
//#endregion
|
|
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
|
-
}
|
|
655
|
-
function parseEventPageSurfaceSubject(value) {
|
|
656
|
-
if (!isRecord$6(value)) return err({ kind: "notObject" });
|
|
657
|
-
switch (value.kind) {
|
|
658
|
-
case "eventSeries": {
|
|
659
|
-
const eventSeriesId = parseEventSeriesId(value.eventSeriesId);
|
|
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));
|
|
673
|
-
}
|
|
674
|
-
case "eventOffering": {
|
|
675
|
-
const eventOfferingId = parseEventOfferingId(value.eventOfferingId);
|
|
676
|
-
if (!eventOfferingId.ok) {
|
|
677
|
-
return err({
|
|
678
|
-
kind: "invalidEventOfferingId",
|
|
679
|
-
error: eventOfferingId.error
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
if (!Array.isArray(value.availableEventSeriesIds)) {
|
|
683
|
-
return err({
|
|
684
|
-
kind: "unsupportedSubject",
|
|
685
|
-
subjectKind: value.kind
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
const parsedSeriesIds = [];
|
|
689
|
-
for (const valueId of value.availableEventSeriesIds) {
|
|
690
|
-
const eventSeriesId = parseEventSeriesId(valueId);
|
|
691
|
-
if (!eventSeriesId.ok) {
|
|
692
|
-
return err({
|
|
693
|
-
kind: "invalidEventSeriesId",
|
|
694
|
-
error: eventSeriesId.error
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
parsedSeriesIds.push(eventSeriesId.value);
|
|
698
|
-
}
|
|
699
|
-
return ok({
|
|
700
|
-
kind: "eventOffering",
|
|
701
|
-
eventOfferingId: eventOfferingId.value,
|
|
702
|
-
availableEventSeriesIds: parsedSeriesIds
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
default: return err({
|
|
706
|
-
kind: "unsupportedSubject",
|
|
707
|
-
subjectKind: value.kind
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
function toEventPageSurfaceSubjectWire(subject) {
|
|
712
|
-
return subject;
|
|
713
|
-
}
|
|
714
|
-
function isRecord$6(value) {
|
|
715
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
583
|
//#endregion
|
|
719
584
|
//#region ../core/src/money-input.ts
|
|
720
585
|
function unsafeAsCents(value) {
|
|
@@ -843,6 +708,675 @@ function formatMoneyInputFromCents(amountCents) {
|
|
|
843
708
|
return `${sign}${units}.${fractional}`;
|
|
844
709
|
}
|
|
845
710
|
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region ../core/src/non-empty-array.ts
|
|
713
|
+
function fromArray(items) {
|
|
714
|
+
const [first, ...rest] = items;
|
|
715
|
+
if (first === undefined) {
|
|
716
|
+
return err("empty_array");
|
|
717
|
+
}
|
|
718
|
+
return ok([first, ...rest]);
|
|
719
|
+
}
|
|
720
|
+
function mapNonEmptyArray(items, map) {
|
|
721
|
+
return items.map(map);
|
|
722
|
+
}
|
|
723
|
+
function fromReadonlyArray(items) {
|
|
724
|
+
const [first, ...rest] = items;
|
|
725
|
+
return first === undefined ? err("empty_array") : ok([first, ...rest]);
|
|
726
|
+
}
|
|
727
|
+
function mapReadonlyNonEmptyArray(items, map) {
|
|
728
|
+
const [first, ...rest] = items;
|
|
729
|
+
return [map(first, 0), ...rest.map((item, index) => map(item, index + 1))];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region ../core/src/event-booking-offer-parser.ts
|
|
734
|
+
function oneIssue(issue) {
|
|
735
|
+
return [issue];
|
|
736
|
+
}
|
|
737
|
+
function isNullish(value) {
|
|
738
|
+
return value === null || value === undefined;
|
|
739
|
+
}
|
|
740
|
+
function parseOptionIdentity(input) {
|
|
741
|
+
const id = parsePricingOptionId(input.id);
|
|
742
|
+
if (!id.ok) return err({ kind: "invalid_id" });
|
|
743
|
+
if (typeof input.name !== "string" || input.name.trim().length === 0) {
|
|
744
|
+
return err({ kind: "invalid_name" });
|
|
745
|
+
}
|
|
746
|
+
if (!isNullish(input.description) && typeof input.description !== "string") {
|
|
747
|
+
return err({ kind: "invalid_description" });
|
|
748
|
+
}
|
|
749
|
+
return ok({
|
|
750
|
+
id: id.value,
|
|
751
|
+
name: input.name.trim(),
|
|
752
|
+
description: input.description ?? null
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
function parseOptionCommercials(input) {
|
|
756
|
+
const price = typeof input.priceAmount === "number" ? parseNonNegativeCents(input.priceAmount) : err({ kind: "invalid-number" });
|
|
757
|
+
if (!price.ok) return err({
|
|
758
|
+
kind: "invalid_price",
|
|
759
|
+
error: price.error
|
|
760
|
+
});
|
|
761
|
+
if (typeof input.isActive !== "boolean") return err({ kind: "invalid_active" });
|
|
762
|
+
if (typeof input.sortOrder !== "number" || !Number.isInteger(input.sortOrder) || input.sortOrder < 0) {
|
|
763
|
+
return err({ kind: "invalid_sort_order" });
|
|
764
|
+
}
|
|
765
|
+
if (!isNullish(input.capacityLimit) && (typeof input.capacityLimit !== "number" || !Number.isInteger(input.capacityLimit) || input.capacityLimit < 1)) {
|
|
766
|
+
return err({ kind: "invalid_capacity" });
|
|
767
|
+
}
|
|
768
|
+
return ok({
|
|
769
|
+
priceAmount: price.value,
|
|
770
|
+
isActive: input.isActive,
|
|
771
|
+
sortOrder: input.sortOrder,
|
|
772
|
+
capacityLimit: input.capacityLimit ?? null
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function parseNullableIsoDateTime(value, issue) {
|
|
776
|
+
if (isNullish(value)) return ok(null);
|
|
777
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
|
|
778
|
+
return err({ kind: issue });
|
|
779
|
+
}
|
|
780
|
+
return ok(value);
|
|
781
|
+
}
|
|
782
|
+
function parseAttendanceScope(value) {
|
|
783
|
+
if (value === "in_person" || value === "online" || value === "both") {
|
|
784
|
+
return ok(value);
|
|
785
|
+
}
|
|
786
|
+
return err({ kind: "invalid_attendance_scope" });
|
|
787
|
+
}
|
|
788
|
+
function parseRecipeIdentifier(value) {
|
|
789
|
+
if (isNullish(value)) return ok(null);
|
|
790
|
+
if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(value)) {
|
|
791
|
+
return err({ kind: "invalid_recipe_identifier" });
|
|
792
|
+
}
|
|
793
|
+
return ok(value);
|
|
794
|
+
}
|
|
795
|
+
function parseOptionAvailability(input) {
|
|
796
|
+
const start = parseNullableIsoDateTime(input.saleStartsAt, "invalid_sale_start");
|
|
797
|
+
if (!start.ok) return start;
|
|
798
|
+
const end = parseNullableIsoDateTime(input.saleEndsAt, "invalid_sale_end");
|
|
799
|
+
if (!end.ok) return end;
|
|
800
|
+
if (start.value !== null && end.value !== null && Date.parse(end.value) <= Date.parse(start.value)) {
|
|
801
|
+
return err({ kind: "invalid_sale_window" });
|
|
802
|
+
}
|
|
803
|
+
const scope = parseAttendanceScope(input.attendanceScope);
|
|
804
|
+
if (!scope.ok) return scope;
|
|
805
|
+
const recipe = parseRecipeIdentifier(input.recipeIdentifier);
|
|
806
|
+
if (!recipe.ok) return recipe;
|
|
807
|
+
return ok({
|
|
808
|
+
saleStartsAt: start.value,
|
|
809
|
+
saleEndsAt: end.value,
|
|
810
|
+
attendanceScope: scope.value,
|
|
811
|
+
recipeIdentifier: recipe.value
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
function parseEventPricingOption(input) {
|
|
815
|
+
const identity = parseOptionIdentity(input);
|
|
816
|
+
const commercials = parseOptionCommercials(input);
|
|
817
|
+
const availability$1 = parseOptionAvailability(input);
|
|
818
|
+
const issues = [
|
|
819
|
+
identity,
|
|
820
|
+
commercials,
|
|
821
|
+
availability$1
|
|
822
|
+
].flatMap((result) => result.ok ? [] : [result.error]);
|
|
823
|
+
if (!identity.ok || !commercials.ok || !availability$1.ok) {
|
|
824
|
+
const nonEmptyIssues = fromReadonlyArray(issues);
|
|
825
|
+
return err(nonEmptyIssues.ok ? nonEmptyIssues.value : oneIssue({ kind: "invalid_object" }));
|
|
826
|
+
}
|
|
827
|
+
return ok({
|
|
828
|
+
...identity.value,
|
|
829
|
+
...commercials.value,
|
|
830
|
+
...availability$1.value
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
function unexpectedValues(input, fields) {
|
|
834
|
+
return fields.flatMap((field) => isNullish(input[field]) ? [] : [{
|
|
835
|
+
kind: "unexpected_value",
|
|
836
|
+
field
|
|
837
|
+
}]);
|
|
838
|
+
}
|
|
839
|
+
function invalidOptionsIssue(value) {
|
|
840
|
+
return Array.isArray(value) && value.length === 0 ? [] : [{
|
|
841
|
+
kind: "unexpected_value",
|
|
842
|
+
field: "options"
|
|
843
|
+
}];
|
|
844
|
+
}
|
|
845
|
+
function parseFreePricing(input, currency) {
|
|
846
|
+
const issues = [...unexpectedValues(input, [
|
|
847
|
+
"singlePriceAmount",
|
|
848
|
+
"slidingMinAmount",
|
|
849
|
+
"slidingMaxAmount",
|
|
850
|
+
"slidingSuggestedAmount"
|
|
851
|
+
]), ...invalidOptionsIssue(input.options)];
|
|
852
|
+
const nonEmptyIssues = fromReadonlyArray(issues);
|
|
853
|
+
return nonEmptyIssues.ok ? err(nonEmptyIssues.value) : ok({
|
|
854
|
+
kind: "free",
|
|
855
|
+
currency
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
function parseSinglePricing(input, currency) {
|
|
859
|
+
const amount = typeof input.singlePriceAmount === "number" ? parsePositiveCents(input.singlePriceAmount) : err({ kind: "invalid-number" });
|
|
860
|
+
const issues = [
|
|
861
|
+
...unexpectedValues(input, [
|
|
862
|
+
"slidingMinAmount",
|
|
863
|
+
"slidingMaxAmount",
|
|
864
|
+
"slidingSuggestedAmount"
|
|
865
|
+
]),
|
|
866
|
+
...invalidOptionsIssue(input.options),
|
|
867
|
+
...amount.ok ? [] : [{
|
|
868
|
+
kind: "invalid_amount",
|
|
869
|
+
field: "singlePriceAmount",
|
|
870
|
+
error: amount.error
|
|
871
|
+
}]
|
|
872
|
+
];
|
|
873
|
+
const nonEmptyIssues = fromReadonlyArray(issues);
|
|
874
|
+
if (nonEmptyIssues.ok) return err(nonEmptyIssues.value);
|
|
875
|
+
if (!amount.ok) return err(oneIssue({
|
|
876
|
+
kind: "invalid_amount",
|
|
877
|
+
field: "singlePriceAmount",
|
|
878
|
+
error: amount.error
|
|
879
|
+
}));
|
|
880
|
+
return ok({
|
|
881
|
+
kind: "single",
|
|
882
|
+
currency,
|
|
883
|
+
amount: amount.value
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
function parseRawPricingOption(raw, index) {
|
|
887
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
888
|
+
return err(oneIssue({
|
|
889
|
+
kind: "invalid_option",
|
|
890
|
+
index,
|
|
891
|
+
issue: { kind: "invalid_object" }
|
|
892
|
+
}));
|
|
893
|
+
}
|
|
894
|
+
const parsed = parseEventPricingOption(raw);
|
|
895
|
+
if (parsed.ok) return parsed;
|
|
896
|
+
const [first, ...rest] = parsed.error;
|
|
897
|
+
return err([{
|
|
898
|
+
kind: "invalid_option",
|
|
899
|
+
index,
|
|
900
|
+
issue: first
|
|
901
|
+
}, ...rest.map((issue) => ({
|
|
902
|
+
kind: "invalid_option",
|
|
903
|
+
index,
|
|
904
|
+
issue
|
|
905
|
+
}))]);
|
|
906
|
+
}
|
|
907
|
+
function parseTieredPricing(input, currency) {
|
|
908
|
+
const structuralIssues = unexpectedValues(input, [
|
|
909
|
+
"singlePriceAmount",
|
|
910
|
+
"slidingMinAmount",
|
|
911
|
+
"slidingMaxAmount",
|
|
912
|
+
"slidingSuggestedAmount"
|
|
913
|
+
]);
|
|
914
|
+
if (!Array.isArray(input.options) || input.options.length === 0) {
|
|
915
|
+
return err([{ kind: "tiered_options_empty" }, ...structuralIssues]);
|
|
916
|
+
}
|
|
917
|
+
const parsed = input.options.map(parseRawPricingOption);
|
|
918
|
+
const issues = [...structuralIssues, ...parsed.flatMap((result) => result.ok ? [] : result.error)];
|
|
919
|
+
const nonEmptyIssues = fromReadonlyArray(issues);
|
|
920
|
+
if (nonEmptyIssues.ok) return err(nonEmptyIssues.value);
|
|
921
|
+
const options = parsed.flatMap((result) => result.ok ? [result.value] : []);
|
|
922
|
+
const nonEmptyOptions = fromReadonlyArray(options);
|
|
923
|
+
if (!nonEmptyOptions.ok) {
|
|
924
|
+
return err(oneIssue({ kind: "tiered_options_empty" }));
|
|
925
|
+
}
|
|
926
|
+
return ok({
|
|
927
|
+
kind: "tiered",
|
|
928
|
+
currency,
|
|
929
|
+
options: nonEmptyOptions.value
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
function amountIssue(field, result) {
|
|
933
|
+
return result.ok ? [] : [{
|
|
934
|
+
kind: "invalid_amount",
|
|
935
|
+
field,
|
|
936
|
+
error: result.error
|
|
937
|
+
}];
|
|
938
|
+
}
|
|
939
|
+
function slidingRangeIssues(minimum, maximum, suggested) {
|
|
940
|
+
if (!minimum.ok || !maximum.ok || !suggested.ok) return [];
|
|
941
|
+
const issues = [];
|
|
942
|
+
if (minimum.value > maximum.value) issues.push({ kind: "invalid_sliding_range" });
|
|
943
|
+
if (suggested.value !== null && (suggested.value < minimum.value || suggested.value > maximum.value)) {
|
|
944
|
+
issues.push({ kind: "suggested_outside_range" });
|
|
945
|
+
}
|
|
946
|
+
return issues;
|
|
947
|
+
}
|
|
948
|
+
function parseSlidingPricing(input, currency) {
|
|
949
|
+
const minimum = typeof input.slidingMinAmount === "number" ? parseNonNegativeCents(input.slidingMinAmount) : err({ kind: "invalid-number" });
|
|
950
|
+
const maximum = typeof input.slidingMaxAmount === "number" ? parsePositiveCents(input.slidingMaxAmount) : err({ kind: "invalid-number" });
|
|
951
|
+
const suggested = isNullish(input.slidingSuggestedAmount) ? ok(null) : typeof input.slidingSuggestedAmount === "number" ? parseNonNegativeCents(input.slidingSuggestedAmount) : err({ kind: "invalid-number" });
|
|
952
|
+
const issues = [
|
|
953
|
+
...unexpectedValues(input, ["singlePriceAmount"]),
|
|
954
|
+
...invalidOptionsIssue(input.options),
|
|
955
|
+
...amountIssue("slidingMinAmount", minimum),
|
|
956
|
+
...amountIssue("slidingMaxAmount", maximum),
|
|
957
|
+
...amountIssue("slidingSuggestedAmount", suggested),
|
|
958
|
+
...slidingRangeIssues(minimum, maximum, suggested)
|
|
959
|
+
];
|
|
960
|
+
const nonEmptyIssues = fromReadonlyArray(issues);
|
|
961
|
+
if (nonEmptyIssues.ok) return err(nonEmptyIssues.value);
|
|
962
|
+
if (!minimum.ok) return err(oneIssue({
|
|
963
|
+
kind: "invalid_amount",
|
|
964
|
+
field: "slidingMinAmount",
|
|
965
|
+
error: minimum.error
|
|
966
|
+
}));
|
|
967
|
+
if (!maximum.ok) return err(oneIssue({
|
|
968
|
+
kind: "invalid_amount",
|
|
969
|
+
field: "slidingMaxAmount",
|
|
970
|
+
error: maximum.error
|
|
971
|
+
}));
|
|
972
|
+
if (!suggested.ok) return err(oneIssue({
|
|
973
|
+
kind: "invalid_amount",
|
|
974
|
+
field: "slidingSuggestedAmount",
|
|
975
|
+
error: suggested.error
|
|
976
|
+
}));
|
|
977
|
+
return ok({
|
|
978
|
+
kind: "sliding_scale",
|
|
979
|
+
currency,
|
|
980
|
+
minimum: minimum.value,
|
|
981
|
+
maximum: maximum.value,
|
|
982
|
+
suggested: suggested.value
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
function parseEventPricing(input) {
|
|
986
|
+
if (!isEventPricingStrategy(input.pricingStrategy)) {
|
|
987
|
+
return err(oneIssue({
|
|
988
|
+
kind: "invalid_strategy",
|
|
989
|
+
value: input.pricingStrategy
|
|
990
|
+
}));
|
|
991
|
+
}
|
|
992
|
+
const currency = typeof input.currency === "string" ? parseCurrencyCode(input.currency) : null;
|
|
993
|
+
if (currency === null) return err(oneIssue({
|
|
994
|
+
kind: "invalid_currency",
|
|
995
|
+
value: input.currency
|
|
996
|
+
}));
|
|
997
|
+
switch (input.pricingStrategy) {
|
|
998
|
+
case "free": return parseFreePricing(input, currency);
|
|
999
|
+
case "single": return parseSinglePricing(input, currency);
|
|
1000
|
+
case "tiered": return parseTieredPricing(input, currency);
|
|
1001
|
+
case "sliding_scale": return parseSlidingPricing(input, currency);
|
|
1002
|
+
default: return assertNever(input.pricingStrategy);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function isEventPricingStrategy(value) {
|
|
1006
|
+
return EVENT_PRICING_STRATEGIES.some((strategy) => strategy === value);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
//#endregion
|
|
1010
|
+
//#region ../core/src/planner.ts
|
|
1011
|
+
function plannerNoChange(details = {}) {
|
|
1012
|
+
return {
|
|
1013
|
+
...details,
|
|
1014
|
+
tag: "no_change"
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function plannerUpdate(details = {}) {
|
|
1018
|
+
return {
|
|
1019
|
+
...details,
|
|
1020
|
+
tag: "update"
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
/** Identity at runtime; marks a planner-derived patch as shallow readonly. */
|
|
1024
|
+
function plannerPatch(patch) {
|
|
1025
|
+
return patch;
|
|
1026
|
+
}
|
|
1027
|
+
/** Default clock for production callers that do not inject time explicitly. */
|
|
1028
|
+
const systemPlanningClock = { now: () => Date.now() };
|
|
1029
|
+
/** Resolve current time from an explicit timestamp, injected clock, or system clock. */
|
|
1030
|
+
function resolvePlanningNow(input = {}) {
|
|
1031
|
+
if (typeof input.now === "number") {
|
|
1032
|
+
return input.now;
|
|
1033
|
+
}
|
|
1034
|
+
return (input.clock ?? systemPlanningClock).now();
|
|
1035
|
+
}
|
|
1036
|
+
/** Resolve current time as a Date from an explicit Date, injected clock, or system clock. */
|
|
1037
|
+
function resolvePlanningNowDate(input = {}) {
|
|
1038
|
+
if (input.now !== undefined) {
|
|
1039
|
+
return input.now;
|
|
1040
|
+
}
|
|
1041
|
+
return new Date((input.clock ?? systemPlanningClock).now());
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Resolve a Date-compatible planner input.
|
|
1045
|
+
*
|
|
1046
|
+
* Precedence: bare Date, `{ now }`, `{ clock }`, then `systemPlanningClock`.
|
|
1047
|
+
* Explicit Date inputs are returned by reference, not cloned.
|
|
1048
|
+
*/
|
|
1049
|
+
function resolvePlanningDateInput(input = {}) {
|
|
1050
|
+
if (input instanceof Date) {
|
|
1051
|
+
return input;
|
|
1052
|
+
}
|
|
1053
|
+
return resolvePlanningNowDate(input);
|
|
1054
|
+
}
|
|
1055
|
+
/** Build an enabled selection while preserving literal value types. */
|
|
1056
|
+
function enabledSelection(value) {
|
|
1057
|
+
return {
|
|
1058
|
+
enabled: true,
|
|
1059
|
+
value
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
/** Build a disabled selection. */
|
|
1063
|
+
function disabledSelection() {
|
|
1064
|
+
return { enabled: false };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region ../core/src/event-pricing.ts
|
|
1069
|
+
/** Capacity closes booking, but does not make an otherwise eligible price unknown. */
|
|
1070
|
+
function isPublicEventPricingOptionDisplayable(option) {
|
|
1071
|
+
if (!option.isActive) return false;
|
|
1072
|
+
if (option.availability.kind === "available") return true;
|
|
1073
|
+
const reason = option.availability.reason;
|
|
1074
|
+
switch (reason) {
|
|
1075
|
+
case "sold_out":
|
|
1076
|
+
case "overallCapacityUnavailable": return true;
|
|
1077
|
+
case "inactive":
|
|
1078
|
+
case "not_on_sale_yet":
|
|
1079
|
+
case "sale_ended":
|
|
1080
|
+
case "attendance_mode_unavailable": return false;
|
|
1081
|
+
default: return assertNever(reason);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
function publicEventPricingDisplay(pricing) {
|
|
1085
|
+
switch (pricing.kind) {
|
|
1086
|
+
case "free":
|
|
1087
|
+
case "single":
|
|
1088
|
+
case "sliding_scale": return canonicalEventPricingDisplay(pricing);
|
|
1089
|
+
case "tiered": return resolvePricingOptionsPriceDisplay({
|
|
1090
|
+
options: pricing.options.filter(isPublicEventPricingOptionDisplayable),
|
|
1091
|
+
currency: pricing.currency,
|
|
1092
|
+
purpose: "configured"
|
|
1093
|
+
});
|
|
1094
|
+
case "external": return {
|
|
1095
|
+
kind: "external",
|
|
1096
|
+
label: pricing.label
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
function publicEventPricingType(pricing) {
|
|
1101
|
+
switch (pricing.kind) {
|
|
1102
|
+
case "free": return "free";
|
|
1103
|
+
case "single":
|
|
1104
|
+
case "tiered":
|
|
1105
|
+
case "sliding_scale": return isFreePublicEventPriceDisplay(publicEventPricingDisplay(pricing)) ? "free" : "paid";
|
|
1106
|
+
case "external": return "external";
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function publicEventPricingCurrency(pricing) {
|
|
1110
|
+
switch (pricing.kind) {
|
|
1111
|
+
case "free":
|
|
1112
|
+
case "single":
|
|
1113
|
+
case "tiered":
|
|
1114
|
+
case "sliding_scale": return pricing.currency;
|
|
1115
|
+
case "external": return null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function publicEventScalarPrice(pricing) {
|
|
1119
|
+
return pricing.kind === "single" ? pricing.amount : null;
|
|
1120
|
+
}
|
|
1121
|
+
function publicEventPricingStrategy(pricing) {
|
|
1122
|
+
switch (pricing.kind) {
|
|
1123
|
+
case "single": return "single";
|
|
1124
|
+
case "free": return "free";
|
|
1125
|
+
case "tiered": return "tiered";
|
|
1126
|
+
case "sliding_scale": return "sliding_scale";
|
|
1127
|
+
case "external": return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
function publicEventPricingOptions(pricing) {
|
|
1131
|
+
return pricing.kind === "tiered" ? pricing.options : [];
|
|
1132
|
+
}
|
|
1133
|
+
function isFreePublicEventPriceDisplay(display) {
|
|
1134
|
+
switch (display.kind) {
|
|
1135
|
+
case "free": return true;
|
|
1136
|
+
case "exact":
|
|
1137
|
+
case "from":
|
|
1138
|
+
case "range":
|
|
1139
|
+
case "mixed":
|
|
1140
|
+
case "unavailable":
|
|
1141
|
+
case "external": return false;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function resolvePricingOptionSaleWindowState(option, now) {
|
|
1145
|
+
if (option.saleStartsAt !== null && option.saleStartsAt !== undefined && new Date(option.saleStartsAt) > now) {
|
|
1146
|
+
return "not_started";
|
|
1147
|
+
}
|
|
1148
|
+
if (option.saleEndsAt !== null && option.saleEndsAt !== undefined && new Date(option.saleEndsAt) < now) {
|
|
1149
|
+
return "ended";
|
|
1150
|
+
}
|
|
1151
|
+
return "open";
|
|
1152
|
+
}
|
|
1153
|
+
function resolveExactOrFreePriceDisplay(amount, currency) {
|
|
1154
|
+
const resolvedAmount = amount ?? 0;
|
|
1155
|
+
return resolvedAmount > 0 ? {
|
|
1156
|
+
kind: "exact",
|
|
1157
|
+
amount: resolvedAmount,
|
|
1158
|
+
currency
|
|
1159
|
+
} : {
|
|
1160
|
+
kind: "free",
|
|
1161
|
+
currency
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function resolvePricingOptionsPriceDisplay(input) {
|
|
1165
|
+
const now = input.purpose === "available_now" ? resolvePlanningNowDate({
|
|
1166
|
+
now: input.now,
|
|
1167
|
+
clock: input.clock
|
|
1168
|
+
}) : null;
|
|
1169
|
+
const amounts = input.options.filter((option) => option.isActive && (now === null || resolvePricingOptionSaleWindowState(option, now) === "open")).map((option) => option.priceAmount);
|
|
1170
|
+
return amounts.length === 0 ? {
|
|
1171
|
+
kind: "unavailable",
|
|
1172
|
+
currency: input.currency
|
|
1173
|
+
} : amountRangeDisplay(amounts, input.currency);
|
|
1174
|
+
}
|
|
1175
|
+
function amountRangeDisplay(amounts, currency) {
|
|
1176
|
+
const minAmount = Math.min(...amounts);
|
|
1177
|
+
const maxAmount = Math.max(...amounts);
|
|
1178
|
+
if (minAmount === maxAmount) {
|
|
1179
|
+
return minAmount === 0 ? {
|
|
1180
|
+
kind: "free",
|
|
1181
|
+
currency
|
|
1182
|
+
} : {
|
|
1183
|
+
kind: "exact",
|
|
1184
|
+
amount: minAmount,
|
|
1185
|
+
currency
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
if (minAmount === 0) {
|
|
1189
|
+
const paidAmounts = amounts.filter((amount) => amount > 0);
|
|
1190
|
+
return {
|
|
1191
|
+
kind: "mixed",
|
|
1192
|
+
paidFromAmount: Math.min(...paidAmounts),
|
|
1193
|
+
paidToAmount: maxAmount,
|
|
1194
|
+
currency
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
kind: "range",
|
|
1199
|
+
minAmount,
|
|
1200
|
+
maxAmount,
|
|
1201
|
+
currency
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
/** Derives configured price presentation directly from canonical pricing. */
|
|
1205
|
+
function canonicalEventPricingDisplay(pricing) {
|
|
1206
|
+
switch (pricing.kind) {
|
|
1207
|
+
case "free": return {
|
|
1208
|
+
kind: "free",
|
|
1209
|
+
currency: pricing.currency
|
|
1210
|
+
};
|
|
1211
|
+
case "single": return {
|
|
1212
|
+
kind: "exact",
|
|
1213
|
+
amount: pricing.amount,
|
|
1214
|
+
currency: pricing.currency
|
|
1215
|
+
};
|
|
1216
|
+
case "sliding_scale": return pricing.minimum === pricing.maximum ? {
|
|
1217
|
+
kind: "exact",
|
|
1218
|
+
amount: pricing.maximum,
|
|
1219
|
+
currency: pricing.currency
|
|
1220
|
+
} : {
|
|
1221
|
+
kind: "range",
|
|
1222
|
+
minAmount: pricing.minimum,
|
|
1223
|
+
maxAmount: pricing.maximum,
|
|
1224
|
+
currency: pricing.currency
|
|
1225
|
+
};
|
|
1226
|
+
case "tiered": {
|
|
1227
|
+
return resolvePricingOptionsPriceDisplay({
|
|
1228
|
+
options: pricing.options,
|
|
1229
|
+
currency: pricing.currency,
|
|
1230
|
+
purpose: "configured"
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
default: return assertNever(pricing);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* The single renderer for a precomputed booking price `display` ADT. This is
|
|
1238
|
+
* the one place booking price variants map to user-facing strings — every call
|
|
1239
|
+
* site (booking entry, events calendar, event/course details, listings, portal)
|
|
1240
|
+
* should route through here so labels never drift. Adding a variant to the ADT
|
|
1241
|
+
* breaks compilation here (via `assertNever`) until it is handled.
|
|
1242
|
+
*/
|
|
1243
|
+
function formatBookingPriceDisplay(display, options = {}) {
|
|
1244
|
+
switch (display.kind) {
|
|
1245
|
+
case "free": return "Free";
|
|
1246
|
+
case "exact": return formatCurrencyAmount(display.amount, display.currency, options);
|
|
1247
|
+
case "from": return `From ${formatCurrencyAmount(display.amount, display.currency, options)}`;
|
|
1248
|
+
case "range": return `${formatCurrencyAmount(display.minAmount, display.currency, options)} - ${formatCurrencyAmount(display.maxAmount, display.currency, options)}`;
|
|
1249
|
+
case "mixed": return display.paidToAmount > display.paidFromAmount ? `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)} - ${formatCurrencyAmount(display.paidToAmount, display.currency, options)}` : `Free / ${formatCurrencyAmount(display.paidFromAmount, display.currency, options)}`;
|
|
1250
|
+
case "unavailable": return "Pricing unavailable";
|
|
1251
|
+
case "external": return display.label;
|
|
1252
|
+
default: return assertNever(display);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
/** Render an event's price directly from its pricing value. */
|
|
1256
|
+
function formatEventPricing(pricing, options = {}) {
|
|
1257
|
+
return formatBookingPriceDisplay(publicEventPricingDisplay(pricing), options);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region ../core/src/event-page-surface-subject.ts
|
|
1262
|
+
function createEventSeriesPageSurfaceSubject(eventSeriesId, bookingScope, eventDestinationMode) {
|
|
1263
|
+
const mode = eventDestinationMode ? { eventDestinationMode } : {};
|
|
1264
|
+
return bookingScope === "whole_series" ? {
|
|
1265
|
+
kind: "eventSeries",
|
|
1266
|
+
eventSeriesId,
|
|
1267
|
+
bookingScope,
|
|
1268
|
+
isWholeSeriesBooking: true,
|
|
1269
|
+
...mode
|
|
1270
|
+
} : {
|
|
1271
|
+
kind: "eventSeries",
|
|
1272
|
+
eventSeriesId,
|
|
1273
|
+
bookingScope,
|
|
1274
|
+
...mode
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
function createEventOfferingPageSurfaceSubject(eventOfferingId, input) {
|
|
1278
|
+
const mode = input.eventDestinationMode ? { eventDestinationMode: input.eventDestinationMode } : {};
|
|
1279
|
+
const identity = {
|
|
1280
|
+
kind: "eventOffering",
|
|
1281
|
+
eventOfferingId,
|
|
1282
|
+
catalogueEventSeriesIds: input.catalogueEventSeriesIds
|
|
1283
|
+
};
|
|
1284
|
+
return input.bookingScope === "whole_series" ? {
|
|
1285
|
+
...identity,
|
|
1286
|
+
bookingScope: input.bookingScope,
|
|
1287
|
+
isWholeSeriesBooking: true,
|
|
1288
|
+
...mode
|
|
1289
|
+
} : {
|
|
1290
|
+
...identity,
|
|
1291
|
+
bookingScope: input.bookingScope,
|
|
1292
|
+
...mode
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Read the destination mode off a wire value.
|
|
1297
|
+
*
|
|
1298
|
+
* Absent or unrecognised reads as absent, which leaves the routed Event record
|
|
1299
|
+
* as the only authority — the behaviour every caller had before the field
|
|
1300
|
+
* existed.
|
|
1301
|
+
*/
|
|
1302
|
+
function parseEventPageSurfaceDestinationMode(value) {
|
|
1303
|
+
return value === "cms-managed" || value === "system-managed" ? value : undefined;
|
|
1304
|
+
}
|
|
1305
|
+
function parseEventPageSurfaceSubject(value) {
|
|
1306
|
+
if (!isRecord$8(value)) return err({ kind: "notObject" });
|
|
1307
|
+
switch (value.kind) {
|
|
1308
|
+
case "eventSeries": {
|
|
1309
|
+
const eventSeriesId = parseEventSeriesId(value.eventSeriesId);
|
|
1310
|
+
if (!eventSeriesId.ok) {
|
|
1311
|
+
return err({
|
|
1312
|
+
kind: "invalidEventSeriesId",
|
|
1313
|
+
error: eventSeriesId.error
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
if (value.bookingScope !== "single_occurrence" && value.bookingScope !== "whole_series") {
|
|
1317
|
+
return err({
|
|
1318
|
+
kind: "invalidEventSeriesBookingScope",
|
|
1319
|
+
bookingScope: value.bookingScope
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
return ok(createEventSeriesPageSurfaceSubject(eventSeriesId.value, value.bookingScope, parseEventPageSurfaceDestinationMode(value.eventDestinationMode)));
|
|
1323
|
+
}
|
|
1324
|
+
case "eventOffering": {
|
|
1325
|
+
const eventOfferingId = parseEventOfferingId(value.eventOfferingId);
|
|
1326
|
+
if (!eventOfferingId.ok) {
|
|
1327
|
+
return err({
|
|
1328
|
+
kind: "invalidEventOfferingId",
|
|
1329
|
+
error: eventOfferingId.error
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
if (!Array.isArray(value.catalogueEventSeriesIds)) {
|
|
1333
|
+
return err({
|
|
1334
|
+
kind: "invalidEventOfferingCatalogue",
|
|
1335
|
+
catalogue: value.catalogueEventSeriesIds
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
if (value.bookingScope !== "single_occurrence" && value.bookingScope !== "whole_series") {
|
|
1339
|
+
return err({
|
|
1340
|
+
kind: "invalidEventOfferingBookingScope",
|
|
1341
|
+
bookingScope: value.bookingScope
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
const parsedCatalogueIds = parseEventSeriesIdList(value.catalogueEventSeriesIds);
|
|
1345
|
+
if (!parsedCatalogueIds.ok) return parsedCatalogueIds;
|
|
1346
|
+
const eventDestinationMode = parseEventPageSurfaceDestinationMode(value.eventDestinationMode);
|
|
1347
|
+
return ok(createEventOfferingPageSurfaceSubject(eventOfferingId.value, {
|
|
1348
|
+
catalogueEventSeriesIds: parsedCatalogueIds.value,
|
|
1349
|
+
bookingScope: value.bookingScope,
|
|
1350
|
+
...eventDestinationMode ? { eventDestinationMode } : {}
|
|
1351
|
+
}));
|
|
1352
|
+
}
|
|
1353
|
+
default: return err({
|
|
1354
|
+
kind: "unsupportedSubject",
|
|
1355
|
+
subjectKind: value.kind
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function toEventPageSurfaceSubjectWire(subject) {
|
|
1360
|
+
return subject;
|
|
1361
|
+
}
|
|
1362
|
+
function parseEventSeriesIdList(values) {
|
|
1363
|
+
const parsed = [];
|
|
1364
|
+
for (const value of values) {
|
|
1365
|
+
const eventSeriesId = parseEventSeriesId(value);
|
|
1366
|
+
if (!eventSeriesId.ok) {
|
|
1367
|
+
return err({
|
|
1368
|
+
kind: "invalidEventSeriesId",
|
|
1369
|
+
error: eventSeriesId.error
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
parsed.push(eventSeriesId.value);
|
|
1373
|
+
}
|
|
1374
|
+
return ok(parsed);
|
|
1375
|
+
}
|
|
1376
|
+
function isRecord$8(value) {
|
|
1377
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
846
1380
|
//#endregion
|
|
847
1381
|
//#region ../core/src/booking-flexible-balance.ts
|
|
848
1382
|
const DEFAULT_FLEXIBLE_BALANCE_STRIPE_MINIMUM_CHARGE_CENTS = 50;
|
|
@@ -1824,19 +2358,6 @@ function unwrapNarrow(result) {
|
|
|
1824
2358
|
return result.value;
|
|
1825
2359
|
}
|
|
1826
2360
|
|
|
1827
|
-
//#endregion
|
|
1828
|
-
//#region ../core/src/non-empty-array.ts
|
|
1829
|
-
function fromArray(items) {
|
|
1830
|
-
const [first, ...rest] = items;
|
|
1831
|
-
if (first === undefined) {
|
|
1832
|
-
return err("empty_array");
|
|
1833
|
-
}
|
|
1834
|
-
return ok([first, ...rest]);
|
|
1835
|
-
}
|
|
1836
|
-
function mapNonEmptyArray(items, map) {
|
|
1837
|
-
return items.map(map);
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
2361
|
//#endregion
|
|
1841
2362
|
//#region ../core/src/editor-state.ts
|
|
1842
2363
|
const unsafeAsEditorSiteId = (value) => value;
|
|
@@ -2718,7 +3239,7 @@ const knownSocialPlatforms = [
|
|
|
2718
3239
|
"x",
|
|
2719
3240
|
"youtube"
|
|
2720
3241
|
];
|
|
2721
|
-
function isRecord$
|
|
3242
|
+
function isRecord$7(value) {
|
|
2722
3243
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2723
3244
|
}
|
|
2724
3245
|
function trimmedOptionalString(value) {
|
|
@@ -2728,7 +3249,7 @@ function trimmedOptionalString(value) {
|
|
|
2728
3249
|
return trimmed.length > 0 ? trimmed : null;
|
|
2729
3250
|
}
|
|
2730
3251
|
function parsePublicContactDetails(value) {
|
|
2731
|
-
if (!isRecord$
|
|
3252
|
+
if (!isRecord$7(value)) {
|
|
2732
3253
|
return err({
|
|
2733
3254
|
code: "invalid_shape",
|
|
2734
3255
|
path: "contact",
|
|
@@ -2824,7 +3345,7 @@ function minutesSinceMidnight(value) {
|
|
|
2824
3345
|
}
|
|
2825
3346
|
function parseOpeningHoursDay(value, expectedWeekday, index) {
|
|
2826
3347
|
const path = `openingHours.days.${index}`;
|
|
2827
|
-
if (!isRecord$
|
|
3348
|
+
if (!isRecord$7(value) || value.weekday !== expectedWeekday) {
|
|
2828
3349
|
return err({
|
|
2829
3350
|
code: "invalid_weekdays",
|
|
2830
3351
|
path,
|
|
@@ -2845,7 +3366,7 @@ function parseOpeningHoursDay(value, expectedWeekday, index) {
|
|
|
2845
3366
|
const intervals = [];
|
|
2846
3367
|
for (const [intervalIndex, rawInterval] of value.intervals.entries()) {
|
|
2847
3368
|
const intervalPath = `${path}.intervals.${intervalIndex}`;
|
|
2848
|
-
if (!isRecord$
|
|
3369
|
+
if (!isRecord$7(rawInterval)) {
|
|
2849
3370
|
return err({
|
|
2850
3371
|
code: "invalid_interval",
|
|
2851
3372
|
path: intervalPath,
|
|
@@ -2895,7 +3416,7 @@ function parseOpeningHoursDay(value, expectedWeekday, index) {
|
|
|
2895
3416
|
});
|
|
2896
3417
|
}
|
|
2897
3418
|
function parseWeeklyOpeningHours(value) {
|
|
2898
|
-
if (!isRecord$
|
|
3419
|
+
if (!isRecord$7(value) || !Array.isArray(value.days) || value.days.length !== publicProfileWeekdays.length) {
|
|
2899
3420
|
return err({
|
|
2900
3421
|
code: "invalid_weekdays",
|
|
2901
3422
|
path: "openingHours.days",
|
|
@@ -2975,7 +3496,7 @@ function parsePublicSocialLinks(value) {
|
|
|
2975
3496
|
const ids = new Set();
|
|
2976
3497
|
for (const [index, rawLink] of value.entries()) {
|
|
2977
3498
|
const path = `socialLinks.${index}`;
|
|
2978
|
-
if (!isRecord$
|
|
3499
|
+
if (!isRecord$7(rawLink)) {
|
|
2979
3500
|
return err({
|
|
2980
3501
|
code: "invalid_social_link",
|
|
2981
3502
|
path,
|
|
@@ -3022,7 +3543,7 @@ function parsePublicSocialLinks(value) {
|
|
|
3022
3543
|
return ok(links);
|
|
3023
3544
|
}
|
|
3024
3545
|
function parsePublicProfile(value) {
|
|
3025
|
-
if (!isRecord$
|
|
3546
|
+
if (!isRecord$7(value)) {
|
|
3026
3547
|
return err({
|
|
3027
3548
|
code: "invalid_shape",
|
|
3028
3549
|
path: "",
|
|
@@ -3035,23 +3556,36 @@ function parsePublicProfile(value) {
|
|
|
3035
3556
|
if (!openingHours.ok) return openingHours;
|
|
3036
3557
|
const socialLinks = parsePublicSocialLinks(value.socialLinks);
|
|
3037
3558
|
if (!socialLinks.ok) return socialLinks;
|
|
3559
|
+
const addressVisibility = parseAddressVisibility(value.addressVisibility);
|
|
3560
|
+
if (!addressVisibility.ok) return addressVisibility;
|
|
3038
3561
|
return ok({
|
|
3039
3562
|
contact: contact.value,
|
|
3040
3563
|
openingHours: openingHours.value,
|
|
3041
|
-
socialLinks: socialLinks.value
|
|
3564
|
+
socialLinks: socialLinks.value,
|
|
3565
|
+
addressVisibility: addressVisibility.value
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
function parseAddressVisibility(value) {
|
|
3569
|
+
if (value === undefined || value === null) return ok("shown");
|
|
3570
|
+
if (value === "shown" || value === "hidden") return ok(value);
|
|
3571
|
+
return err({
|
|
3572
|
+
code: "invalid_address_visibility",
|
|
3573
|
+
path: "addressVisibility",
|
|
3574
|
+
message: "Address visibility must be shown or hidden."
|
|
3042
3575
|
});
|
|
3043
3576
|
}
|
|
3044
3577
|
function emptyPublicProfile() {
|
|
3045
3578
|
return {
|
|
3046
3579
|
contact: null,
|
|
3047
3580
|
openingHours: null,
|
|
3048
|
-
socialLinks: []
|
|
3581
|
+
socialLinks: [],
|
|
3582
|
+
addressVisibility: "shown"
|
|
3049
3583
|
};
|
|
3050
3584
|
}
|
|
3051
3585
|
|
|
3052
3586
|
//#endregion
|
|
3053
3587
|
//#region ../core/src/rich-text.ts
|
|
3054
|
-
function isRecord$
|
|
3588
|
+
function isRecord$6(value) {
|
|
3055
3589
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3056
3590
|
}
|
|
3057
3591
|
function hasOnlyKeys(value, allowed) {
|
|
@@ -3073,10 +3607,10 @@ function isDenseArrayOf(value, predicate) {
|
|
|
3073
3607
|
return true;
|
|
3074
3608
|
}
|
|
3075
3609
|
function isTipTapMark(value) {
|
|
3076
|
-
return isRecord$
|
|
3610
|
+
return isRecord$6(value) && hasOnlyKeys(value, markKeys) && typeof value.type === "string" && value.type.length > 0 && (value.attrs === undefined || isRecord$6(value.attrs));
|
|
3077
3611
|
}
|
|
3078
3612
|
function isTipTapNode(value) {
|
|
3079
|
-
if (!isRecord$
|
|
3613
|
+
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))) {
|
|
3080
3614
|
return false;
|
|
3081
3615
|
}
|
|
3082
3616
|
if (value.type === "text") {
|
|
@@ -3085,89 +3619,222 @@ function isTipTapNode(value) {
|
|
|
3085
3619
|
return value.text === undefined;
|
|
3086
3620
|
}
|
|
3087
3621
|
function isCanonicalRichTextDoc(value) {
|
|
3088
|
-
return isRecord$
|
|
3622
|
+
return isRecord$6(value) && hasOnlyKeys(value, rootKeys) && value.type === "doc" && Array.isArray(value.content) && isDenseArrayOf(value.content, isTipTapNode);
|
|
3089
3623
|
}
|
|
3090
3624
|
|
|
3091
3625
|
//#endregion
|
|
3092
|
-
//#region ../core/src/
|
|
3626
|
+
//#region ../core/src/mailing-list-identifiers.ts
|
|
3093
3627
|
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;
|
|
3094
|
-
const
|
|
3628
|
+
const OPAQUE_ID_PATTERN = /^[A-Za-z0-9_-]{1,100}$/u;
|
|
3095
3629
|
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
|
|
3096
|
-
function
|
|
3097
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3098
|
-
}
|
|
3099
|
-
function parseTrimmedString(value) {
|
|
3630
|
+
function parseTrimmedMatchingString(value, pattern) {
|
|
3100
3631
|
if (typeof value !== "string") return null;
|
|
3101
3632
|
const trimmed = value.trim();
|
|
3102
|
-
return trimmed
|
|
3633
|
+
return pattern.test(trimmed) ? trimmed : null;
|
|
3634
|
+
}
|
|
3635
|
+
function parsePositiveInteger(value) {
|
|
3636
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
3103
3637
|
}
|
|
3104
3638
|
function parseNewsletterListId(value) {
|
|
3105
|
-
|
|
3106
|
-
return trimmed !== null && UUID_PATTERN.test(trimmed) ? trimmed : null;
|
|
3639
|
+
return parseTrimmedMatchingString(value, UUID_PATTERN);
|
|
3107
3640
|
}
|
|
3108
3641
|
function parseMailchimpConnectionId(value) {
|
|
3109
|
-
|
|
3110
|
-
|
|
3642
|
+
return parseTrimmedMatchingString(value, UUID_PATTERN);
|
|
3643
|
+
}
|
|
3644
|
+
function parseMailchimpAccountId(value) {
|
|
3645
|
+
return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
|
|
3111
3646
|
}
|
|
3112
3647
|
function parseMailchimpAudienceId(value) {
|
|
3113
|
-
|
|
3114
|
-
|
|
3648
|
+
return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
|
|
3649
|
+
}
|
|
3650
|
+
function parseMailchimpInterestId(value) {
|
|
3651
|
+
return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
|
|
3652
|
+
}
|
|
3653
|
+
function parseMailchimpTagName(value) {
|
|
3654
|
+
if (typeof value !== "string") return null;
|
|
3655
|
+
const trimmed = value.trim();
|
|
3656
|
+
return trimmed.length > 0 && trimmed.length <= 100 && !CONTROL_CHARACTER_PATTERN.test(trimmed) ? trimmed : null;
|
|
3657
|
+
}
|
|
3658
|
+
function parseBrevoConnectionId(value) {
|
|
3659
|
+
return parseTrimmedMatchingString(value, UUID_PATTERN);
|
|
3660
|
+
}
|
|
3661
|
+
function parseBrevoAccountId(value) {
|
|
3662
|
+
return parseTrimmedMatchingString(value, OPAQUE_ID_PATTERN);
|
|
3663
|
+
}
|
|
3664
|
+
function parseBrevoListId(value) {
|
|
3665
|
+
return parsePositiveInteger(value);
|
|
3666
|
+
}
|
|
3667
|
+
function parseBrevoConfirmationTemplateId(value) {
|
|
3668
|
+
return parsePositiveInteger(value);
|
|
3115
3669
|
}
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3670
|
+
|
|
3671
|
+
//#endregion
|
|
3672
|
+
//#region ../core/src/mailing-list-targeting-parser.ts
|
|
3673
|
+
function parseMailingListPlacementTargeting(value) {
|
|
3674
|
+
if (!isRecord$5(value)) return targetingError("shape");
|
|
3675
|
+
if (value.mode === "default") return ok({ mode: "default" });
|
|
3676
|
+
if (value.mode !== "custom") return targetingError("mode");
|
|
3677
|
+
if (!isRecord$5(value.selection)) return targetingError("selection");
|
|
3678
|
+
switch (value.provider) {
|
|
3679
|
+
case "native": return parseNativeTargeting(value.selection);
|
|
3680
|
+
case "brevo": return parseBrevoTargeting(value.selection);
|
|
3681
|
+
case "mailchimp": return parseMailchimpTargeting(value, value.selection);
|
|
3682
|
+
default: return targetingError("provider");
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
function parseMailingListVisitorSelection(value) {
|
|
3686
|
+
if (!isRecord$5(value)) return visitorSelectionError("shape");
|
|
3687
|
+
switch (value.provider) {
|
|
3688
|
+
case "native": {
|
|
3689
|
+
const topicIds = parseVisitorIds(value.topicIds, parseNewsletterListId);
|
|
3690
|
+
return topicIds === null ? visitorSelectionError("targets") : ok({
|
|
3691
|
+
provider: "native",
|
|
3692
|
+
topicIds
|
|
3133
3693
|
});
|
|
3134
3694
|
}
|
|
3135
|
-
case "
|
|
3136
|
-
const
|
|
3137
|
-
|
|
3138
|
-
|
|
3695
|
+
case "brevo": {
|
|
3696
|
+
const listIds = parseVisitorIds(value.listIds, parseBrevoListId);
|
|
3697
|
+
return listIds === null ? visitorSelectionError("targets") : ok({
|
|
3698
|
+
provider: "brevo",
|
|
3699
|
+
listIds
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
case "mailchimp": {
|
|
3703
|
+
const interestIds = parseVisitorIds(value.interestIds, parseMailchimpInterestId);
|
|
3704
|
+
return interestIds === null ? visitorSelectionError("targets") : ok({
|
|
3705
|
+
provider: "mailchimp",
|
|
3706
|
+
interestIds
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
default: return visitorSelectionError("provider");
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
function parseNativeTargeting(selection) {
|
|
3713
|
+
if (selection.tag === "fixed") {
|
|
3714
|
+
const topicIds = parseNonEmptyUniqueIds(selection.topicIds, parseNewsletterListId);
|
|
3715
|
+
return topicIds.ok ? ok({
|
|
3716
|
+
mode: "custom",
|
|
3717
|
+
provider: "native",
|
|
3718
|
+
selection: {
|
|
3719
|
+
tag: "fixed",
|
|
3720
|
+
topicIds: topicIds.value
|
|
3139
3721
|
}
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3722
|
+
}) : topicIds;
|
|
3723
|
+
}
|
|
3724
|
+
if (selection.tag === "visitor_choice") {
|
|
3725
|
+
const offeredTopicIds = parseNonEmptyUniqueIds(selection.offeredTopicIds, parseNewsletterListId);
|
|
3726
|
+
return offeredTopicIds.ok ? ok({
|
|
3727
|
+
mode: "custom",
|
|
3728
|
+
provider: "native",
|
|
3729
|
+
selection: {
|
|
3730
|
+
tag: "visitor_choice",
|
|
3731
|
+
offeredTopicIds: offeredTopicIds.value
|
|
3143
3732
|
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3733
|
+
}) : offeredTopicIds;
|
|
3734
|
+
}
|
|
3735
|
+
return targetingError("selection");
|
|
3736
|
+
}
|
|
3737
|
+
function parseBrevoTargeting(selection) {
|
|
3738
|
+
if (selection.tag === "fixed") {
|
|
3739
|
+
const listIds = parseNonEmptyUniqueIds(selection.listIds, parseBrevoListId);
|
|
3740
|
+
return listIds.ok ? ok({
|
|
3741
|
+
mode: "custom",
|
|
3742
|
+
provider: "brevo",
|
|
3743
|
+
selection: {
|
|
3744
|
+
tag: "fixed",
|
|
3745
|
+
listIds: listIds.value
|
|
3147
3746
|
}
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3747
|
+
}) : listIds;
|
|
3748
|
+
}
|
|
3749
|
+
if (selection.tag === "visitor_choice") {
|
|
3750
|
+
const offeredListIds = parseNonEmptyUniqueIds(selection.offeredListIds, parseBrevoListId);
|
|
3751
|
+
return offeredListIds.ok ? ok({
|
|
3752
|
+
mode: "custom",
|
|
3753
|
+
provider: "brevo",
|
|
3754
|
+
selection: {
|
|
3755
|
+
tag: "visitor_choice",
|
|
3756
|
+
offeredListIds: offeredListIds.value
|
|
3153
3757
|
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3758
|
+
}) : offeredListIds;
|
|
3759
|
+
}
|
|
3760
|
+
return targetingError("selection");
|
|
3761
|
+
}
|
|
3762
|
+
function parseMailchimpTargeting(value, selection) {
|
|
3763
|
+
const audienceId = parseMailchimpAudienceId(value.audienceId);
|
|
3764
|
+
if (audienceId === null) return targetingError("audienceId");
|
|
3765
|
+
if (selection.tag === "fixed") {
|
|
3766
|
+
const interestIds = parseUniqueIds(selection.interestIds, parseMailchimpInterestId);
|
|
3767
|
+
return interestIds.ok ? ok({
|
|
3768
|
+
mode: "custom",
|
|
3769
|
+
provider: "mailchimp",
|
|
3770
|
+
audienceId,
|
|
3771
|
+
selection: {
|
|
3772
|
+
tag: "fixed",
|
|
3773
|
+
interestIds: interestIds.value
|
|
3774
|
+
}
|
|
3775
|
+
}) : interestIds;
|
|
3776
|
+
}
|
|
3777
|
+
if (selection.tag === "visitor_choice") {
|
|
3778
|
+
const offeredInterestIds = parseNonEmptyUniqueIds(selection.offeredInterestIds, parseMailchimpInterestId);
|
|
3779
|
+
return offeredInterestIds.ok ? ok({
|
|
3780
|
+
mode: "custom",
|
|
3781
|
+
provider: "mailchimp",
|
|
3782
|
+
audienceId,
|
|
3783
|
+
selection: {
|
|
3784
|
+
tag: "visitor_choice",
|
|
3785
|
+
offeredInterestIds: offeredInterestIds.value
|
|
3786
|
+
}
|
|
3787
|
+
}) : offeredInterestIds;
|
|
3788
|
+
}
|
|
3789
|
+
return targetingError("selection");
|
|
3790
|
+
}
|
|
3791
|
+
function parseNonEmptyUniqueIds(value, parseId) {
|
|
3792
|
+
const parsed = parseUniqueIds(value, parseId);
|
|
3793
|
+
if (!parsed.ok) return parsed;
|
|
3794
|
+
const [first, ...rest] = parsed.value;
|
|
3795
|
+
return first === undefined ? targetingError("targets") : ok([first, ...rest]);
|
|
3796
|
+
}
|
|
3797
|
+
function parseUniqueIds(value, parseId) {
|
|
3798
|
+
if (!Array.isArray(value)) return targetingError("targets");
|
|
3799
|
+
const parsed = [];
|
|
3800
|
+
for (const item of value) {
|
|
3801
|
+
const id = parseId(item);
|
|
3802
|
+
if (id === null) return targetingError("targets");
|
|
3803
|
+
if (parsed.includes(id)) return targetingError("duplicate_target");
|
|
3804
|
+
parsed.push(id);
|
|
3805
|
+
}
|
|
3806
|
+
return ok(parsed);
|
|
3807
|
+
}
|
|
3808
|
+
function parseVisitorIds(value, parseId) {
|
|
3809
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
3810
|
+
const parsed = [];
|
|
3811
|
+
for (const item of value) {
|
|
3812
|
+
const id = parseId(item);
|
|
3813
|
+
if (id === null) return null;
|
|
3814
|
+
if (parsed.includes(id)) return null;
|
|
3815
|
+
parsed.push(id);
|
|
3165
3816
|
}
|
|
3817
|
+
return parsed;
|
|
3818
|
+
}
|
|
3819
|
+
function isRecord$5(value) {
|
|
3820
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3821
|
+
}
|
|
3822
|
+
function targetingError(field) {
|
|
3823
|
+
return err({
|
|
3824
|
+
tag: "invalid_mailing_list_targeting",
|
|
3825
|
+
field
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
function visitorSelectionError(field) {
|
|
3829
|
+
return err({
|
|
3830
|
+
tag: "invalid_mailing_list_visitor_selection",
|
|
3831
|
+
field
|
|
3832
|
+
});
|
|
3166
3833
|
}
|
|
3167
3834
|
|
|
3168
3835
|
//#endregion
|
|
3169
3836
|
//#region ../core/src/footer-engagement.ts
|
|
3170
|
-
function isRecord$
|
|
3837
|
+
function isRecord$4(value) {
|
|
3171
3838
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3172
3839
|
}
|
|
3173
3840
|
function invalid(path, message) {
|
|
@@ -3201,7 +3868,7 @@ function parseEngagementBase(value, base, path) {
|
|
|
3201
3868
|
}
|
|
3202
3869
|
const treatment = parseTreatment(value.treatment, `${path}.treatment`);
|
|
3203
3870
|
if (!treatment.ok) return treatment;
|
|
3204
|
-
if (!isRecord$
|
|
3871
|
+
if (!isRecord$4(value.source)) {
|
|
3205
3872
|
return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
|
|
3206
3873
|
}
|
|
3207
3874
|
const intro = parseBoundedText(value.source.intro, `${path}.source.intro`, {
|
|
@@ -3230,36 +3897,17 @@ function parseStandardContact(source, base, path) {
|
|
|
3230
3897
|
}
|
|
3231
3898
|
function parseMailingList(source, base, path) {
|
|
3232
3899
|
const { intro, ...section } = base;
|
|
3233
|
-
const
|
|
3234
|
-
if (!
|
|
3235
|
-
return err(invalid(`${path}.
|
|
3236
|
-
}
|
|
3237
|
-
const buttonLabel = parseBoundedText(source.buttonLabel, `${path}.buttonLabel`, { maxLength: 40 });
|
|
3238
|
-
if (!buttonLabel.ok) return buttonLabel;
|
|
3239
|
-
const successMessage = parseBoundedText(source.successMessage, `${path}.successMessage`, { maxLength: 160 });
|
|
3240
|
-
if (!successMessage.ok) return successMessage;
|
|
3241
|
-
const privacyNote = parseBoundedText(source.privacyNote, `${path}.privacyNote`, {
|
|
3242
|
-
maxLength: 240,
|
|
3243
|
-
optional: true
|
|
3244
|
-
});
|
|
3245
|
-
if (!privacyNote.ok) return privacyNote;
|
|
3246
|
-
if (buttonLabel.value === null || successMessage.value === null) {
|
|
3247
|
-
return err(invalid(path, "Footer mailing-list copy cannot be empty."));
|
|
3248
|
-
}
|
|
3249
|
-
if (source.fieldPreset !== "emailOnly") {
|
|
3250
|
-
return err(invalid(`${path}.fieldPreset`, "Footer mailing-list signup must be email-only."));
|
|
3900
|
+
const targeting = parseMailingListPlacementTargeting(source.targeting);
|
|
3901
|
+
if (!targeting.ok) {
|
|
3902
|
+
return err(invalid(`${path}.targeting`, "Footer mailing-list targeting is invalid."));
|
|
3251
3903
|
}
|
|
3252
3904
|
return ok({
|
|
3253
3905
|
...section,
|
|
3254
3906
|
kind: "engagement",
|
|
3255
3907
|
source: {
|
|
3256
3908
|
kind: "mailing-list",
|
|
3257
|
-
|
|
3258
|
-
intro
|
|
3259
|
-
buttonLabel: buttonLabel.value,
|
|
3260
|
-
successMessage: successMessage.value,
|
|
3261
|
-
privacyNote: privacyNote.value,
|
|
3262
|
-
fieldPreset: "emailOnly"
|
|
3909
|
+
targeting: targeting.value,
|
|
3910
|
+
intro
|
|
3263
3911
|
}
|
|
3264
3912
|
});
|
|
3265
3913
|
}
|
|
@@ -3267,7 +3915,7 @@ function parseFooterEngagementSectionValue(value, sectionBase, path) {
|
|
|
3267
3915
|
const base = parseEngagementBase(value, sectionBase, path);
|
|
3268
3916
|
if (!base.ok) return base;
|
|
3269
3917
|
const source = value.source;
|
|
3270
|
-
if (!isRecord$
|
|
3918
|
+
if (!isRecord$4(source)) {
|
|
3271
3919
|
return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
|
|
3272
3920
|
}
|
|
3273
3921
|
switch (source.kind) {
|
|
@@ -3382,7 +4030,7 @@ function unsafeAsFooterModuleId(value) {
|
|
|
3382
4030
|
function parseFooterModuleId(value) {
|
|
3383
4031
|
return parseSectionId(value, "sectionId");
|
|
3384
4032
|
}
|
|
3385
|
-
function isRecord$
|
|
4033
|
+
function isRecord$3(value) {
|
|
3386
4034
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3387
4035
|
}
|
|
3388
4036
|
function isRichText(value) {
|
|
@@ -3412,7 +4060,7 @@ function parseHeading(value, path) {
|
|
|
3412
4060
|
}
|
|
3413
4061
|
function parseFooterSection(value, index) {
|
|
3414
4062
|
const path = `sections.${index}`;
|
|
3415
|
-
if (!isRecord$
|
|
4063
|
+
if (!isRecord$3(value)) {
|
|
3416
4064
|
return err({
|
|
3417
4065
|
code: "invalid_section",
|
|
3418
4066
|
path,
|
|
@@ -3447,7 +4095,7 @@ function parseFooterSection(value, index) {
|
|
|
3447
4095
|
}
|
|
3448
4096
|
break;
|
|
3449
4097
|
case "navigation":
|
|
3450
|
-
if (isRecord$
|
|
4098
|
+
if (isRecord$3(value.source) && value.source.kind === "menu-role" && value.source.role === "main") {
|
|
3451
4099
|
return ok({
|
|
3452
4100
|
...base,
|
|
3453
4101
|
kind: "navigation",
|
|
@@ -3460,7 +4108,7 @@ function parseFooterSection(value, index) {
|
|
|
3460
4108
|
}
|
|
3461
4109
|
break;
|
|
3462
4110
|
case "legal-navigation":
|
|
3463
|
-
if (isRecord$
|
|
4111
|
+
if (isRecord$3(value.source) && value.source.kind === "menu-role" && value.source.role === "legal-policy") {
|
|
3464
4112
|
return ok({
|
|
3465
4113
|
...base,
|
|
3466
4114
|
kind: "legal-navigation",
|
|
@@ -3473,7 +4121,7 @@ function parseFooterSection(value, index) {
|
|
|
3473
4121
|
}
|
|
3474
4122
|
break;
|
|
3475
4123
|
case "business-address":
|
|
3476
|
-
if (isRecord$
|
|
4124
|
+
if (isRecord$3(value.source) && value.source.kind === "business-address") {
|
|
3477
4125
|
return ok({
|
|
3478
4126
|
...base,
|
|
3479
4127
|
kind: "business-address",
|
|
@@ -3485,7 +4133,7 @@ function parseFooterSection(value, index) {
|
|
|
3485
4133
|
case "contact":
|
|
3486
4134
|
case "opening-hours":
|
|
3487
4135
|
case "social-links":
|
|
3488
|
-
if (isRecord$
|
|
4136
|
+
if (isRecord$3(value.source) && value.source.kind === "public-profile") {
|
|
3489
4137
|
return ok({
|
|
3490
4138
|
...base,
|
|
3491
4139
|
kind: value.kind,
|
|
@@ -3537,7 +4185,7 @@ function validateFooterComposition(composition) {
|
|
|
3537
4185
|
return ok(composition);
|
|
3538
4186
|
}
|
|
3539
4187
|
function parseFooterCompositionV1(value) {
|
|
3540
|
-
if (!isRecord$
|
|
4188
|
+
if (!isRecord$3(value)) {
|
|
3541
4189
|
return err({
|
|
3542
4190
|
code: "invalid_shape",
|
|
3543
4191
|
path: "",
|
|
@@ -3570,7 +4218,7 @@ function parseFooterCompositionV1(value) {
|
|
|
3570
4218
|
});
|
|
3571
4219
|
}
|
|
3572
4220
|
function parseLegacyPlacementSection(value, index, selectedIds) {
|
|
3573
|
-
if (!isRecord$
|
|
4221
|
+
if (!isRecord$3(value)) {
|
|
3574
4222
|
return err({
|
|
3575
4223
|
code: "invalid_section",
|
|
3576
4224
|
path: `modules.${index}`,
|
|
@@ -3578,7 +4226,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
|
|
|
3578
4226
|
});
|
|
3579
4227
|
}
|
|
3580
4228
|
const selected = typeof value.id === "string" && selectedIds.has(value.id);
|
|
3581
|
-
const source = isRecord$
|
|
4229
|
+
const source = isRecord$3(value.source) ? value.source : null;
|
|
3582
4230
|
const mapped = value.kind === "navigation" && source?.kind === "menu-role" && source.role === "footer" ? {
|
|
3583
4231
|
...value,
|
|
3584
4232
|
source: {
|
|
@@ -3597,7 +4245,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
|
|
|
3597
4245
|
* It is intentionally not called by the runtime parser.
|
|
3598
4246
|
*/
|
|
3599
4247
|
function upgradePlacementFooterCompositionV1(value) {
|
|
3600
|
-
if (!isRecord$
|
|
4248
|
+
if (!isRecord$3(value) || value.schemaVersion !== 1 || !Array.isArray(value.modules) || !Array.isArray(value.placements)) {
|
|
3601
4249
|
return err({
|
|
3602
4250
|
code: "invalid_shape",
|
|
3603
4251
|
path: "",
|
|
@@ -3606,7 +4254,7 @@ function upgradePlacementFooterCompositionV1(value) {
|
|
|
3606
4254
|
}
|
|
3607
4255
|
const selectedIds = new Set();
|
|
3608
4256
|
for (const [index, placement] of value.placements.entries()) {
|
|
3609
|
-
if (!isRecord$
|
|
4257
|
+
if (!isRecord$3(placement) || typeof placement.moduleId !== "string") {
|
|
3610
4258
|
return err({
|
|
3611
4259
|
code: "invalid_shape",
|
|
3612
4260
|
path: `placements.${index}`,
|
|
@@ -3709,8 +4357,8 @@ function repairTargetFor(section) {
|
|
|
3709
4357
|
};
|
|
3710
4358
|
}
|
|
3711
4359
|
}
|
|
3712
|
-
function resolveFooterSections(composition, availability) {
|
|
3713
|
-
return composition.sections.filter((section) => section.selected).map((section) => isSectionRenderable(section, availability) ? {
|
|
4360
|
+
function resolveFooterSections(composition, availability$1) {
|
|
4361
|
+
return composition.sections.filter((section) => section.selected).map((section) => isSectionRenderable(section, availability$1) ? {
|
|
3714
4362
|
kind: "renderable",
|
|
3715
4363
|
section
|
|
3716
4364
|
} : {
|
|
@@ -3719,127 +4367,837 @@ function resolveFooterSections(composition, availability) {
|
|
|
3719
4367
|
repairTarget: repairTargetFor(section)
|
|
3720
4368
|
});
|
|
3721
4369
|
}
|
|
3722
|
-
function isSectionRenderable(section, availability) {
|
|
3723
|
-
switch (section.kind) {
|
|
3724
|
-
case "brand": return section.showLogo && availability.hasLogo || section.showTitle;
|
|
3725
|
-
case "navigation": return availability.hasMainNavigation;
|
|
3726
|
-
case "legal-navigation": return availability.hasLegalPolicyNavigation;
|
|
3727
|
-
case "business-address": return availability.hasBusinessAddress;
|
|
3728
|
-
case "contact": return availability.hasContact;
|
|
3729
|
-
case "opening-hours": return availability.hasOpeningHours;
|
|
3730
|
-
case "social-links": return availability.hasSocialLinks;
|
|
3731
|
-
case "engagement": return availability.hasEngagementSource;
|
|
3732
|
-
case "supporting-text":
|
|
3733
|
-
case "legal": return section.content !== null;
|
|
4370
|
+
function isSectionRenderable(section, availability$1) {
|
|
4371
|
+
switch (section.kind) {
|
|
4372
|
+
case "brand": return section.showLogo && availability$1.hasLogo || section.showTitle;
|
|
4373
|
+
case "navigation": return availability$1.hasMainNavigation;
|
|
4374
|
+
case "legal-navigation": return availability$1.hasLegalPolicyNavigation;
|
|
4375
|
+
case "business-address": return availability$1.hasBusinessAddress;
|
|
4376
|
+
case "contact": return availability$1.hasContact;
|
|
4377
|
+
case "opening-hours": return availability$1.hasOpeningHours;
|
|
4378
|
+
case "social-links": return availability$1.hasSocialLinks;
|
|
4379
|
+
case "engagement": return availability$1.hasEngagementSource;
|
|
4380
|
+
case "supporting-text":
|
|
4381
|
+
case "legal": return section.content !== null;
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
function deriveFooterEditorOverview(composition) {
|
|
4385
|
+
return {
|
|
4386
|
+
selected: composition.sections.filter((section) => section.selected).map((section) => ({
|
|
4387
|
+
section,
|
|
4388
|
+
status: "selected"
|
|
4389
|
+
})),
|
|
4390
|
+
available: composition.sections.filter((section) => !section.selected).map((section) => ({
|
|
4391
|
+
section,
|
|
4392
|
+
status: "available"
|
|
4393
|
+
}))
|
|
4394
|
+
};
|
|
4395
|
+
}
|
|
4396
|
+
const defaultSections = (legacy, options) => {
|
|
4397
|
+
const footerVisible = options.footerVisible ?? true;
|
|
4398
|
+
return [
|
|
4399
|
+
{
|
|
4400
|
+
id: unsafeAsFooterModuleId("brand"),
|
|
4401
|
+
kind: "brand",
|
|
4402
|
+
heading: null,
|
|
4403
|
+
selected: footerVisible,
|
|
4404
|
+
showLogo: options.brand?.showLogo ?? true,
|
|
4405
|
+
showTitle: options.brand?.showTitle ?? true
|
|
4406
|
+
},
|
|
4407
|
+
{
|
|
4408
|
+
id: unsafeAsFooterModuleId("navigation"),
|
|
4409
|
+
kind: "navigation",
|
|
4410
|
+
heading: null,
|
|
4411
|
+
selected: footerVisible,
|
|
4412
|
+
source: {
|
|
4413
|
+
kind: "menu-role",
|
|
4414
|
+
role: "main"
|
|
4415
|
+
}
|
|
4416
|
+
},
|
|
4417
|
+
{
|
|
4418
|
+
id: unsafeAsFooterModuleId("legal-navigation"),
|
|
4419
|
+
kind: "legal-navigation",
|
|
4420
|
+
heading: null,
|
|
4421
|
+
selected: false,
|
|
4422
|
+
source: {
|
|
4423
|
+
kind: "menu-role",
|
|
4424
|
+
role: "legal-policy"
|
|
4425
|
+
}
|
|
4426
|
+
},
|
|
4427
|
+
{
|
|
4428
|
+
id: unsafeAsFooterModuleId("business-address"),
|
|
4429
|
+
kind: "business-address",
|
|
4430
|
+
heading: null,
|
|
4431
|
+
selected: footerVisible && legacy.showBusinessAddress === true,
|
|
4432
|
+
source: { kind: "business-address" }
|
|
4433
|
+
},
|
|
4434
|
+
{
|
|
4435
|
+
id: unsafeAsFooterModuleId("contact"),
|
|
4436
|
+
kind: "contact",
|
|
4437
|
+
heading: null,
|
|
4438
|
+
selected: false,
|
|
4439
|
+
source: { kind: "public-profile" }
|
|
4440
|
+
},
|
|
4441
|
+
{
|
|
4442
|
+
id: unsafeAsFooterModuleId("opening-hours"),
|
|
4443
|
+
kind: "opening-hours",
|
|
4444
|
+
heading: null,
|
|
4445
|
+
selected: false,
|
|
4446
|
+
source: { kind: "public-profile" }
|
|
4447
|
+
},
|
|
4448
|
+
{
|
|
4449
|
+
id: unsafeAsFooterModuleId("social-links"),
|
|
4450
|
+
kind: "social-links",
|
|
4451
|
+
heading: null,
|
|
4452
|
+
selected: false,
|
|
4453
|
+
source: { kind: "public-profile" }
|
|
4454
|
+
},
|
|
4455
|
+
{
|
|
4456
|
+
id: unsafeAsFooterModuleId("supporting-text"),
|
|
4457
|
+
kind: "supporting-text",
|
|
4458
|
+
heading: null,
|
|
4459
|
+
selected: footerVisible && isRichText(legacy.bodyText),
|
|
4460
|
+
content: isRichText(legacy.bodyText) ? legacy.bodyText : null
|
|
4461
|
+
},
|
|
4462
|
+
{
|
|
4463
|
+
id: unsafeAsFooterModuleId("legal"),
|
|
4464
|
+
kind: "legal",
|
|
4465
|
+
heading: null,
|
|
4466
|
+
selected: footerVisible && isRichText(legacy.bottomText),
|
|
4467
|
+
content: isRichText(legacy.bottomText) ? legacy.bottomText : null
|
|
4468
|
+
}
|
|
4469
|
+
];
|
|
4470
|
+
};
|
|
4471
|
+
function normalizeLegacyFooterContentToV1(value, options = {}) {
|
|
4472
|
+
if (!isRecord$3(value)) {
|
|
4473
|
+
return err({
|
|
4474
|
+
code: "invalid_shape",
|
|
4475
|
+
path: "",
|
|
4476
|
+
message: "Legacy footer content must be an object."
|
|
4477
|
+
});
|
|
4478
|
+
}
|
|
4479
|
+
return validateFooterComposition({
|
|
4480
|
+
schemaVersion: 1,
|
|
4481
|
+
sections: defaultSections(value, options)
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
//#endregion
|
|
4486
|
+
//#region ../core/src/footer-navigation-layout.ts
|
|
4487
|
+
const footerNavigationLayouts = ["stack", "inline-wrap"];
|
|
4488
|
+
const defaultFooterNavigationLayout = "inline-wrap";
|
|
4489
|
+
function normalizeFooterNavigationLayout(value) {
|
|
4490
|
+
return value === "stack" ? "stack" : defaultFooterNavigationLayout;
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
//#endregion
|
|
4494
|
+
//#region ../core/src/mailing-list-setup.ts
|
|
4495
|
+
function planMailingListFieldCompatibility(input) {
|
|
4496
|
+
const collectedFields = resolveCollectedGlobalFields(input.form);
|
|
4497
|
+
const requiredFields = resolveRequiredGlobalFields(input.form.fields);
|
|
4498
|
+
const issues = [];
|
|
4499
|
+
for (const destination of input.destinations) {
|
|
4500
|
+
for (const field of destination.requiredFields) {
|
|
4501
|
+
if (!collectedFields.includes(field)) {
|
|
4502
|
+
issues.push({
|
|
4503
|
+
tag: "destination_requires_uncollected_field",
|
|
4504
|
+
destinationKey: destination.destinationKey,
|
|
4505
|
+
field
|
|
4506
|
+
});
|
|
4507
|
+
} else if (!requiredFields.includes(field)) {
|
|
4508
|
+
issues.push({
|
|
4509
|
+
tag: "destination_requires_site_required_field",
|
|
4510
|
+
destinationKey: destination.destinationKey,
|
|
4511
|
+
field
|
|
4512
|
+
});
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
for (const field of collectedFields) {
|
|
4516
|
+
if (!destination.acceptedFields.includes(field)) {
|
|
4517
|
+
issues.push({
|
|
4518
|
+
tag: requiredFields.includes(field) ? "destination_rejects_required_site_field" : "destination_rejects_collected_site_field",
|
|
4519
|
+
destinationKey: destination.destinationKey,
|
|
4520
|
+
field
|
|
4521
|
+
});
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
const [first, ...rest] = issues;
|
|
4526
|
+
return first === undefined ? { tag: "compatible" } : {
|
|
4527
|
+
tag: "blocked",
|
|
4528
|
+
issues: [first, ...rest]
|
|
4529
|
+
};
|
|
4530
|
+
}
|
|
4531
|
+
function resolveCollectedGlobalFields(form) {
|
|
4532
|
+
const phone = form.includeOptionalPhone ? ["phone"] : [];
|
|
4533
|
+
switch (form.fields.preset) {
|
|
4534
|
+
case "emailOnly": return phone;
|
|
4535
|
+
case "emailName": return ["name", ...phone];
|
|
4536
|
+
case "emailFirstLast": return [
|
|
4537
|
+
"first_name",
|
|
4538
|
+
"last_name",
|
|
4539
|
+
...phone
|
|
4540
|
+
];
|
|
4541
|
+
case "emailNameCompany": return [
|
|
4542
|
+
"name",
|
|
4543
|
+
"company",
|
|
4544
|
+
...phone
|
|
4545
|
+
];
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
function resolveRequiredGlobalFields(fields) {
|
|
4549
|
+
switch (fields.preset) {
|
|
4550
|
+
case "emailOnly": return [];
|
|
4551
|
+
case "emailName": return fields.name === "required" ? ["name"] : [];
|
|
4552
|
+
case "emailFirstLast": return [...fields.firstName === "required" ? ["first_name"] : [], ...fields.lastName === "required" ? ["last_name"] : []];
|
|
4553
|
+
case "emailNameCompany": return [...fields.name === "required" ? ["name"] : [], ...fields.company === "required" ? ["company"] : []];
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
function planMailingListSetupSwitch(input) {
|
|
4557
|
+
if (input.previousScope === null) {
|
|
4558
|
+
return {
|
|
4559
|
+
tag: "activate",
|
|
4560
|
+
affectedPlacementCount: 0
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4563
|
+
if (sameProviderScope(input.previousScope, input.nextScope)) {
|
|
4564
|
+
return {
|
|
4565
|
+
tag: "update_same_scope",
|
|
4566
|
+
affectedPlacementCount: input.defaultChanged ? countDefaultMailingListPlacements(input.placementModes) : 0
|
|
4567
|
+
};
|
|
4568
|
+
}
|
|
4569
|
+
return {
|
|
4570
|
+
tag: "replace_scope",
|
|
4571
|
+
affectedPlacementCount: input.placementModes.length
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
function sameProviderScope(previous, next) {
|
|
4575
|
+
if (previous.provider !== next.provider) return false;
|
|
4576
|
+
switch (previous.provider) {
|
|
4577
|
+
case "native": return true;
|
|
4578
|
+
case "mailchimp": return next.provider === "mailchimp" && previous.accountId === next.accountId;
|
|
4579
|
+
case "brevo": return next.provider === "brevo" && previous.accountId === next.accountId;
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
function countDefaultMailingListPlacements(placementModes) {
|
|
4583
|
+
return placementModes.filter((mode) => mode === "default").length;
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
//#endregion
|
|
4587
|
+
//#region ../core/src/mailing-list-setup-parser.ts
|
|
4588
|
+
function parseMailingListGlobalForm(value) {
|
|
4589
|
+
if (!isRecord$2(value)) return formError("shape");
|
|
4590
|
+
const fields = parseGlobalFields(value.fields);
|
|
4591
|
+
if (fields === null) return formError("fields");
|
|
4592
|
+
if (typeof value.includeOptionalPhone !== "boolean") {
|
|
4593
|
+
return formError("includeOptionalPhone");
|
|
4594
|
+
}
|
|
4595
|
+
const buttonLabel = parseRequiredString(value.buttonLabel);
|
|
4596
|
+
if (buttonLabel === null) return formError("buttonLabel");
|
|
4597
|
+
const privacyNote = parseOptionalString(value.privacyNote);
|
|
4598
|
+
if (privacyNote === undefined) return formError("privacyNote");
|
|
4599
|
+
return ok({
|
|
4600
|
+
fields,
|
|
4601
|
+
includeOptionalPhone: value.includeOptionalPhone,
|
|
4602
|
+
buttonLabel,
|
|
4603
|
+
privacyNote
|
|
4604
|
+
});
|
|
4605
|
+
}
|
|
4606
|
+
function parseMailingListSiteSetup(value) {
|
|
4607
|
+
if (!isRecord$2(value)) return setupError("shape");
|
|
4608
|
+
const siteId = parseSiteId(value.siteId);
|
|
4609
|
+
if (!siteId.ok) return setupError("siteId");
|
|
4610
|
+
if (value.tag === "not_configured") {
|
|
4611
|
+
return ok({
|
|
4612
|
+
tag: "not_configured",
|
|
4613
|
+
siteId: siteId.value
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4616
|
+
if (value.tag !== "configured") return setupError("tag");
|
|
4617
|
+
const revision = parseMailingListSetupRevision(value.revision);
|
|
4618
|
+
if (revision === null) return setupError("revision");
|
|
4619
|
+
const form = parseMailingListGlobalForm(value.form);
|
|
4620
|
+
if (!form.ok) return setupError("form");
|
|
4621
|
+
const provider = parseConfiguredMailingListProvider(value.provider);
|
|
4622
|
+
if (!provider.ok) return provider;
|
|
4623
|
+
return ok({
|
|
4624
|
+
tag: "configured",
|
|
4625
|
+
siteId: siteId.value,
|
|
4626
|
+
revision,
|
|
4627
|
+
form: form.value,
|
|
4628
|
+
provider: provider.value
|
|
4629
|
+
});
|
|
4630
|
+
}
|
|
4631
|
+
function parseConfiguredMailingListProvider(value) {
|
|
4632
|
+
if (!isRecord$2(value) || typeof value.provider !== "string") {
|
|
4633
|
+
return setupError("provider");
|
|
4634
|
+
}
|
|
4635
|
+
switch (value.provider) {
|
|
4636
|
+
case "native": {
|
|
4637
|
+
const defaultTopicId = parseNewsletterListId(value.defaultTopicId);
|
|
4638
|
+
if (defaultTopicId === null) return setupError("defaultTargetId");
|
|
4639
|
+
const confirmation = parseToggleConfirmation(value.confirmation);
|
|
4640
|
+
return confirmation === null ? setupError("confirmation") : ok({
|
|
4641
|
+
provider: "native",
|
|
4642
|
+
defaultTopicId,
|
|
4643
|
+
confirmation
|
|
4644
|
+
});
|
|
4645
|
+
}
|
|
4646
|
+
case "mailchimp": {
|
|
4647
|
+
const connectionId = parseMailchimpConnectionId(value.connectionId);
|
|
4648
|
+
if (connectionId === null) return setupError("connectionId");
|
|
4649
|
+
const accountId = parseMailchimpAccountId(value.accountId);
|
|
4650
|
+
if (accountId === null) return setupError("accountId");
|
|
4651
|
+
const defaultAudienceId = parseMailchimpAudienceId(value.defaultAudienceId);
|
|
4652
|
+
if (defaultAudienceId === null) return setupError("defaultTargetId");
|
|
4653
|
+
const confirmation = parseToggleConfirmation(value.confirmation);
|
|
4654
|
+
return confirmation === null ? setupError("confirmation") : ok({
|
|
4655
|
+
provider: "mailchimp",
|
|
4656
|
+
connectionId,
|
|
4657
|
+
accountId,
|
|
4658
|
+
defaultAudienceId,
|
|
4659
|
+
confirmation
|
|
4660
|
+
});
|
|
4661
|
+
}
|
|
4662
|
+
case "brevo": {
|
|
4663
|
+
const connectionId = parseBrevoConnectionId(value.connectionId);
|
|
4664
|
+
if (connectionId === null) return setupError("connectionId");
|
|
4665
|
+
const accountId = parseBrevoAccountId(value.accountId);
|
|
4666
|
+
if (accountId === null) return setupError("accountId");
|
|
4667
|
+
const defaultListId = parseBrevoListId(value.defaultListId);
|
|
4668
|
+
if (defaultListId === null) return setupError("defaultTargetId");
|
|
4669
|
+
const confirmation = parseBrevoConfirmation(value.confirmation);
|
|
4670
|
+
return confirmation === null ? setupError("confirmation") : ok({
|
|
4671
|
+
provider: "brevo",
|
|
4672
|
+
connectionId,
|
|
4673
|
+
accountId,
|
|
4674
|
+
defaultListId,
|
|
4675
|
+
confirmation
|
|
4676
|
+
});
|
|
4677
|
+
}
|
|
4678
|
+
default: return setupError("provider");
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
function parseGlobalFields(value) {
|
|
4682
|
+
if (!isRecord$2(value) || typeof value.preset !== "string") return null;
|
|
4683
|
+
switch (value.preset) {
|
|
4684
|
+
case "emailOnly": return { preset: "emailOnly" };
|
|
4685
|
+
case "emailName": return isRequirement(value.name) ? {
|
|
4686
|
+
preset: "emailName",
|
|
4687
|
+
name: value.name
|
|
4688
|
+
} : null;
|
|
4689
|
+
case "emailFirstLast": return isRequirement(value.firstName) && isRequirement(value.lastName) ? {
|
|
4690
|
+
preset: "emailFirstLast",
|
|
4691
|
+
firstName: value.firstName,
|
|
4692
|
+
lastName: value.lastName
|
|
4693
|
+
} : null;
|
|
4694
|
+
case "emailNameCompany": return isRequirement(value.name) && isRequirement(value.company) ? {
|
|
4695
|
+
preset: "emailNameCompany",
|
|
4696
|
+
name: value.name,
|
|
4697
|
+
company: value.company
|
|
4698
|
+
} : null;
|
|
4699
|
+
default: return null;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
function parseToggleConfirmation(value) {
|
|
4703
|
+
if (!isRecord$2(value)) return null;
|
|
4704
|
+
return value.tag === "on" || value.tag === "off" ? { tag: value.tag } : null;
|
|
4705
|
+
}
|
|
4706
|
+
function parseBrevoConfirmation(value) {
|
|
4707
|
+
if (!isRecord$2(value)) return null;
|
|
4708
|
+
if (value.tag === "off") return { tag: "off" };
|
|
4709
|
+
if (value.tag !== "on") return null;
|
|
4710
|
+
const templateId = parseBrevoConfirmationTemplateId(value.templateId);
|
|
4711
|
+
const redirectUrl = parseHttpUrl(value.redirectUrl);
|
|
4712
|
+
return templateId === null || redirectUrl === null ? null : {
|
|
4713
|
+
tag: "on",
|
|
4714
|
+
templateId,
|
|
4715
|
+
redirectUrl
|
|
4716
|
+
};
|
|
4717
|
+
}
|
|
4718
|
+
function parseMailingListSetupRevision(value) {
|
|
4719
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
4720
|
+
}
|
|
4721
|
+
function parseHttpUrl(value) {
|
|
4722
|
+
if (typeof value !== "string") return null;
|
|
4723
|
+
try {
|
|
4724
|
+
const url = new URL(value);
|
|
4725
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
|
|
4726
|
+
} catch {
|
|
4727
|
+
return null;
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
function parseRequiredString(value) {
|
|
4731
|
+
if (typeof value !== "string") return null;
|
|
4732
|
+
const trimmed = value.trim();
|
|
4733
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
4734
|
+
}
|
|
4735
|
+
function parseOptionalString(value) {
|
|
4736
|
+
if (value === null || value === undefined) return null;
|
|
4737
|
+
if (typeof value !== "string") return undefined;
|
|
4738
|
+
const trimmed = value.trim();
|
|
4739
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
4740
|
+
}
|
|
4741
|
+
function isRequirement(value) {
|
|
4742
|
+
return value === "required" || value === "optional";
|
|
4743
|
+
}
|
|
4744
|
+
function isRecord$2(value) {
|
|
4745
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4746
|
+
}
|
|
4747
|
+
function formError(field) {
|
|
4748
|
+
return err({
|
|
4749
|
+
tag: "invalid_mailing_list_global_form",
|
|
4750
|
+
field
|
|
4751
|
+
});
|
|
4752
|
+
}
|
|
4753
|
+
function setupError(field) {
|
|
4754
|
+
return err({
|
|
4755
|
+
tag: "invalid_mailing_list_setup",
|
|
4756
|
+
field
|
|
4757
|
+
});
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
//#endregion
|
|
4761
|
+
//#region ../core/src/mailing-list-targeting.ts
|
|
4762
|
+
function sameMailingListPlacementTargeting(left, right) {
|
|
4763
|
+
if (left.mode !== right.mode) return false;
|
|
4764
|
+
if (left.mode === "default" || right.mode === "default") return true;
|
|
4765
|
+
if (left.provider !== right.provider || left.selection.tag !== right.selection.tag) return false;
|
|
4766
|
+
if (left.provider === "mailchimp" && (right.provider !== "mailchimp" || left.audienceId !== right.audienceId)) return false;
|
|
4767
|
+
return sameIds(targetingSelectionIds(left), targetingSelectionIds(right));
|
|
4768
|
+
}
|
|
4769
|
+
function targetingSelectionIds(targeting) {
|
|
4770
|
+
switch (targeting.provider) {
|
|
4771
|
+
case "native": return targeting.selection.tag === "fixed" ? targeting.selection.topicIds : targeting.selection.offeredTopicIds;
|
|
4772
|
+
case "brevo": return targeting.selection.tag === "fixed" ? targeting.selection.listIds : targeting.selection.offeredListIds;
|
|
4773
|
+
case "mailchimp": return targeting.selection.tag === "fixed" ? targeting.selection.interestIds : targeting.selection.offeredInterestIds;
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
function sameIds(left, right) {
|
|
4777
|
+
const leftIds = new Set(left);
|
|
4778
|
+
const rightIds = new Set(right);
|
|
4779
|
+
return leftIds.size === left.length && rightIds.size === right.length && leftIds.size === rightIds.size && [...leftIds].every((id) => rightIds.has(id));
|
|
4780
|
+
}
|
|
4781
|
+
function resolveMailingListTarget(input) {
|
|
4782
|
+
if (input.targeting.mode === "default") {
|
|
4783
|
+
if (input.visitorSelection !== undefined) {
|
|
4784
|
+
return err({ tag: "visitor_selection_not_allowed" });
|
|
4785
|
+
}
|
|
4786
|
+
return ok(resolveDefaultTarget(input.setup));
|
|
4787
|
+
}
|
|
4788
|
+
if (input.targeting.provider !== input.setup.provider) {
|
|
4789
|
+
return err({
|
|
4790
|
+
tag: "targeting_provider_mismatch",
|
|
4791
|
+
setupProvider: input.setup.provider,
|
|
4792
|
+
targetingProvider: input.targeting.provider
|
|
4793
|
+
});
|
|
4794
|
+
}
|
|
4795
|
+
return resolveCustomTarget(input.targeting, input.visitorSelection);
|
|
4796
|
+
}
|
|
4797
|
+
function resolveDefaultTarget(setup) {
|
|
4798
|
+
switch (setup.provider) {
|
|
4799
|
+
case "native": return {
|
|
4800
|
+
provider: "native",
|
|
4801
|
+
topicIds: [setup.defaultTopicId]
|
|
4802
|
+
};
|
|
4803
|
+
case "brevo": return {
|
|
4804
|
+
provider: "brevo",
|
|
4805
|
+
listIds: [setup.defaultListId]
|
|
4806
|
+
};
|
|
4807
|
+
case "mailchimp": return {
|
|
4808
|
+
provider: "mailchimp",
|
|
4809
|
+
audienceId: setup.defaultAudienceId,
|
|
4810
|
+
interestIds: []
|
|
4811
|
+
};
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
function resolveCustomTarget(targeting, visitorSelection) {
|
|
4815
|
+
switch (targeting.provider) {
|
|
4816
|
+
case "native": return resolveNativeTarget(targeting, visitorSelection);
|
|
4817
|
+
case "brevo": return resolveBrevoTarget(targeting, visitorSelection);
|
|
4818
|
+
case "mailchimp": return resolveMailchimpTarget(targeting, visitorSelection);
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
function resolveNativeTarget(targeting, selection) {
|
|
4822
|
+
if (targeting.selection.tag === "fixed") {
|
|
4823
|
+
return selection === undefined ? ok({
|
|
4824
|
+
provider: "native",
|
|
4825
|
+
topicIds: targeting.selection.topicIds
|
|
4826
|
+
}) : err({ tag: "visitor_selection_not_allowed" });
|
|
4827
|
+
}
|
|
4828
|
+
if (selection === undefined) {
|
|
4829
|
+
return targeting.selection.offeredTopicIds.length === 1 ? ok({
|
|
4830
|
+
provider: "native",
|
|
4831
|
+
topicIds: targeting.selection.offeredTopicIds
|
|
4832
|
+
}) : err({ tag: "visitor_selection_required" });
|
|
4833
|
+
}
|
|
4834
|
+
if (selection.provider !== "native") {
|
|
4835
|
+
return err({ tag: "visitor_selection_provider_mismatch" });
|
|
4836
|
+
}
|
|
4837
|
+
const selected = selectedNonEmptySubset(selection.topicIds, targeting.selection.offeredTopicIds);
|
|
4838
|
+
return selected.ok ? ok({
|
|
4839
|
+
provider: "native",
|
|
4840
|
+
topicIds: selected.value
|
|
4841
|
+
}) : err({ tag: "visitor_selection_outside_offer" });
|
|
4842
|
+
}
|
|
4843
|
+
function resolveBrevoTarget(targeting, selection) {
|
|
4844
|
+
if (targeting.selection.tag === "fixed") {
|
|
4845
|
+
return selection === undefined ? ok({
|
|
4846
|
+
provider: "brevo",
|
|
4847
|
+
listIds: targeting.selection.listIds
|
|
4848
|
+
}) : err({ tag: "visitor_selection_not_allowed" });
|
|
4849
|
+
}
|
|
4850
|
+
if (selection === undefined) {
|
|
4851
|
+
return targeting.selection.offeredListIds.length === 1 ? ok({
|
|
4852
|
+
provider: "brevo",
|
|
4853
|
+
listIds: targeting.selection.offeredListIds
|
|
4854
|
+
}) : err({ tag: "visitor_selection_required" });
|
|
4855
|
+
}
|
|
4856
|
+
if (selection.provider !== "brevo") {
|
|
4857
|
+
return err({ tag: "visitor_selection_provider_mismatch" });
|
|
4858
|
+
}
|
|
4859
|
+
const selected = selectedNonEmptySubset(selection.listIds, targeting.selection.offeredListIds);
|
|
4860
|
+
return selected.ok ? ok({
|
|
4861
|
+
provider: "brevo",
|
|
4862
|
+
listIds: selected.value
|
|
4863
|
+
}) : err({ tag: "visitor_selection_outside_offer" });
|
|
4864
|
+
}
|
|
4865
|
+
function resolveMailchimpTarget(targeting, selection) {
|
|
4866
|
+
if (targeting.selection.tag === "fixed") {
|
|
4867
|
+
return selection === undefined ? ok({
|
|
4868
|
+
provider: "mailchimp",
|
|
4869
|
+
audienceId: targeting.audienceId,
|
|
4870
|
+
interestIds: targeting.selection.interestIds
|
|
4871
|
+
}) : err({ tag: "visitor_selection_not_allowed" });
|
|
4872
|
+
}
|
|
4873
|
+
if (selection === undefined) {
|
|
4874
|
+
return targeting.selection.offeredInterestIds.length === 1 ? ok({
|
|
4875
|
+
provider: "mailchimp",
|
|
4876
|
+
audienceId: targeting.audienceId,
|
|
4877
|
+
interestIds: targeting.selection.offeredInterestIds
|
|
4878
|
+
}) : err({ tag: "visitor_selection_required" });
|
|
4879
|
+
}
|
|
4880
|
+
if (selection.provider !== "mailchimp") {
|
|
4881
|
+
return err({ tag: "visitor_selection_provider_mismatch" });
|
|
4882
|
+
}
|
|
4883
|
+
const subset = selectedNonEmptySubset(selection.interestIds, targeting.selection.offeredInterestIds);
|
|
4884
|
+
return subset.ok ? ok({
|
|
4885
|
+
provider: "mailchimp",
|
|
4886
|
+
audienceId: targeting.audienceId,
|
|
4887
|
+
interestIds: subset.value
|
|
4888
|
+
}) : err({ tag: "visitor_selection_outside_offer" });
|
|
4889
|
+
}
|
|
4890
|
+
function selectedNonEmptySubset(selected, offered) {
|
|
4891
|
+
const normalized = [...new Set(selected)];
|
|
4892
|
+
const [first, ...rest] = normalized;
|
|
4893
|
+
if (first === undefined || normalized.some((item) => !offered.includes(item))) {
|
|
4894
|
+
return err("invalid_selection");
|
|
4895
|
+
}
|
|
4896
|
+
return ok([first, ...rest]);
|
|
4897
|
+
}
|
|
4898
|
+
function planMailingListChoicePresentation(targeting) {
|
|
4899
|
+
if (targeting.mode === "default" || targeting.selection.tag === "fixed") {
|
|
4900
|
+
return { tag: "hidden" };
|
|
4901
|
+
}
|
|
4902
|
+
switch (targeting.provider) {
|
|
4903
|
+
case "native": return targeting.selection.offeredTopicIds.length === 1 ? {
|
|
4904
|
+
tag: "automatic",
|
|
4905
|
+
selection: {
|
|
4906
|
+
provider: "native",
|
|
4907
|
+
topicIds: targeting.selection.offeredTopicIds
|
|
4908
|
+
}
|
|
4909
|
+
} : {
|
|
4910
|
+
tag: "visitor_choice",
|
|
4911
|
+
provider: "native",
|
|
4912
|
+
optionCount: targeting.selection.offeredTopicIds.length
|
|
4913
|
+
};
|
|
4914
|
+
case "brevo": return targeting.selection.offeredListIds.length === 1 ? {
|
|
4915
|
+
tag: "automatic",
|
|
4916
|
+
selection: {
|
|
4917
|
+
provider: "brevo",
|
|
4918
|
+
listIds: targeting.selection.offeredListIds
|
|
4919
|
+
}
|
|
4920
|
+
} : {
|
|
4921
|
+
tag: "visitor_choice",
|
|
4922
|
+
provider: "brevo",
|
|
4923
|
+
optionCount: targeting.selection.offeredListIds.length
|
|
4924
|
+
};
|
|
4925
|
+
case "mailchimp": return targeting.selection.offeredInterestIds.length === 1 ? {
|
|
4926
|
+
tag: "automatic",
|
|
4927
|
+
selection: {
|
|
4928
|
+
provider: "mailchimp",
|
|
4929
|
+
interestIds: targeting.selection.offeredInterestIds
|
|
4930
|
+
}
|
|
4931
|
+
} : {
|
|
4932
|
+
tag: "visitor_choice",
|
|
4933
|
+
provider: "mailchimp",
|
|
4934
|
+
optionCount: targeting.selection.offeredInterestIds.length
|
|
4935
|
+
};
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4939
|
+
//#endregion
|
|
4940
|
+
//#region ../core/src/mailing-list-runtime.ts
|
|
4941
|
+
function parseMailingListRuntimeContext(value) {
|
|
4942
|
+
if (!isRecord$1(value)) return invalidRuntimeContext();
|
|
4943
|
+
if (value.tag === "not_configured") return ok({ tag: "not_configured" });
|
|
4944
|
+
if (value.tag !== "configured") return invalidRuntimeContext();
|
|
4945
|
+
const revision = parseMailingListSetupRevision(value.revision);
|
|
4946
|
+
const form = parseMailingListGlobalForm(value.form);
|
|
4947
|
+
const provider = parseRuntimeProvider(value.provider);
|
|
4948
|
+
if (revision === null || !form.ok || provider === null) {
|
|
4949
|
+
return invalidRuntimeContext();
|
|
3734
4950
|
}
|
|
4951
|
+
return ok({
|
|
4952
|
+
tag: "configured",
|
|
4953
|
+
revision,
|
|
4954
|
+
form: form.value,
|
|
4955
|
+
provider
|
|
4956
|
+
});
|
|
3735
4957
|
}
|
|
3736
|
-
function
|
|
4958
|
+
function parseRuntimeProvider(value) {
|
|
4959
|
+
if (!isRecord$1(value)) return null;
|
|
4960
|
+
switch (value.provider) {
|
|
4961
|
+
case "native": return parseNativeRuntimeProvider(value);
|
|
4962
|
+
case "brevo": return parseBrevoRuntimeProvider(value);
|
|
4963
|
+
case "mailchimp": return parseMailchimpRuntimeProvider(value);
|
|
4964
|
+
default: return null;
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
function parseNativeRuntimeProvider(value) {
|
|
4968
|
+
const defaultTopicId = parseNewsletterListId(value.defaultTopicId);
|
|
4969
|
+
const topics = parseNamedItems(value.topics, parseNewsletterListId);
|
|
4970
|
+
return defaultTopicId !== null && topics !== null ? {
|
|
4971
|
+
provider: "native",
|
|
4972
|
+
defaultTopicId,
|
|
4973
|
+
topics
|
|
4974
|
+
} : null;
|
|
4975
|
+
}
|
|
4976
|
+
function parseBrevoRuntimeProvider(value) {
|
|
4977
|
+
const defaultListId = parseBrevoListId(value.defaultListId);
|
|
4978
|
+
const lists = parseNamedItems(value.lists, parseBrevoListId);
|
|
4979
|
+
return defaultListId !== null && lists !== null ? {
|
|
4980
|
+
provider: "brevo",
|
|
4981
|
+
defaultListId,
|
|
4982
|
+
lists
|
|
4983
|
+
} : null;
|
|
4984
|
+
}
|
|
4985
|
+
function parseMailchimpRuntimeProvider(value) {
|
|
4986
|
+
if (!Array.isArray(value.audiences)) return null;
|
|
4987
|
+
const defaultAudienceId = parseMailchimpAudienceId(value.defaultAudienceId);
|
|
4988
|
+
if (defaultAudienceId === null) return null;
|
|
4989
|
+
const audiences = [];
|
|
4990
|
+
for (const audience of value.audiences) {
|
|
4991
|
+
if (!isRecord$1(audience)) return null;
|
|
4992
|
+
const id = parseMailchimpAudienceId(audience.id);
|
|
4993
|
+
const name = parseName(audience.name);
|
|
4994
|
+
if (id === null || name === null || !Array.isArray(audience.interests)) {
|
|
4995
|
+
return null;
|
|
4996
|
+
}
|
|
4997
|
+
const interests = parseMailchimpInterests(audience.interests);
|
|
4998
|
+
if (interests === null) return null;
|
|
4999
|
+
audiences.push({
|
|
5000
|
+
id,
|
|
5001
|
+
name,
|
|
5002
|
+
interests
|
|
5003
|
+
});
|
|
5004
|
+
}
|
|
3737
5005
|
return {
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
})),
|
|
3742
|
-
available: composition.sections.filter((section) => !section.selected).map((section) => ({
|
|
3743
|
-
section,
|
|
3744
|
-
status: "available"
|
|
3745
|
-
}))
|
|
5006
|
+
provider: "mailchimp",
|
|
5007
|
+
defaultAudienceId,
|
|
5008
|
+
audiences
|
|
3746
5009
|
};
|
|
3747
5010
|
}
|
|
3748
|
-
|
|
3749
|
-
const
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
id: unsafeAsFooterModuleId("navigation"),
|
|
3761
|
-
kind: "navigation",
|
|
3762
|
-
heading: null,
|
|
3763
|
-
selected: footerVisible,
|
|
3764
|
-
source: {
|
|
3765
|
-
kind: "menu-role",
|
|
3766
|
-
role: "main"
|
|
3767
|
-
}
|
|
3768
|
-
},
|
|
3769
|
-
{
|
|
3770
|
-
id: unsafeAsFooterModuleId("legal-navigation"),
|
|
3771
|
-
kind: "legal-navigation",
|
|
3772
|
-
heading: null,
|
|
3773
|
-
selected: false,
|
|
3774
|
-
source: {
|
|
3775
|
-
kind: "menu-role",
|
|
3776
|
-
role: "legal-policy"
|
|
3777
|
-
}
|
|
3778
|
-
},
|
|
3779
|
-
{
|
|
3780
|
-
id: unsafeAsFooterModuleId("business-address"),
|
|
3781
|
-
kind: "business-address",
|
|
3782
|
-
heading: null,
|
|
3783
|
-
selected: footerVisible && legacy.showBusinessAddress === true,
|
|
3784
|
-
source: { kind: "business-address" }
|
|
3785
|
-
},
|
|
3786
|
-
{
|
|
3787
|
-
id: unsafeAsFooterModuleId("contact"),
|
|
3788
|
-
kind: "contact",
|
|
3789
|
-
heading: null,
|
|
3790
|
-
selected: false,
|
|
3791
|
-
source: { kind: "public-profile" }
|
|
3792
|
-
},
|
|
3793
|
-
{
|
|
3794
|
-
id: unsafeAsFooterModuleId("opening-hours"),
|
|
3795
|
-
kind: "opening-hours",
|
|
3796
|
-
heading: null,
|
|
3797
|
-
selected: false,
|
|
3798
|
-
source: { kind: "public-profile" }
|
|
3799
|
-
},
|
|
3800
|
-
{
|
|
3801
|
-
id: unsafeAsFooterModuleId("social-links"),
|
|
3802
|
-
kind: "social-links",
|
|
3803
|
-
heading: null,
|
|
3804
|
-
selected: false,
|
|
3805
|
-
source: { kind: "public-profile" }
|
|
3806
|
-
},
|
|
3807
|
-
{
|
|
3808
|
-
id: unsafeAsFooterModuleId("supporting-text"),
|
|
3809
|
-
kind: "supporting-text",
|
|
3810
|
-
heading: null,
|
|
3811
|
-
selected: footerVisible && isRichText(legacy.bodyText),
|
|
3812
|
-
content: isRichText(legacy.bodyText) ? legacy.bodyText : null
|
|
3813
|
-
},
|
|
3814
|
-
{
|
|
3815
|
-
id: unsafeAsFooterModuleId("legal"),
|
|
3816
|
-
kind: "legal",
|
|
3817
|
-
heading: null,
|
|
3818
|
-
selected: footerVisible && isRichText(legacy.bottomText),
|
|
3819
|
-
content: isRichText(legacy.bottomText) ? legacy.bottomText : null
|
|
3820
|
-
}
|
|
3821
|
-
];
|
|
3822
|
-
};
|
|
3823
|
-
function normalizeLegacyFooterContentToV1(value, options = {}) {
|
|
3824
|
-
if (!isRecord$1(value)) {
|
|
3825
|
-
return err({
|
|
3826
|
-
code: "invalid_shape",
|
|
3827
|
-
path: "",
|
|
3828
|
-
message: "Legacy footer content must be an object."
|
|
5011
|
+
function parseMailchimpInterests(value) {
|
|
5012
|
+
const interests = [];
|
|
5013
|
+
for (const interest of value) {
|
|
5014
|
+
if (!isRecord$1(interest)) return null;
|
|
5015
|
+
const id = parseMailchimpInterestId(interest.id);
|
|
5016
|
+
const name = parseName(interest.name);
|
|
5017
|
+
const categoryName = parseName(interest.categoryName);
|
|
5018
|
+
if (id === null || name === null || categoryName === null) return null;
|
|
5019
|
+
interests.push({
|
|
5020
|
+
id,
|
|
5021
|
+
name,
|
|
5022
|
+
categoryName
|
|
3829
5023
|
});
|
|
3830
5024
|
}
|
|
3831
|
-
return
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
5025
|
+
return interests;
|
|
5026
|
+
}
|
|
5027
|
+
function parseNamedItems(value, parseId) {
|
|
5028
|
+
if (!Array.isArray(value)) return null;
|
|
5029
|
+
const items = [];
|
|
5030
|
+
for (const item of value) {
|
|
5031
|
+
if (!isRecord$1(item)) return null;
|
|
5032
|
+
const id = parseId(item.id);
|
|
5033
|
+
const name = parseName(item.name);
|
|
5034
|
+
if (id === null || name === null) return null;
|
|
5035
|
+
items.push({
|
|
5036
|
+
id,
|
|
5037
|
+
name
|
|
5038
|
+
});
|
|
5039
|
+
}
|
|
5040
|
+
return items;
|
|
5041
|
+
}
|
|
5042
|
+
function parseName(value) {
|
|
5043
|
+
if (typeof value !== "string") return null;
|
|
5044
|
+
const name = value.trim();
|
|
5045
|
+
return name.length > 0 ? name : null;
|
|
5046
|
+
}
|
|
5047
|
+
function isRecord$1(value) {
|
|
5048
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5049
|
+
}
|
|
5050
|
+
function invalidRuntimeContext() {
|
|
5051
|
+
return err({ tag: "invalid_mailing_list_runtime_context" });
|
|
3835
5052
|
}
|
|
3836
5053
|
|
|
3837
5054
|
//#endregion
|
|
3838
|
-
//#region ../core/src/
|
|
3839
|
-
const
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
return
|
|
5055
|
+
//#region ../core/src/mailing-list-submission.ts
|
|
5056
|
+
const MAILING_LIST_SUBMISSION_BUDGET_MS = 1e4;
|
|
5057
|
+
function parseMailingListBlockId(value) {
|
|
5058
|
+
const parsed = typeof value === "string" ? value.trim() : "";
|
|
5059
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(parsed) ? parsed : null;
|
|
5060
|
+
}
|
|
5061
|
+
function parseMailingListSignedSourceToken(value) {
|
|
5062
|
+
const parsed = typeof value === "string" ? value.trim() : "";
|
|
5063
|
+
return /^[A-Za-z0-9._~-]{16,1000}$/u.test(parsed) ? parsed : null;
|
|
5064
|
+
}
|
|
5065
|
+
function parseMailingListSdkFooterBlockId(value) {
|
|
5066
|
+
const parsed = typeof value === "string" ? value.trim() : "";
|
|
5067
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(parsed) ? parsed : null;
|
|
5068
|
+
}
|
|
5069
|
+
function parseMailingListSdkContentHash(value) {
|
|
5070
|
+
const parsed = typeof value === "string" ? value.trim() : "";
|
|
5071
|
+
return /^[a-f0-9]{16}$/iu.test(parsed) ? parsed : null;
|
|
5072
|
+
}
|
|
5073
|
+
function parseMailingListContentVersionId(value) {
|
|
5074
|
+
const parsed = parseContentVersionId(value);
|
|
5075
|
+
return parsed.ok ? parsed.value : null;
|
|
5076
|
+
}
|
|
5077
|
+
function parseMailingListFooterRevision(value) {
|
|
5078
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
5079
|
+
}
|
|
5080
|
+
function buildMailingListSignupBlockSourceRef(blockId) {
|
|
5081
|
+
const parsed = parseMailingListBlockId(blockId);
|
|
5082
|
+
return parsed !== null ? {
|
|
5083
|
+
tag: "block_ref",
|
|
5084
|
+
blockId: parsed
|
|
5085
|
+
} : null;
|
|
5086
|
+
}
|
|
5087
|
+
function parseMailingListSignupProfile(form, input) {
|
|
5088
|
+
const email = normalizeSubscriberEmail(input.email);
|
|
5089
|
+
if (email === null) return {
|
|
5090
|
+
ok: false,
|
|
5091
|
+
error: { tag: "invalid_email" }
|
|
5092
|
+
};
|
|
5093
|
+
const phone = form.includeOptionalPhone ? {
|
|
5094
|
+
tag: "collected",
|
|
5095
|
+
value: normalizeOptionalValue(input.phone)
|
|
5096
|
+
} : { tag: "not_collected" };
|
|
5097
|
+
switch (form.fields.preset) {
|
|
5098
|
+
case "emailOnly": return {
|
|
5099
|
+
ok: true,
|
|
5100
|
+
value: {
|
|
5101
|
+
preset: "emailOnly",
|
|
5102
|
+
email,
|
|
5103
|
+
phone
|
|
5104
|
+
}
|
|
5105
|
+
};
|
|
5106
|
+
case "emailName": {
|
|
5107
|
+
const name = resolveProfileField("name", form.fields.name, input.name);
|
|
5108
|
+
return name.ok ? {
|
|
5109
|
+
ok: true,
|
|
5110
|
+
value: {
|
|
5111
|
+
preset: "emailName",
|
|
5112
|
+
email,
|
|
5113
|
+
phone,
|
|
5114
|
+
name: name.value
|
|
5115
|
+
}
|
|
5116
|
+
} : name;
|
|
5117
|
+
}
|
|
5118
|
+
case "emailFirstLast": {
|
|
5119
|
+
const firstName = resolveProfileField("first_name", form.fields.firstName, input.firstName);
|
|
5120
|
+
if (!firstName.ok) return firstName;
|
|
5121
|
+
const lastName = resolveProfileField("last_name", form.fields.lastName, input.lastName);
|
|
5122
|
+
return lastName.ok ? {
|
|
5123
|
+
ok: true,
|
|
5124
|
+
value: {
|
|
5125
|
+
preset: "emailFirstLast",
|
|
5126
|
+
email,
|
|
5127
|
+
phone,
|
|
5128
|
+
firstName: firstName.value,
|
|
5129
|
+
lastName: lastName.value
|
|
5130
|
+
}
|
|
5131
|
+
} : lastName;
|
|
5132
|
+
}
|
|
5133
|
+
case "emailNameCompany": {
|
|
5134
|
+
const name = resolveProfileField("name", form.fields.name, input.name);
|
|
5135
|
+
if (!name.ok) return name;
|
|
5136
|
+
const company = resolveProfileField("company", form.fields.company, input.company);
|
|
5137
|
+
return company.ok ? {
|
|
5138
|
+
ok: true,
|
|
5139
|
+
value: {
|
|
5140
|
+
preset: "emailNameCompany",
|
|
5141
|
+
email,
|
|
5142
|
+
phone,
|
|
5143
|
+
name: name.value,
|
|
5144
|
+
company: company.value
|
|
5145
|
+
}
|
|
5146
|
+
} : company;
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
function resolveProfileField(field, requirement, input) {
|
|
5151
|
+
const value = normalizeOptionalValue(input);
|
|
5152
|
+
if (requirement === "required") {
|
|
5153
|
+
return value === null ? {
|
|
5154
|
+
ok: false,
|
|
5155
|
+
error: {
|
|
5156
|
+
tag: "required_field_missing",
|
|
5157
|
+
field
|
|
5158
|
+
}
|
|
5159
|
+
} : {
|
|
5160
|
+
ok: true,
|
|
5161
|
+
value: {
|
|
5162
|
+
requirement: "required",
|
|
5163
|
+
value
|
|
5164
|
+
}
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5167
|
+
return {
|
|
5168
|
+
ok: true,
|
|
5169
|
+
value: {
|
|
5170
|
+
requirement: "optional",
|
|
5171
|
+
value
|
|
5172
|
+
}
|
|
5173
|
+
};
|
|
5174
|
+
}
|
|
5175
|
+
function normalizeSubscriberEmail(value) {
|
|
5176
|
+
const normalized = value.trim().toLowerCase();
|
|
5177
|
+
const at = normalized.indexOf("@");
|
|
5178
|
+
return normalized.length <= 320 && at > 0 && at === normalized.lastIndexOf("@") && at < normalized.length - 1 ? normalized : null;
|
|
5179
|
+
}
|
|
5180
|
+
function normalizeOptionalValue(value) {
|
|
5181
|
+
const normalized = value?.trim() ?? "";
|
|
5182
|
+
return normalized.length > 0 ? normalized : null;
|
|
5183
|
+
}
|
|
5184
|
+
function toMailingListPublicSignupResponse(result) {
|
|
5185
|
+
if (result.ok) {
|
|
5186
|
+
return result.acknowledgement.tag === "subscribed" ? { tag: "subscribed" } : { tag: "confirmation_required" };
|
|
5187
|
+
}
|
|
5188
|
+
if (result.error.tag === "profile_rejected") {
|
|
5189
|
+
return {
|
|
5190
|
+
tag: "invalid_input",
|
|
5191
|
+
reason: "profile_rejected"
|
|
5192
|
+
};
|
|
5193
|
+
}
|
|
5194
|
+
return result.error.tag === "uncertain" ? {
|
|
5195
|
+
tag: "uncertain",
|
|
5196
|
+
reason: result.error.reason
|
|
5197
|
+
} : {
|
|
5198
|
+
tag: "unavailable",
|
|
5199
|
+
reason: result.error.tag
|
|
5200
|
+
};
|
|
3843
5201
|
}
|
|
3844
5202
|
|
|
3845
5203
|
//#endregion
|
|
@@ -4653,61 +6011,161 @@ function foldSiteRouteTarget(target, cases) {
|
|
|
4653
6011
|
}
|
|
4654
6012
|
|
|
4655
6013
|
//#endregion
|
|
4656
|
-
//#region ../core/src/
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
6014
|
+
//#region ../core/src/whole-series-run-schedule.ts
|
|
6015
|
+
/**
|
|
6016
|
+
* A session that is not happening.
|
|
6017
|
+
*
|
|
6018
|
+
* Exported because every surface that renders a run's schedule has to mark the
|
|
6019
|
+
* same weeks, and a second copy of the string is a second chance to disagree.
|
|
6020
|
+
*/
|
|
6021
|
+
const CANCELLED_SESSION_STATUS = "cancelled";
|
|
6022
|
+
/** A run the organiser called off. Spelled the same, about a different row. */
|
|
6023
|
+
const CANCELLED_SERIES_STATUS = "cancelled";
|
|
6024
|
+
function sessionStartMs(session) {
|
|
6025
|
+
return Date.parse(session.startsAt);
|
|
4662
6026
|
}
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
6027
|
+
/**
|
|
6028
|
+
* The run's sessions that still count, in schedule order.
|
|
6029
|
+
*
|
|
6030
|
+
* Sessions whose start cannot be read are dropped rather than sorted to an
|
|
6031
|
+
* arbitrary end: an unparseable date is not a date a visitor can attend.
|
|
6032
|
+
*/
|
|
6033
|
+
function orderedLiveRunSessions(sessions) {
|
|
6034
|
+
return sessions.filter((session) => session.status !== CANCELLED_SESSION_STATUS && Number.isFinite(sessionStartMs(session))).sort((left, right) => sessionStartMs(left) - sessionStartMs(right));
|
|
4668
6035
|
}
|
|
4669
|
-
/**
|
|
4670
|
-
function
|
|
4671
|
-
|
|
6036
|
+
/** When the first session a visitor could attend begins. */
|
|
6037
|
+
function firstLiveRunSessionStartMs(sessions) {
|
|
6038
|
+
const first = orderedLiveRunSessions(sessions)[0];
|
|
6039
|
+
return first === undefined ? null : sessionStartMs(first);
|
|
4672
6040
|
}
|
|
4673
|
-
/**
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
6041
|
+
/**
|
|
6042
|
+
* Booking stays open until the run's first surviving session has started.
|
|
6043
|
+
*
|
|
6044
|
+
* There has to be something left to sell: a run with no schedule at all, or one
|
|
6045
|
+
* whose every session has been cancelled, is closed rather than open. Adding a
|
|
6046
|
+
* future session opens it again, subject to the usual status, visibility and
|
|
6047
|
+
* admission rules.
|
|
6048
|
+
*/
|
|
6049
|
+
function isWholeSeriesRegistrationOpen(sessions, now) {
|
|
6050
|
+
const firstStart = firstLiveRunSessionStartMs(sessions);
|
|
6051
|
+
return firstStart !== null && firstStart > now.getTime();
|
|
4681
6052
|
}
|
|
4682
|
-
/**
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
6053
|
+
/**
|
|
6054
|
+
* Where the run sits in its own life, independent of any one session.
|
|
6055
|
+
*
|
|
6056
|
+
* A cancelled or completed series status says so whatever the dates read.
|
|
6057
|
+
* Otherwise the surviving schedule decides: before the first session it is
|
|
6058
|
+
* upcoming, after the last session ends it is complete, and in between it is
|
|
6059
|
+
* under way. A run with nothing left to attend is `upcoming` and never
|
|
6060
|
+
* `in_progress` — an empty schedule has not started.
|
|
6061
|
+
*/
|
|
6062
|
+
function resolveWholeSeriesRunLifecycle(input, now) {
|
|
6063
|
+
if (input.seriesStatus === CANCELLED_SERIES_STATUS) return "cancelled";
|
|
6064
|
+
if (input.seriesStatus === "completed") return "completed";
|
|
6065
|
+
const live = orderedLiveRunSessions(input.sessions);
|
|
6066
|
+
const first = live[0];
|
|
6067
|
+
const last = live[live.length - 1];
|
|
6068
|
+
if (first === undefined || last === undefined) return "upcoming";
|
|
6069
|
+
if (sessionStartMs(first) > now.getTime()) return "upcoming";
|
|
6070
|
+
return Date.parse(last.endsAt) < now.getTime() ? "completed" : "in_progress";
|
|
4688
6071
|
}
|
|
4689
6072
|
/**
|
|
4690
|
-
*
|
|
6073
|
+
* Whether a prepared run is still inviting bookings at render time.
|
|
4691
6074
|
*
|
|
4692
|
-
*
|
|
4693
|
-
*
|
|
6075
|
+
* `lifecycle` and `enrollmentOpen` come from the projection; the first session
|
|
6076
|
+
* start is the one fact re-read against `now`, because that is the boundary a
|
|
6077
|
+
* cached page can cross while it sits in front of a visitor.
|
|
4694
6078
|
*/
|
|
4695
|
-
function
|
|
4696
|
-
|
|
4697
|
-
return
|
|
6079
|
+
function resolveWholeSeriesBookingPhase(run, now) {
|
|
6080
|
+
switch (run.lifecycle) {
|
|
6081
|
+
case "cancelled": return {
|
|
6082
|
+
kind: "closed",
|
|
6083
|
+
reason: "cancelled"
|
|
6084
|
+
};
|
|
6085
|
+
case "in_progress": return {
|
|
6086
|
+
kind: "closed",
|
|
6087
|
+
reason: "in-progress"
|
|
6088
|
+
};
|
|
6089
|
+
case "completed": return {
|
|
6090
|
+
kind: "closed",
|
|
6091
|
+
reason: "completed"
|
|
6092
|
+
};
|
|
6093
|
+
case "upcoming": {
|
|
6094
|
+
if (firstLiveRunSessionStartMs(run.sessions) === null) {
|
|
6095
|
+
return {
|
|
6096
|
+
kind: "closed",
|
|
6097
|
+
reason: "no-sessions"
|
|
6098
|
+
};
|
|
6099
|
+
}
|
|
6100
|
+
if (!isWholeSeriesRegistrationOpen(run.sessions, now)) {
|
|
6101
|
+
return {
|
|
6102
|
+
kind: "closed",
|
|
6103
|
+
reason: "in-progress"
|
|
6104
|
+
};
|
|
6105
|
+
}
|
|
6106
|
+
return run.enrollmentOpen ? { kind: "open" } : {
|
|
6107
|
+
kind: "closed",
|
|
6108
|
+
reason: "enrolment-closed"
|
|
6109
|
+
};
|
|
6110
|
+
}
|
|
4698
6111
|
}
|
|
4699
|
-
return resolvePlanningNowDate(input);
|
|
4700
6112
|
}
|
|
4701
|
-
|
|
4702
|
-
|
|
6113
|
+
|
|
6114
|
+
//#endregion
|
|
6115
|
+
//#region ../core/src/public-read-cache-contract.ts
|
|
6116
|
+
const PUBLIC_AVAILABILITY_FRESHNESS_SECONDS = 60;
|
|
6117
|
+
const PUBLIC_CONTENT_FRESHNESS_SECONDS = 3600;
|
|
6118
|
+
function availability(transport) {
|
|
4703
6119
|
return {
|
|
4704
|
-
|
|
4705
|
-
|
|
6120
|
+
kind: "live",
|
|
6121
|
+
reason: "booking_correctness",
|
|
6122
|
+
freshness: "availability",
|
|
6123
|
+
transport
|
|
4706
6124
|
};
|
|
4707
6125
|
}
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
6126
|
+
function content(tags, transport = "direct") {
|
|
6127
|
+
return {
|
|
6128
|
+
kind: "tagged",
|
|
6129
|
+
tags,
|
|
6130
|
+
freshness: "content",
|
|
6131
|
+
transport
|
|
6132
|
+
};
|
|
6133
|
+
}
|
|
6134
|
+
/** The single classification authority for cacheable public reads. */
|
|
6135
|
+
const PUBLIC_READ_CACHE_POLICIES = {
|
|
6136
|
+
listPublishedEntries: content(["published_content"]),
|
|
6137
|
+
getPublishedEntryPreview: content(["published_content"]),
|
|
6138
|
+
listPublicEvents: availability("direct"),
|
|
6139
|
+
listPublicEventCalendarWindow: availability("direct"),
|
|
6140
|
+
listPublicRecurringSchedule: availability("batch_only"),
|
|
6141
|
+
getPublicEventDetails: availability("batch_only"),
|
|
6142
|
+
resolveEventSubroute: availability("direct"),
|
|
6143
|
+
listPublicRouteCandidates: content(["published_content"], "batch_only"),
|
|
6144
|
+
listPublicTeamMembers: content(["public_team_members"]),
|
|
6145
|
+
listPublicProducts: availability("direct"),
|
|
6146
|
+
listPublicPasses: availability("direct"),
|
|
6147
|
+
listPublicMemberships: availability("direct"),
|
|
6148
|
+
getPublicFormById: content(["site_forms"]),
|
|
6149
|
+
getPublicBookingServices: availability("direct"),
|
|
6150
|
+
getPublicBookingService: availability("direct"),
|
|
6151
|
+
getPublicBookingResourcesReference: availability("direct"),
|
|
6152
|
+
getPublicBookingAvailableDates: availability("direct"),
|
|
6153
|
+
getPublicBookingAvailableSlots: availability("direct"),
|
|
6154
|
+
getPublicWeeklySlotAvailability: availability("direct"),
|
|
6155
|
+
listPublicForms: content(["site_forms"]),
|
|
6156
|
+
checkRedirect: content([]),
|
|
6157
|
+
getPublicRoutableContent: content(["published_content"]),
|
|
6158
|
+
getPublicSitemapRoutes: content(["published_content"])
|
|
6159
|
+
};
|
|
6160
|
+
const PUBLIC_READ_ENDPOINT_SET = new Set(Object.keys(PUBLIC_READ_CACHE_POLICIES));
|
|
6161
|
+
function isPublicReadEndpoint(endpoint) {
|
|
6162
|
+
return PUBLIC_READ_ENDPOINT_SET.has(endpoint);
|
|
6163
|
+
}
|
|
6164
|
+
function getPublicReadCachePolicy(endpoint) {
|
|
6165
|
+
return PUBLIC_READ_CACHE_POLICIES[endpoint];
|
|
6166
|
+
}
|
|
6167
|
+
function publicReadFreshnessSeconds(endpoint) {
|
|
6168
|
+
return getPublicReadCachePolicy(endpoint).freshness === "availability" ? PUBLIC_AVAILABILITY_FRESHNESS_SECONDS : PUBLIC_CONTENT_FRESHNESS_SECONDS;
|
|
4711
6169
|
}
|
|
4712
6170
|
|
|
4713
6171
|
//#endregion
|
|
@@ -4793,13 +6251,13 @@ const BOOKING_DEFAULTS_BY_BEHAVIOUR = {
|
|
|
4793
6251
|
memberships: "off"
|
|
4794
6252
|
}
|
|
4795
6253
|
};
|
|
4796
|
-
function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability = "onboarding") {
|
|
6254
|
+
function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability$1 = "onboarding") {
|
|
4797
6255
|
return {
|
|
4798
6256
|
id,
|
|
4799
6257
|
verticalId,
|
|
4800
6258
|
canonicalBehaviour,
|
|
4801
6259
|
engine: BOOKING_ENGINE_BY_BEHAVIOUR[canonicalBehaviour],
|
|
4802
|
-
availability,
|
|
6260
|
+
availability: availability$1,
|
|
4803
6261
|
label,
|
|
4804
6262
|
pluralLabel,
|
|
4805
6263
|
description
|
|
@@ -5153,12 +6611,12 @@ const VERTICAL_PRESET_AVAILABILITIES = [
|
|
|
5153
6611
|
"future"
|
|
5154
6612
|
];
|
|
5155
6613
|
function getVerticalPresetsForVertical(verticalId, options) {
|
|
5156
|
-
const availability = options?.availability;
|
|
6614
|
+
const availability$1 = options?.availability;
|
|
5157
6615
|
return VERTICAL_BOOKING_PRESETS.filter((presetDefinition) => {
|
|
5158
6616
|
if (presetDefinition.verticalId !== verticalId) return false;
|
|
5159
|
-
if (!availability) return true;
|
|
5160
|
-
if (availability === "selectable") return presetDefinition.availability !== "future";
|
|
5161
|
-
return presetDefinition.availability === availability;
|
|
6617
|
+
if (!availability$1) return true;
|
|
6618
|
+
if (availability$1 === "selectable") return presetDefinition.availability !== "future";
|
|
6619
|
+
return presetDefinition.availability === availability$1;
|
|
5162
6620
|
});
|
|
5163
6621
|
}
|
|
5164
6622
|
function getOnboardingPresetsForVertical(verticalId) {
|
|
@@ -5382,4 +6840,62 @@ function orderPresetIdsForVertical(verticalId, presetIds) {
|
|
|
5382
6840
|
}
|
|
5383
6841
|
|
|
5384
6842
|
//#endregion
|
|
5385
|
-
|
|
6843
|
+
//#region ../core/src/navigation-locations.ts
|
|
6844
|
+
/** The only places a site's navigation can be displayed. */
|
|
6845
|
+
const NAVIGATION_LOCATIONS = [
|
|
6846
|
+
"main",
|
|
6847
|
+
"footer",
|
|
6848
|
+
"legal"
|
|
6849
|
+
];
|
|
6850
|
+
const NAVIGATION_LOCATION_DESCRIPTORS = {
|
|
6851
|
+
main: {
|
|
6852
|
+
location: "main",
|
|
6853
|
+
label: "Main navigation",
|
|
6854
|
+
description: "Links shown in the site header when header navigation is enabled."
|
|
6855
|
+
},
|
|
6856
|
+
footer: {
|
|
6857
|
+
location: "footer",
|
|
6858
|
+
label: "Footer links",
|
|
6859
|
+
description: "Links shown in the main footer links section when it is enabled."
|
|
6860
|
+
},
|
|
6861
|
+
legal: {
|
|
6862
|
+
location: "legal",
|
|
6863
|
+
label: "Legal links",
|
|
6864
|
+
description: "Smaller footer links for privacy, terms and other policies."
|
|
6865
|
+
}
|
|
6866
|
+
};
|
|
6867
|
+
function isNavigationLocation(value) {
|
|
6868
|
+
return typeof value === "string" && NAVIGATION_LOCATIONS.includes(value);
|
|
6869
|
+
}
|
|
6870
|
+
/** Parse all rows for one site into a complete fixed-location record. */
|
|
6871
|
+
function parseSiteNavigationLocations(rows) {
|
|
6872
|
+
const locations = {};
|
|
6873
|
+
for (const [index, row] of rows.entries()) {
|
|
6874
|
+
if (!isNavigationLocation(row.location)) {
|
|
6875
|
+
return err({
|
|
6876
|
+
kind: "invalid_location",
|
|
6877
|
+
index,
|
|
6878
|
+
value: row.location
|
|
6879
|
+
});
|
|
6880
|
+
}
|
|
6881
|
+
if (Object.prototype.hasOwnProperty.call(locations, row.location)) {
|
|
6882
|
+
return err({
|
|
6883
|
+
kind: "duplicate_location",
|
|
6884
|
+
location: row.location
|
|
6885
|
+
});
|
|
6886
|
+
}
|
|
6887
|
+
locations[row.location] = row.value;
|
|
6888
|
+
}
|
|
6889
|
+
for (const location of NAVIGATION_LOCATIONS) {
|
|
6890
|
+
if (!Object.prototype.hasOwnProperty.call(locations, location)) {
|
|
6891
|
+
return err({
|
|
6892
|
+
kind: "missing_location",
|
|
6893
|
+
location
|
|
6894
|
+
});
|
|
6895
|
+
}
|
|
6896
|
+
}
|
|
6897
|
+
return ok(locations);
|
|
6898
|
+
}
|
|
6899
|
+
|
|
6900
|
+
//#endregion
|
|
6901
|
+
export { publicEventPricingDisplay as $, assertAllowedPublicUrl as A, unsafeAsSiteFontId as At, getBookingLocalDateKey as B, parseMailingListSignedSourceToken as C, unsafeAsMediaAssetId as Ct, parseFooterCompositionV1 as D, unsafeAsPositiveSeconds as Dt, footerNavigationLayouts as E, unsafeAsPositiveByteCount as Et, formatBookingDate as F, unsafeAsStoragePath as Ft, toScheduledBookingDateRange as G, getLocalScheduleTime as H, formatBookingDateParts as I, unsafeAsUploadSessionId as It, parseEventPageSurfaceSubject as J, toScheduledBookingDateTime as K, formatBookingTime as L, unsafeAsUserId as Lt, normalizeLocationAddressText as M, unsafeAsSiteScopedStoragePath as Mt, toLocationCoordinates as N, unsafeAsStaffMemberId as Nt, parseMailingListPlacementTargeting as O, unsafeAsProductId as Ot, buildBookingUtcDateWindow as P, unsafeAsStorageBucketName as Pt, publicEventPricingCurrency as Q, formatLocalScheduleDate as R, err as Rt, parseMailingListBlockId as S, unsafeAsFileChecksumSha256 as St, planMailingListChoicePresentation as T, unsafeAsOriginalFilename as Tt, parseUtcIsoDateTimeString as U, getLocalScheduleDate as V, toBookingTimeZone as W, formatEventPricing as X, formatBookingPriceDisplay as Y, isPublicEventPricingOptionDisplayable as Z, createSecret as _, unsafeAsDemoCapability as _t, parseVerticalId as a, formatCurrencyAmount as at, stableJsonStringify as b, unsafeAsEventOfferingId as bt, DEFAULT_EVENT_TYPE_PRESETS as c, parseEventSeriesId as ct, SURFACE_EVENT_PRESET_VALUES as d, parseSiteId as dt, publicEventPricingType as et, WHOLE_SERIES_EVENT_PRESET_VALUES as f, parseUserId as ft, resolveWholeSeriesBookingPhase as g, unsafeAsDeclaredMimeType as gt, orderedLiveRunSessions as h, unsafeAsBookingSessionId as ht, isEventSeriesVerticalPresetDefinition as i, mapNonEmptyArray as it, isPresent as j, unsafeAsSiteId as jt, isCanonicalRichTextDoc as k, unsafeAsSafeDownloadFilename as kt, EVENT_PRESET_VALUES as l, parseMediaAssetId as lt, CANCELLED_SESSION_STATUS as m, unsafeAsBookingRequirementId as mt, areConfiguredBookingVerticalConfigsEqual as n, resolvePricingOptionsPriceDisplay as nt, parseVerticalPresetId as o, assertNever as ot, publicReadFreshnessSeconds as p, unsafeAsAppointmentResourceId as pt, parseBookingTimeZone as q, canonicalizeConfiguredBookingVerticalConfig as r, fromArray as rt, resolveSiteVerticalBookingConfig as s, parseApiKeyId as st, NAVIGATION_LOCATIONS as t, resolveExactOrFreePriceDisplay as tt, ORDINARY_EVENT_PRESET_VALUES as u, parsePageId as ut, exposeSecret as v, unsafeAsDetectedMimeType as vt, parseMailingListRuntimeContext as w, unsafeAsOccurrenceId as wt, buildMailingListSignupBlockSourceRef as x, unsafeAsEventSeriesId as xt, parseManagementKey as y, unsafeAsEntitlementId as yt, formatLocalScheduleTime as z, ok as zt };
|