CU-05: Event-type configuration → public booking-page enforcement

Priority: P0 Accounts/sessions: P1 host only (ravikantguptaofficial@gmail.com, signed into Calendo via Google, Gmail = host notification inbox). P3 (everythingaichannelemail@gmail.com) is used ONLY as a real guest email for the L3 guest-notification sub-section (m). No login as P3 is required. Parallel-safe: Yes. This suite only creates/edits/deletes its OWN, RUNID-scoped event types and bookings. It never edits global host availability, settings, or other suites' data. Exclusive (rewrites global host availability?): No. Do NOT touch the Availability tab. All limits are set per-event-type inside the event-type edit panel. Estimated time: ~75–90 minutes for one full run (14 sub-sections, each create + verify + cleanup). L3 reality checks: One — sub-section (n): the additional-guest invitee (P3) must receive a real "you're invited" email in their inbox. All other sub-sections are L1/L2 only.

Goal

This suite proves that every per-event-type setting a host configures in the Calendo dashboard is actually ENFORCED on the public booking page that invitees see and use. A scheduling product is only trustworthy if "I set a 15-minute slot interval / require a phone number / cap at 2 bookings a day / hide this event / allow group sign-ups" really changes what the invitee experiences. We create one uniquely-named event type per setting, configure it, then open the public booking page as an anonymous invitee and confirm the setting takes effect (duration shown, questions required, near-now slots hidden, secret event hidden from the list but bookable by direct link, group "spots left", prefilled fields, guest notification email actually delivered, etc.). It is the core "config → enforcement" contract of the product.

Preconditions

Test data

Steps

0. Setup (run once)

  1. Action: Go to https://calendo.dev/dashboard/. Wait for the dashboard to load. Expect: #appLayout and #welcomeHeader visible; no login screen. [L1]
  2. Action: Read the host booking link text from the share box (#bookingLinkValue). It looks like https://calendo.dev/booking/?user=<HOST_SLUG>. Record <HOST_SLUG> (everything after user=). Expect: A non-empty slug. [L1] Capture screenshot: cu05-00-dashboard-link.
  3. Action: Click the Event Types sidebar tab (.sidebar-nav a[data-tab="event-types"]). Expect: Panel #panel-event-types becomes active and shows the create form fields (#etName, #etDuration, #etLocation, #etSubmitBtn) and the existing event types list (#eventTypesList). [L1]

Order note: For each sub-section below, the pattern is the same. (1) CREATE the event type from the FULL form in the Event Types tab (this is the only form with Secret/Group/Guests/Redirect/location-address). (2) For settings the full form lacks (slot interval, buffers, min notice, max-per-day, locked timezone, custom questions, conditional questions), open the per-event slide-in EDIT panel (#editEtPanel) on that event type and set them there, then Save. (3) Open the PUBLIC booking page in a way that simulates an anonymous invitee and verify enforcement.

Helper — opening the public booking page as an invitee: navigate to https://calendo.dev/booking/?user=<HOST_SLUG>&event=<EVENT_SLUG>. The booking detail view is #booking-view; the event header is #sidebar-event-name. To pick a slot: click the first day cell with class .calendar-day.available, then click the first .time-slot button; if no available day appears, click the next-month nav button (.calendar-nav-btn, last one) and retry up to 6 times. The confirm form fields are #input-name, #input-email, optional #input-phone, custom questions inside #custom-questions, guests inside #guests-section; submit is #btn-confirm; success is the .confirmation-title reading "Booking Confirmed!".

Helper — opening the EDIT panel for an event type: in the Event Types tab, find the event-type card whose title contains the RUNID name and click its "Edit" control. The slide-in panel #editEtPanel gets class active. Its sections are collapsible — click the section header text ("Limits & Buffers" or "Booking Questions") to expand. Field ids: #editEtSlotInterval, #editEtBufferBefore, #editEtBufferAfter, #editEtMinNotice, #editEtMaxPerDay, #editEtLockedTz, reminder checkboxes #editEtRemEmail24/1 + #editEtRemSms24/1, #editEtReconfirmation, questions list #editEtQuestionsList with + Add question button. Save with #editEtSaveBtn; the panel closes / removes active.


(a) Duration + slot interval / granularity

  1. Action: In the Event Types tab create form, fill name CU05 Duration ${RUNID} (#etName), select Duration = 30 min (#etDuration value 30), leave Location = Google Meet, click Create (#etSubmitBtn). Expect: Form clears and the new card appears in #eventTypesList showing the name and "30m". [L1]
  2. Action: Open the edit panel for CU05 Duration ${RUNID}. Expand "Limits & Buffers". Set Slot interval (#editEtSlotInterval) = 30 min. Save (#editEtSaveBtn). Expect: Panel closes. [L1]
  3. Action: Reopen the edit panel, expand "Limits & Buffers". Expect: #editEtSlotInterval = 30 and #editEtDuration = 30 (settings persisted). [L2] Close panel.
  4. Action: Open the public booking page …&event=cu05-duration-${RUNID}. Expect: #sidebar-event-name shows the name; #sidebar-duration-text shows "30 min" (duration enforced). [L1]
  5. Action: Click the first available day; inspect the listed time slots (.time-slot buttons in #time-slots-list). Expect: Consecutive slot start times are 30 minutes apart (e.g. 9:00, 9:30, 10:00 — NOT 9:15), confirming the 30-minute granularity. [L1] Capture screenshot: cu05-a-slots-30min.
  6. Pass criterion (a): Sidebar shows 30 min duration AND visible slot starts are 30 minutes apart.

(b) Description + color

  1. Action: Create CU05 DescColor ${RUNID} with Duration 30. In the create form set Description (#etDescription) = Color and description check ${RUNID} and pick a non-default color swatch in the color picker (#etColorSwatches; the hidden value lives in #etColor, default #2563eb blue — choose any other swatch). Click Create. Expect: Card appears; its color dot reflects the chosen color. [L1]
  2. Action: Open the edit panel for it; confirm #editEtDesc holds the description text and the Color section preview shows the chosen color. Expect: Values persisted. [L2] Close panel.
  3. Action: Open the public booking page …&event=cu05-desccolor-${RUNID}. Expect: #sidebar-description shows the description text; the page accent / event color matches the chosen swatch (header dot / brand accent). [L1] Capture screenshot: cu05-b-desc-color.
  4. Pass criterion (b): Description text and chosen color both appear on the public booking page.

(c) Location config (video / in-person address) shown

  1. Action: Create CU05 Location ${RUNID} with Duration 30. In the create form set Location (#etLocation) = "In person"; the detail input (#etLocationDetail#etLocationValue) appears — fill it with Suite 200, 1 Market St, SF ${RUNID}. Click Create. Expect: Card created with In person location. [L1]
  2. Action: Open the public booking page …&event=cu05-location-${RUNID}. Expect: #sidebar-location-text shows the in-person address Suite 200, 1 Market St, SF ${RUNID} (or "In person" with the address). [L1] Capture screenshot: cu05-c-location-inperson.
  3. Action (video variant): Create a second event CU05 LocationVideo ${RUNID} (slug cu05-locationvideo-${RUNID}) with Location = "Google Meet". Open its public page. Expect: #sidebar-location-text shows "Google Meet" (auto-generated video). [L1]
  4. Pass criterion (c): In-person address text renders on the public page for the in-person event; "Google Meet" renders for the video event.

(d) Custom questions (text/select, required) render + required-validation blocks submit + answers saved [L2]

  1. Action: Create CU05 Questions ${RUNID} (Duration 30, Allow guests can stay default). Then open its edit panel and expand "Booking Questions". Click "+ Add question" in #editEtQuestionsList. In the new row, set label = Company (placeholder "Question label"), keep type = text, and CHECK the required checkbox (input[type="checkbox"] in that row). Add a second question: label = Team size, change its type select to a dropdown/select option, add options (e.g. Small, Large), and CHECK required. Save (#editEtSaveBtn). Expect: Panel closes. [L1]
  2. Action: Reopen the edit panel, expand "Booking Questions". Expect: Both questions present with correct labels, types, and required flags. [L2] Close panel.
  3. Action: Open the public booking page …&event=cu05-questions-${RUNID}, pick a slot to reach the confirm form. Expect: #custom-questions renders both questions; the required ones carry a required marker. [L1]
  4. Action: Fill #input-name = Invitee ${RUNID} d, #input-email = ravikantguptaofficial+inv-${RUNID}-d@gmail.com, LEAVE the required Company question EMPTY, and click #btn-confirm. Expect: Submission is blocked — the empty required question field is focused / shows native validation (its checkValidity() is false); no confirmation appears. [L1] Capture screenshot: cu05-d-required-block.
  5. Action: Now fill Company = Acme ${RUNID} and select Team size = Large, click #btn-confirm. Expect: .confirmation-title shows "Booking Confirmed!". [L1] Capture screenshot: cu05-d-confirmed.
  6. Action: Return to https://calendo.dev/dashboard/, open the Bookings tab (a[data-tab="bookings"]#bookingsTableContainer), find the booking for invitee Invitee ${RUNID} d, open its detail. Expect: Detail shows a "Booking Questions" section listing Company = Acme ${RUNID} and Team size = Large (answers persisted). [L2] Capture screenshot: cu05-d-answers-saved.
  7. Pass criterion (d): Required question blocks submit when empty; after filling, booking confirms; saved answers appear verbatim in the dashboard booking detail.

(e) Conditional questions show/hide based on prior answer

  1. Action: Create CU05 Conditional ${RUNID} (Duration 30). Open its edit panel, expand "Booking Questions". Add question 1: label = Plan (or a Yes/No-style question). Add question 2: label = Budget, and configure its condition so it only shows when question 1's answer equals a specific value (e.g. Enterprise) — in the row's condition controls choose "show when [question 1] = Enterprise". Save. Expect: Panel closes; reopening shows both questions and the condition on Budget. [L2]

Note: the worker rejects forward-references / non-existent indices, so the condition MUST point to an earlier question. If the UI lets you save an invalid condition and the API errors, flag it.

  1. Action: Open the public page …&event=cu05-conditional-${RUNID}, pick a slot to reach the form. In #custom-questions, set question 1 (Plan) to a NON-triggering value (e.g. Startup). Expect: The Budget question's form-group is hidden. [L1]
  2. Action: Change question 1 to the triggering value Enterprise. Expect: The Budget question becomes visible. [L1] Capture screenshot: cu05-e-conditional-shown.
  3. Action: With Budget visible and required-by-condition, leave it blank and click #btn-confirm. Expect: Submit blocked (Budget invalid). Then fill Budget = Over 50k, fill name/email (+inv-${RUNID}-e), confirm. Expect: "Booking Confirmed!". [L1]
  4. Pass criterion (e): Budget hidden for non-triggering answer, shown (and validated) for the triggering answer; booking confirms once filled.

(f) Buffers before/after

  1. Action: Create CU05 Buffers ${RUNID} (Duration 30). Open its edit panel, expand "Limits & Buffers". Set Buffer before (#editEtBufferBefore) = 30 and Buffer after (#editEtBufferAfter) = 30. Save. Reopen and confirm both values persisted = 30. Expect: Persisted. [L2]
  2. Action: Create a baseline twin CU05 BuffersBase ${RUNID} (Duration 30, buffers left at 0) for comparison.
  3. Action: Open the public page for the baseline event, pick the first available day, and note the count of slots offered for that day. Then open the public page for cu05-buffers-${RUNID} on the SAME day, and note the slot count. Expect: The buffered event shows FEWER (or equal-but-spaced-out) slots than the baseline on the same day, because the 30-min before/after buffers consume gaps between meetings. [L1] Capture screenshot: cu05-f-buffers-fewer-slots.
  4. Pass criterion (f): Buffered event offers strictly fewer slots than the zero-buffer twin on the same available day (or visibly larger gaps between offered start times).

(g) Daily / weekly meeting caps (book up to the cap, confirm next is blocked)

  1. Action: Create CU05 Caps ${RUNID} (Duration 30). Open its edit panel, "Limits & Buffers", set Max per day (#editEtMaxPerDay) = 1. Save; reopen to confirm #editEtMaxPerDay = 1. Expect: Persisted. [L2]
  2. Action: Open the public page …&event=cu05-caps-${RUNID}, pick the first available DAY, book one slot (name/email +inv-${RUNID}-g1). Expect: "Booking Confirmed!". [L1]
  3. Action: Re-open the public page for the same event and navigate to the SAME day you just booked. Expect: That day now shows ZERO available slots (the day is full / not selectable / shows no .time-slot), because the per-day cap of 1 is reached. Other days still show slots. [L1] Capture screenshot: cu05-g-day-capped.
  4. Pass criterion (g): After 1 booking, the capped day offers no further slots while other days remain bookable.

Note on weekly caps: the dashboard edit panel exposes only per-day cap (#editEtMaxPerDay); weekly/daily "meeting limits" are a schedule-level feature (availability schedule meeting_limits), not a per-event-type field, so a true per-event WEEKLY cap is out of scope here — see TBD. Verify the daily cap above as the representative cap test.

(h) Minimum scheduling notice (near-now slots hidden)

  1. Action: Create CU05 Notice ${RUNID} (Duration 30). Open its edit panel, "Limits & Buffers", set Min notice (hours) (#editEtMinNotice) = 72. Save; reopen to confirm = 72. Expect: Persisted. [L2]
  2. Action: Open the public page …&event=cu05-notice-${RUNID}. Expect: No slots are offered within the next 72 hours from now — today and the next ~3 days show no available .time-slots; the earliest selectable day is roughly 3+ days out. [L1] Capture screenshot: cu05-h-notice-hidden.
  3. Action (control): Compare against any default event (e.g. "15 Minute Chat") on the public page, which should offer slots much sooner. Expect: The 72h-notice event's earliest slot is clearly later than the control's. [L1]
  4. Pass criterion (h): No bookable slot exists within 72 hours of "now" for the min-notice event; control event offers earlier slots.

(i) Timezone lock ("(locked)" shown on booking page)

  1. Action: Create CU05 LockTz ${RUNID} (Duration 30). Open its edit panel, "Limits & Buffers", set Lock timezone (#editEtLockedTz) = New York (America/New_York). Save; reopen to confirm. Expect: Persisted. [L2]
  2. Action: Open the public page …&event=cu05-locktz-${RUNID}. Expect: The timezone selector (#timezone-select) is DISABLED, its value is America/New_York, and the visible option text contains "(locked)" (e.g. "America/New York (locked)"). The invitee cannot change timezone. [L1] Capture screenshot: cu05-i-tz-locked.
  3. Pass criterion (i): #timezone-select is disabled, fixed to America/New_York, and displays "(locked)".

(j) Post-booking redirect URL (invitee redirected with context params)

  1. Action: Create CU05 Redirect ${RUNID}. In the create form, expand "Advanced" and set Redirect URL (#etRedirectUrl) = https://example.com/thanks-${RUNID} (must be http/https; the API rejects non-URLs and non-http schemes). Click Create. Expect: Card created. [L1]
  2. Action: Open its edit/detail or re-open the create form on it to confirm the redirect URL persisted, OR proceed to behavioral check. [L2]
  3. Action: Open the public page …&event=cu05-redirect-${RUNID}, pick a slot, fill name Invitee ${RUNID} j / email +inv-${RUNID}-j, click #btn-confirm. Expect: Instead of the in-app "Booking Confirmed!" screen, the browser navigates to https://example.com/thanks-${RUNID}?... with query params including invitee_name, invitee_email, event_type=cu05-redirect-${RUNID}, and start_time=<booked ISO>. [L1] Capture screenshot: cu05-j-redirect-url (capture the redirected URL bar showing the params).
  4. Pass criterion (j): After confirming, the invitee lands on the configured redirect URL with invitee_name, invitee_email, event_type, and start_time query params populated.

(k) Secret event (hidden from public list, bookable via direct link, lock badge in dashboard)

  1. Action: Create CU05 Secret ${RUNID} (Duration 30). In the create form CHECK "Secret" (#etSecretToggle). Click Create. Expect: Card created; in the dashboard event-types list it shows a secret/lock indicator (and is excluded from public listing). [L1] Capture screenshot: cu05-k-secret-dashboard.
  2. Action: Open the public LIST page (no event= param): https://calendo.dev/booking/?user=<HOST_SLUG>. Expect: #event-list-view / #event-types-grid lists the host's public events but does NOT contain CU05 Secret ${RUNID}. [L1]
  3. Action: Open the DIRECT link …&event=cu05-secret-${RUNID}. Expect: #booking-view opens and #sidebar-event-name shows CU05 Secret ${RUNID} — the secret event IS bookable via direct link. [L1] Optionally complete one booking (name/email +inv-${RUNID}-k) → "Booking Confirmed!". [L1] Capture screenshot: cu05-k-secret-direct.
  4. Pass criterion (k): Secret event is absent from the public list but reachable and bookable via its direct ?event= link; dashboard shows it as secret.

(l) Group event (max attendees, "X spots left", multiple invitees can book)

  1. Action: Create CU05 Group ${RUNID} (Duration 30). In the create form CHECK "Group event" (#etGroupToggle); the group fields appear (#etGroupFields) — set Max attendees per slot (#etGroupMax) = 3. Keep Allow guests as-is. Click Create. Expect: Card created; dashboard shows a "Group (max 3)" badge. [L1] Capture screenshot: cu05-l-group-dashboard.
  2. Action: Open the public page …&event=cu05-group-${RUNID}, pick a day. Expect: Slots render with a "spots" hint (e.g. "3 spots") next to slot times (group mode). [L1]
  3. Action: Book the SAME slot once as invitee +inv-${RUNID}-l1 → "Booking Confirmed!". Re-open the public page, navigate to the same day/slot. Expect: That slot is STILL available and now reads "2 spots left" (or "X spots left" with X reduced by 1). [L1] Capture screenshot: cu05-l-spots-left.
  4. Action: Book the same slot again as +inv-${RUNID}-l2 → "Booking Confirmed!". Re-open; the slot should now read "1 spot left". Book a third time as +inv-${RUNID}-l3. Expect: Third booking confirms (reaching max 3). Re-open the page on that day. Expect: The slot is now GONE/unavailable (max attendees reached). [L1]
  5. Pass criterion (l): Slot accepts up to 3 distinct invitees, shows decreasing "X spots left" after each, and disappears once the cap of 3 is hit.

(m) Prefilled link ?name=&email= auto-fills the form

  1. Action: Create CU05 Prefill ${RUNID} (Duration 30). [L1]
  2. Action: Open the public page with prefill params: https://calendo.dev/booking/?user=<HOST_SLUG>&event=cu05-prefill-${RUNID}&name=Pre%20Filled%20${RUNID}&email=ravikantguptaofficial%2Binv-${RUNID}-m%40gmail.com. Pick a slot to reach the confirm form. Expect: #input-name is pre-populated with Pre Filled ${RUNID} and #input-email with ravikantguptaofficial+inv-${RUNID}-m@gmail.com, before any typing. [L1] Capture screenshot: cu05-m-prefilled.
  3. Action: Click #btn-confirm without editing. Expect: "Booking Confirmed!". [L1]
  4. Pass criterion (m): Name and email fields auto-fill from the URL params; booking confirms with those values.

(n) Allow guests — add guest emails (P3) and L3-confirm the guest gets a notification email

  1. Action: Create CU05 Guests ${RUNID} (Duration 30) with "Allow guests" (#etAllowGuestsToggle) CHECKED (it is checked by default). Click Create. Expect: Card created. [L1]
  2. Action: Open the public page …&event=cu05-guests-${RUNID}, pick a slot to reach the confirm form. Expect: The "Additional Guests" section (#guests-section) is VISIBLE with an "Add guest" button (#add-guest-btn). [L1]
  3. Action: Click #add-guest-btn once; in the new guest input (.guest-email-input) enter everythingaichannelemail@gmail.com (P3). Fill #input-name = Invitee ${RUNID} n, #input-email = ravikantguptaofficial+inv-${RUNID}-n@gmail.com. Click #btn-confirm. Expect: "Booking Confirmed!". [L1] Capture screenshot: cu05-n-guest-confirmed.
  4. Action: Go to the dashboard Bookings tab, open the booking for Invitee ${RUNID} n. Expect: Detail shows the guest everythingaichannelemail@gmail.com listed (or the bookings list shows a "+1 guest" indicator). [L2] Capture screenshot: cu05-n-guest-in-detail.
  5. Action (negative control): Create CU05 NoGuests ${RUNID} with Allow guests UNCHECKED, open its public page, pick a slot. Expect: #guests-section is HIDDEN (no guest UI). [L1]
  6. L3: See L3 reality checks below — confirm P3 actually received the guest invite email.
  7. Pass criterion (n): Guest section shows when allowed and hides when disallowed; the booking stores the guest; AND (L3) P3 receives a real guest-notification email.

L3 reality checks

Only sub-section (n) has an L3 check.

  1. Action: In the browser, open Gmail for P3 at https://mail.google.com (the inbox for everythingaichannelemail@gmail.com — it must already be the active/selected Gmail account per preconditions). In the Gmail search box, run the query: subject:(invited) ${RUNID} and, if nothing, also try from:(calendo) ${RUNID} and "CU05 Guests ${RUNID}". Expect: A recent email (within the last few minutes) whose subject indicates the recipient was invited to a meeting (the guest-notification email subject contains "invited"), and whose body references the event CU05 Guests ${RUNID} (or "Meeting"), the host name (P1 / Ravi), and the invitee Invitee ${RUNID} n, at the booked date/time. [L3] Capture screenshot: cu05-n-L3-guest-email.
  2. Action: Open the email and verify the meeting date/time matches the slot you booked and that it is addressed to the guest (P3), not just the primary invitee. Expect: Details consistent with the booking. [L3]

Note: The PRIMARY invitee confirmation emails for all sub-sections land in P1's Gmail as plus-aliases (ravikantguptaofficial+inv-${RUNID}-*@gmail.com). Verifying those is OPTIONAL for this suite (this suite's only required L3 is the guest notification). If you want extra evidence, search P1's Gmail for inv-${RUNID} to see the batch of confirmations — but treat any missing primary-confirmation emails as Manual residue, not a suite failure, since email deliverability for the host inbox is covered by the notifications suite.

Cleanup

Delete EVERYTHING this run created so host accounts stay clean. Use the RUNID to find items.

  1. Bookings first: In the dashboard Bookings tab (#bookingsTableContainer), for EACH booking whose invitee email contains inv-${RUNID} or whose event name contains CU05 … ${RUNID} (including the multiple group bookings l1/l2/l3 and all guest/prefill/redirect bookings), open it and Cancel it. Confirm each shows cancelled status. (Cancel bookings BEFORE deleting their event types so they don't dangle.)
  2. Event types: In the Event Types tab, for EACH event type whose name contains CU05 and ${RUNID} — that is (a) Duration, (b) DescColor, (c) Location, the LocationVideo twin, (d) Questions, (e) Conditional, (f) Buffers + BuffersBase twin, (g) Caps, (h) Notice, (i) LockTz, (j) Redirect, (k) Secret, (l) Group, (m) Prefill, (n) Guests + NoGuests twin — open its edit panel and click Delete (#editEtDeleteBtn), confirm in the dialog (#dialogConfirm). Expect: Each disappears from the list. Do NOT delete the 4 default event types.
  3. Calendar side effects: Booking confirmations may create Google Calendar events on P1's connected calendar. After cancelling all bookings, open https://calendar.google.com as P1 and confirm no leftover events titled with CU05 … ${RUNID} remain on the booked dates; delete any stragglers. (The redirect sub-section (j) and group sub-section (l) created real calendar events — verify those are gone after cancellation.)
  4. Verify clean: Reload the Event Types tab and Bookings tab; searching/scanning for ${RUNID} should return nothing. The throwaway redirect target (example.com) requires no cleanup. No throwaway Calendo account was created (we used the persisted P1 session), so none to delete.

Pass/Fail criteria

The run PASSES only if ALL of the following hold:

The run FAILS if any sub-section's pass criterion is unmet, if any setting configured in the dashboard does NOT take effect on the public booking page, if the L3 guest email never arrives, or if a precondition (logged-in P1 session, host availability) is missing and was not properly flagged.

Evidence to capture

Manual residue / cannot-verify