Class Booking Software

ClassTap

Fill Every Seat Effortlessly

ClassTap is a lightweight white‑label class-booking platform that handles scheduling, secure payments, waitlists, and automated SMS/email reminders for independent instructors, community centers, and small studios running in-person or hybrid classes, preventing double-bookings, halving admin time, cutting no-shows, and generating branded booking pages in under two minutes.

Subscribe to get amazing product ideas like this one delivered daily to your inbox!

ClassTap

Product Details

Explore this AI-generated product idea in detail. Each aspect has been thoughtfully created to inspire your next venture.

Vision & Mission

Vision
Empower independent instructors and small studios to fill every seat reliably, boost earnings, and run bookings without friction.
Long Term Goal
Within 3 years, power bookings for 50,000 independent instructors across North America, reduce aggregate no‑shows by 25%, and eliminate manual scheduling workflows.
Impact
Helps independent instructors and small studios cut administrative time by 50%, reduce no-shows by 30% through automated calendar-sync reminders, and boost booking conversions 15% with instant branded class pages, enabling new classes to be onboarded in under five minutes.

Problem & Solution

Problem Statement
Independent instructors and small-studio owners struggle with double-bookings, manual payments, and frequent no-shows, while marketplaces and enterprise schedulers are costly, complex, and erase local branding, leaving booking and payment workflows fragmented and fragile.
Solution Overview
We provide a lightweight white‑label booking platform that prevents double‑bookings and reduces no‑shows by syncing instructors' calendars in real time and sending automated, timed SMS and email reminders to students.

Details & Audience

Description
ClassTap is a lightweight SaaS that handles class scheduling, secure payments, waitlists, and automated reminders. It serves independent instructors, community centers, and small studios running in-person or hybrid classes. It prevents double-bookings, halves admin time, and cuts no-shows with calendar sync and scheduled reminders. Its instant class-page generator creates a branded booking page with native payment links in under two minutes.
Target Audience
Independent instructors and small-studio owners aged 25-55 needing effortless bookings, fewer no-shows, and on-the-go management.
Inspiration
As a weekend pottery teacher I juggled frantic texts, a tangled spreadsheet, and Venmo requests until a venue cancellation sent a flood of reschedules. I slapped together a one-page booking link and timed reminders; cancellations stopped, deposits landed, and familiar faces came back. That instant of calm—replacing chaos with a tiny booking page and automated nudges—sparked ClassTap: the tool I wished I’d had.

User Personas

Detailed profiles of the target users who would benefit most from this product.

C

Corporate Wellness Casey

- Age 29–42; HR/Benefits or Workplace Experience title - Mid-size to enterprise (200–5,000 employees); multi-office footprint - Bachelor’s in HR/business; SHRM or wellness certs - Budget authority up to $50K; KPI: participation rate

Background

Started as office coordinator managing room calendars and vendor invoices. After costly no-shows, built a wellness program from scattered spreadsheets. Now coordinates multi-site sessions and quarterly wellness challenges.

Needs & Pain Points

Needs

1. Companywide scheduling with room/resource conflict prevention 2. Bulk reminders driving attendance across departments 3. Consolidated invoices and attendance exports

Pain Points

1. No-shows from calendar clutter and forgotten invites 2. Double-booked rooms across offices and timezones 3. Vendor payments scattered across systems

Psychographics

- Measures success relentlessly, hates fuzzy metrics - Pragmatic problem-solver, favors dependable, low-drama tools - Employee wellbeing champion, budget-guarded realist - Prefers automation over manual follow-ups

Channels

1. LinkedIn groups 2. SHRM forum 3. Google Search 4. Hacking HR Slack 5. HR Brew newsletter

C

Creator Coach Cam

- Age 22–35; fitness, art, or wellness creator - 50K–500K followers across Instagram/TikTok - Income from classes, merch, digital programs - Solo or tiny team; rents studios ad hoc

Background

Built audience with short-form content and pop-up workouts. Tried DMs and forms for signups; lost revenue to no-shows and link friction. Now runs drops with tight caps and waitlists.

Needs & Pain Points

Needs

1. One-tap booking from stories and bio 2. Auto reminders reducing impulse no-shows 3. Quick waitlists for sold-out drops

Pain Points

1. DMs causing double-bookings and lost payments 2. Link-in-bio clutter hurting conversion 3. Last-minute location changes confusing attendees

Psychographics

- Obsessed with momentum and viral conversion - Values aesthetics; brand control non-negotiable - Moves fast; demands zero-setup tools - Community-first, prioritizes nurturing repeat superfans

Channels

1. Instagram stories 2. TikTok lives 3. Linktree bio 4. YouTube community 5. Substack newsletter

F

Franchise Ops Farah

- Age 30–48; multi-location fitness or arts franchise - Oversees 5–50 sites; regional managers and owners - Bachelor’s; background in operations or consulting - KPIs: utilization, revenue per class, cancellations

Background

Scaled from single studio manager to regional ops. After inconsistent spreadsheets caused double-bookings and missed revenue, mandated standardized systems. Now implements tools chainwide under budget constraints.

Needs & Pain Points

Needs

1. Multi-location calendar with role permissions 2. Chainwide branding and templates 3. Centralized reporting across sites

Pain Points

1. Inconsistent processes causing costly errors 2. Manual consolidation across locations weekly 3. Franchisees resisting tool adoption

Psychographics

- Standardization zealot; hates process variance - Data-driven; dashboards decide, not gut - Empowers local teams within clear guardrails - Risk-averse regarding security and compliance

Channels

1. LinkedIn feed 2. IHRSA newsletter 3. Google Search 4. Ops Leaders Slack 5. Zoom webinars

P

Parent Booker Priya

- Age 28–45; parent or guardian of 1–3 children - Suburban or urban; coordinates carpool and calendars - Heavy iPhone/Android use; Apple/Google Calendar synced - Budget-conscious; prefers class packs and credits

Background

Started with sporadic drop-ins; chaos led to missed sessions and fees. Adopted shared family calendar after double-booking soccer and piano. Prefers providers with easy reschedules and waitlists.

Needs & Pain Points

Needs

1. Book multiple children in one flow 2. Automatic calendar sync per child 3. Clear policies, easy reschedules, credits

Pain Points

1. Separate bookings per child are tedious 2. Missed waitlist promotions without alerts 3. Confusing refund and makeup policies

Psychographics

- Reliability fanatic; hates surprises and vague policies - Convenience-first; values speed over customization - Seeks trusted providers with kid-friendly communication

Channels

1. Facebook Groups 2. Google Maps 3. Instagram DMs 4. WhatsApp chats 5. Nextdoor posts

P

Pop-up Planner Parker

- Age 26–40; side-hustle or small event business - Mix of art, craft, wellness, or food workshops - Books community halls, cafes, parks monthly - Revenue from tickets and sponsorships

Background

Started with Eventbrite but wanted full branding and lower fees. Learned hard lessons from double-booked rooms and walk-up chaos. Now prioritizes waitlists and onsite check-in.

Needs & Pain Points

Needs

1. Two-minute branded page per venue 2. Capacity rules per location and equipment 3. QR check-in and day-of roster

Pain Points

1. Venue conflicts and permits missed 2. Walk-up bottlenecks delaying start times 3. Confusing directions reduce attendance

Psychographics

- Logistics-minded, thrives on tidy run-of-show - Frugal; hates unnecessary platform fees - Audience-builder; values email list growth

Channels

1. Instagram posts 2. Google Search 3. Facebook events 4. Mailchimp newsletter 5. Meetup groups

C

Compliance Registrar Riley

- Age 32–55; training director or registrar - Fields: healthcare, safety, finance, or trades - Runs cohorts quarterly; blends classroom and skills labs - Accountable for compliance audits and records retention

Background

Migrated from legacy registration software after failed audits flagged missing rosters. Consolidated waivers and certificates into one workflow. Now prioritizes clean exports and prerequisite enforcement.

Needs & Pain Points

Needs

1. Prerequisite gating and certification tracking 2. Digital waivers and attendance signatures 3. Audit-ready exports, retention controls

Pain Points

1. Missing signatures jeopardize accreditation 2. Manual roster cleanup before audits 3. Disconnected systems create data silos

Psychographics

- Compliance-first; zero tolerance for data gaps - Systematic thinker; values predictable workflows - Detail-obsessed; demands traceable records

Channels

1. LinkedIn groups 2. IACET newsletter 3. Google Search 4. L&D Collective Slack 5. Zoom webinars

Product Features

Key capabilities that make this product valuable to its target users.

Turbo Rebook

Surface a prominent one-tap Rebook button on class pages, past bookings, receipts, and reminders. Pre-selects the same class time, instructor, attendees, and preferred payment method so users confirm in under 10 seconds—boosting repeat conversions and reducing front-desk churn.

Requirements

Global Rebook CTA Placement
"As a returning student, I want a clearly visible Rebook button wherever I interact with a class or my past bookings so that I can rebook quickly without searching."
Description

Display a prominent, consistent Rebook button across key user touchpoints—class detail pages, past bookings, digital receipts, and SMS/email reminders. The CTA must be visually distinct, accessible (WCAG AA), and responsive across mobile and desktop. Button visibility should be conditional on eligibility (e.g., user has a prior booking for the class type, studio allows rebooking), and hidden or disabled when inapplicable. The component should support white‑label theming (colors, typography, copy) at the studio level and localization for supported languages. Implement a lightweight client module that can be embedded in branded booking pages and a server-side policy check for eligibility. Ensure consistent placement above the fold on supported layouts and provide a fallback link when space is constrained. Include basic loading states and error handling so the CTA never blocks the page render.

Acceptance Criteria
Rebook CTA visible and enabled on class detail page for eligible user
Given a signed-in user with at least one past booking for the same class type at the current studio and the studio has rebooking enabled When the user opens a class detail page for an upcoming session Then a primary Rebook button is rendered within the first viewport (element top < window.innerHeight) on mobile (≤414x896) and desktop (≥1280x800), is enabled, and is not overlapped by other UI And the button is placed in the designated primary-action slot consistent with the studio template And activating the button via click or Enter/Space navigates to the studio-configured rebook flow URL with classId, classTypeId, and instructorId included as parameters
Eligibility gating: hide or disable CTA when rebooking is inapplicable
Given the eligibility API responds ineligible with code POLICY_DISABLED for the studio Then the Rebook CTA is not rendered in the DOM on all touchpoints Given the eligibility API responds ineligible with code NO_PRIOR_BOOKING for the user and class type Then the Rebook CTA is not rendered in the DOM on all touchpoints Given the user is not authenticated and the studio requires sign-in to rebook When the page renders Then a disabled Rebook CTA is shown with aria-disabled=true and helper text Sign in to rebook, and clicking it routes to the sign-in flow; upon successful sign-in and eligibility recheck passing, the CTA becomes enabled without page reload
CTA meets WCAG AA accessibility requirements
Rule: Text-to-background contrast ratio is ≥ 4.5:1 for the button label across all studio themes; focus indicator has ≥ 3:1 contrast and ≥ 2 px width Rule: The control is reachable via keyboard Tab order; Enter and Space activate it; no keyboard trap Rule: The accessible name contains the localized phrase Rebook this class; when disabled, it announces as unavailable via aria-disabled=true Rule: Minimum hit area is 44 x 44 CSS px with at least 8 px spacing from adjacent interactive elements Rule: No color-only or motion-only cues; any icon has accompanying text; disabled state cannot be activated and is announced correctly by screen readers
Responsive, above-the-fold placement with fallback on constrained layouts
Given supported touchpoints (class detail page, past bookings, digital receipts, SMS/email reminders) When the page or template initially loads on mobile 360x640 and desktop 1366x768 reference viewports Then the Rebook CTA renders in the primary-action region and its top is above the first viewport bottom (element top < window.innerHeight) Given the available width for the primary action region is < 160 px Then render a text fallback link labeled Rebook in the header/meta area and do not render the button variant And the fallback link is focusable, has contrast ≥ 4.5:1, and navigates to the same rebook URL as the button And switching between button and fallback introduces CLS ≤ 0.05
Studio-level white-label theming is applied without breaking contrast
Given a studio theme config defining primary color, typography scale, and custom CTA copy When the page loads Then the Rebook CTA adopts those values via CSS variables within 200 ms of client module initialization And if computed contrast would drop below 4.5:1, the component auto-applies an accessible fallback text color to meet AA And changing the active studio theme at runtime updates CTA styles and copy within 200 ms without a flash of unstyled content > 100 ms And the CTA label supports studio override up to 24 characters and truncates with an accessible title attribute when longer
CTA localization and RTL layout support
Given the site locale is en, es, fr, or de Then the CTA label, aria-label, tooltip, and helper text render in that locale using platform i18n; otherwise they default to en Given an RTL locale (e.g., ar, he) Then the CTA mirrors layout, flips icon alignment, and sets direction=rtl without clipping or overflow at up to 30% text expansion When the locale is switched at runtime Then CTA strings update within 200 ms without page reload
Non-blocking client module with server-side eligibility check and graceful errors
Rule: The CTA client module loads asynchronously after critical content; gzipped payload size ≤ 12 KB; adds ≤ 20 ms Total Blocking Time on a mid-tier mobile device Rule: While eligibility is checked, show a skeleton/placeholder of ≤ 48 px height; the module never blocks page render and does not regress page FCP/LCP beyond the agreed budget Rule: The server eligibility endpoint returns a decision in ≤ 300 ms p95; if it times out after 400 ms or errors (HTTP 5xx/network), render a non-blocking Rebook text link that defers eligibility to the rebook flow Rule: Eligibility decisions are cached per user, studio, and class type for 5 minutes and invalidated immediately after a booking is created or canceled
Pre‑Filled Booking Context & Next Occurrence Logic
"As a repeat attendee, I want my previous booking details and the next equivalent class time auto‑selected so that I can confirm without re‑entering information."
Description

When a user taps Rebook, automatically preselect the most relevant next class instance and prior booking details: class type, instructor (when still assigned), location/modality (in‑person vs hybrid), attendee(s), and the user’s preferred payment method or credit pack. Implement smart selection to find the next valid occurrence matching the previous weekday/time window, instructor preference, and location; if the exact slot is unavailable, propose the closest alternatives in order of fit. Validate capacity, age/waiver requirements, and membership eligibility before presenting confirmation. Handle edge cases such as schedule changes, instructor substitutions, holidays, and price updates by transparently indicating differences and recalculating totals, taxes, and credits. Timezone-aware logic ensures travelers see correct local times. Place a short seat hold (e.g., 90 seconds) during confirmation to prevent double-bookings. Persist the prefill payload so deep links and resumes are consistent across devices.

Acceptance Criteria
One-Tap Rebook Prefills Previous Booking Context
Given a user has at least one past completed booking for a class, When they tap Rebook from a supported surface (class page, past booking, receipt, or reminder), Then within 2 seconds the booking form preselects class type, location/modality, instructor (if still assigned), attendee(s), and the preferred payment method or credit pack from the referenced booking. Given the prefilled form is displayed, When the user reviews it, Then all preselected fields are editable and no payment is processed until the user taps Confirm. Given the prior instructor is no longer assigned to that class type, When prefilling, Then the instructor field defaults to Any Instructor and Next Occurrence logic determines the session. Given the prior booking included multiple attendees, When prefilling, Then only eligible attendees are preselected and any ineligible attendee is excluded with an inline reason and a link to resolve (e.g., missing waiver).
Smart Next Occurrence Selection with Ranked Fallbacks
Given a referenced past booking with class type, location/modality, and instructor, When determining the next session, Then the system selects the next occurrence within 14 days on the same weekday with a start time within ±60 minutes and with the same instructor if available. Given no session matches all preferred attributes, When selecting alternatives, Then up to 3 options are proposed ordered by: (1) same instructor + same location + closest start time; (2) same location + closest start time; (3) same instructor + closest start time in the same modality. Given a holiday or temporary closure, When computing candidates, Then canceled or blocked sessions are excluded from both the preselection and alternatives. Given no matching sessions exist within 30 days, When the user opens Rebook, Then no session is preselected, Confirm is disabled, and a Notify Me/Waitlist option is presented.
Eligibility and Capacity Validation Before Confirmation
Given a preselected session is identified, When the form renders, Then seat availability is checked in real time and the Confirm button is disabled if capacity is 0 with an option to join waitlist. Given any attendee fails age, waiver, or signed-policy requirements, When validations run, Then Confirm remains disabled and inline actions allow the user to complete missing steps; upon completion, validation rechecks automatically. Given the selected payment method is a credit pack, When validations run, Then remaining credits are verified; if insufficient, the user is prompted to purchase/top up or switch payment before Confirm is enabled. Given the class requires an active membership, When validations run, Then membership status is verified and ineligible users are shown eligible products or a clear reason preventing booking. Given the user taps Confirm, When a final consistency check runs, Then capacity and eligibility are revalidated atomically; if any check fails, the booking is not created and a specific error with alternatives is shown.
Transparent Pricing Changes and Recalculation
Given a session is preselected, When pricing is calculated, Then current class price, taxes, fees, discounts, and credit application are recomputed and shown as a line-item breakdown before Confirm. Given the current price differs from the prior booking, When the breakdown is rendered, Then a Price Changed indicator shows the delta (absolute and percentage) versus the previous booking. Given a previously used promo or credit has expired or is ineligible, When pricing is recomputed, Then it is not applied and a short note explains why. Given credits fully cover the booking, When pricing is shown, Then total due equals $0.00 and card entry is not required. Given location determines tax jurisdiction, When tax is computed, Then the venue’s tax rules are applied and the grand total reflects the correct tax for the venue location.
Timezone-Aware Selection and Communication
Given the venue has a defined IANA timezone and the user device timezone may differ, When displaying the session time, Then the time is shown in the venue’s local time with a secondary annotation for the user’s local time when different. Given a session occurs across a DST transition, When the time is rendered, Then the UTC offset used matches the venue’s rules for that date and the displayed local time is correct. Given SMS/email reminders are generated, When composing times, Then the venue-local start time is included and, if the recipient’s timezone differs, the recipient-local time is appended. Given a traveler opens Rebook from a different timezone, When selecting the next occurrence, Then the scheduling logic uses the venue’s timezone for matching weekday and time window.
Seat Hold and Double-Booking Prevention
Given capacity is available and the user enters the confirmation step, When the confirmation screen is shown, Then a 90-second seat hold is created for the selected session and attendee count and a visible countdown begins. Given a seat hold exists, When the timer expires without confirmation, Then the hold is released, inventory is restored, Confirm is disabled, and the user is prompted to reselect or retry. Given high concurrency, When two users attempt to hold the last seat simultaneously, Then exactly one hold succeeds and the other user sees an out-of-capacity message with alternatives. Given the user taps Confirm within the hold window, When the booking is created, Then the hold is consumed and cannot result in duplicate bookings even if the user taps Confirm repeatedly (idempotent).
Persisted Prefill Payload and Cross-Device Resume
Given a user initiates Rebook, When the prefill payload is created, Then it is stored as a signed, expiring token containing only non-sensitive references (booking ID, selection parameters, attendee IDs, payment method token reference) and no raw payment data. Given the user opens a Rebook deep link on another device within 30 minutes, When the link is loaded, Then the booking form reproduces the same prefilled selections; if the exact session is no longer available, the Next Occurrence fallback is applied and a short notice explains the change. Given the user’s progress is interrupted (app closed or network loss), When they resume within the token validity window, Then the form restores to the last known state within 2 seconds. Given a booking is successfully completed or the token expires, When the token is reused, Then it is rejected and the user is directed to start a new Rebook flow.
One‑Tap Payment Confirmation
"As a student, I want to confirm my rebooking with one tap using my saved payment method so that the process is fast and secure."
Description

Provide a single-screen confirmation that completes the rebooking in one tap using a stored, tokenized payment method or available credits. Display a concise summary (date/time, instructor, location, attendees, price/credit usage) and a clear Confirm button. Integrate PSP tokenization, support network‑tokenized cards, and invoke step‑up authentication (3DS/SCA) only when required by risk or regulation; fall back to full checkout if step‑up fails. Support biometric confirmation on capable devices and quick-pay options (e.g., Apple Pay/Google Pay) when previously saved. Ensure PCI compliance, idempotent booking creation, and robust error handling with automatic retries where safe. On success, issue receipt, update attendance rosters, decrement capacity, and trigger reminders as usual. Average confirmation time from tap to success should be under 10 seconds on a 3G connection.

Acceptance Criteria
One-Tap Rebook with Stored Tokenized Card (No Step-Up)
Given the user has a previously stored PSP-tokenized payment method and no step-up authentication is required for this transaction And the selected class has available capacity When the user taps Rebook and is presented with the one-tap confirmation screen Then the screen displays date, time, instructor, location, attendees, price (and any credit usage), and the selected payment method And only a single primary action "Confirm" is visible and enabled And no PAN or CSC is displayed or logged; only masked details and token identifiers are used When the user taps Confirm over a simulated 3G connection Then one booking is created and confirmed within 10 seconds And class capacity is decremented and the attendance roster is updated atomically And the receipt is issued and SMS/email reminders are scheduled per standard rules And the UI shows a success state with booking details
Step-Up Authentication Path with Fallback to Full Checkout
Given the stored payment method requires 3DS/SCA for this transaction When the user taps Confirm Then a step-up authentication challenge is invoked via the PSP And upon successful completion the booking is created and confirmed, with receipt issued and roster/capacity updated If the step-up authentication fails or is abandoned Then no booking is created and no successful charge is captured And the user is routed to full checkout with all rebook details pre-populated
One-Tap Rebook Using Account Credits
Given the user has sufficient account credits to cover 100% of the class price When the confirmation screen loads Then the payment summary shows credits applied and zero additional charge When the user taps Confirm Then the credits balance is decremented accordingly, the booking is created, and no card charge is attempted And the receipt reflects credit usage and remaining balance
Biometric Confirmation with Saved Apple Pay/Google Pay
Given the device supports a previously saved Apple Pay or Google Pay method with biometric authentication enabled When the user taps Confirm Then the native wallet confirmation is presented and accepts biometric confirmation When biometric confirmation succeeds Then the booking is created and confirmed using the saved wallet token (network tokenized if available) If wallet authorization fails Then the user is routed to full checkout with pre-populated details and no booking created
Idempotent Booking Creation and Double-Tap Protection
Given the user taps Confirm multiple times within 2 seconds or the network causes duplicate submissions When the backend processes the confirmation requests using an idempotency key Then exactly one booking record is created And exactly one payment capture or credit deduction occurs And subsequent duplicate requests return the original booking reference without side effects
Transient Payment Error Retry and User Messaging
Given the PSP returns a transient error (e.g., timeout or HTTP 5xx) on confirmation Then the system automatically retries the payment request up to 2 times with exponential backoff within 8 seconds If all retries fail Then the user sees a clear error message with a retry option and a path to full checkout; no booking is created Given the PSP returns a definitive decline (e.g., insufficient funds, do not honor) Then no automatic retries are attempted and the user is routed to full checkout; no booking is created
Confirmation Performance on 3G Network
Given a simulated 3G network profile (approx. 1.6 Mbps down, 750 Kbps up, 300 ms RTT) When executing 30 one-tap confirmations without step-up authentication across representative classes Then the average time from Confirm tap to success is 10 seconds or less And instrumentation logs the tap and success timestamps for each transaction for auditability
Waitlist Auto‑Enroll Fallback
"As a student, I want to be auto‑enrolled to the waitlist with my saved preferences if a class is full so that I’m first in line without extra steps."
Description

If the target class is at capacity, offer one‑tap waitlist enrollment that preserves all rebook preferences and payment method. Support optional pre‑authorization or credit reservation per studio policy, and automatically confirm the booking when a seat opens, notifying the user via SMS/email with final details. Respect studio rules for waitlist order, cutoff times, and cancellation windows. Provide clear UI messaging about status and charges, and allow users to opt out or change preferences. Ensure atomic transitions from waitlist to confirmed to prevent double charges or overbooking, and update rosters and notifications accordingly.

Acceptance Criteria
Display Waitlist CTA for Full Class During Rebook
Given a user taps Rebook on a class that is at capacity When the rebook flow initializes Then a primary "Join Waitlist" CTA is displayed and the "Confirm Booking" CTA is suppressed for that class And the UI displays a clear message that the class is full and the user can join the waitlist, including an estimate of position if available And the same class time, instructor, attendees, and preferred payment method are preselected And the user can complete waitlist enrollment in ≤ 2 taps with a total p95 latency ≤ 3 seconds
Preserve Rebook Preferences and Payment on Waitlist Enrollment
Given a user has prior booking metadata (class time, instructor, attendees) and a saved payment method When the user joins the waitlist via Rebook Then those preferences are prefilled and persisted to the waitlist record And the user can optionally edit any preference before confirming enrollment And any edited preferences replace the defaults on the waitlist record And no charge is captured at this step unless the studio policy requires pre-authorization or credit reservation
Pre-Authorization/Credit Hold per Studio Policy
Given the studio policy requires pre-authorization or credit reservation to join a waitlist When the user confirms waitlist enrollment Then the system places a payment pre-authorization or reserves credits for the stated amount and duration And the UI and confirmation message state the hold amount, expiration, and capture conditions And if the pre-authorization fails, waitlist enrollment is blocked and the user receives a clear error with retry and payment update options And if the policy does not require a hold, no hold is placed and the UI states that no immediate charge will occur
Atomic Auto-Enrollment When Seat Opens
Given a seat becomes available before the studio’s auto-enroll cutoff and the user is next in waitlist order and meets policy constraints When the auto-enroll job runs Then the transition from waitlist to confirmed booking is executed atomically and idempotently with a single seat decrement And the payment is captured (from pre-auth) or charged exactly once with no duplicate authorizations And the roster, capacity counts, and user booking history update within 5 seconds And the user and studio receive SMS/email notifications with final booking details and receipt And if payment capture/charge fails, the user is notified to update payment within a configurable grace period and the next eligible waitlisted user is processed per policy
Honor Waitlist Order, Cutoffs, and Cancellation Windows
Given studio-defined waitlist order, auto-enroll cutoff times, and cancellation windows When allocating an opened seat Then the system honors the configured order (e.g., FIFO by join timestamp unless priority rules override) And no auto-enroll occurs after the cutoff; instead the next eligible user receives a claim-notification per studio policy And enrollments and cancellations are blocked if initiated outside the allowed windows, with clear messaging to the user
User Opt-Out and Preference Management for Waitlist
Given a user is on a waitlist via Rebook When the user opts out or edits preferences (attendees, payment method) before a seat is allocated Then the system immediately updates or cancels the waitlist record and releases any payment hold within 1 minute And the UI and SMS/email confirm the change or cancellation and reflect updated position or removal And if the user changes to preferences that no longer match the target class (e.g., different time/instructor), they are prompted to start a new rebook flow rather than silently modifying the existing waitlist request
Deep‑Link Rebook from Communications
"As a student, I want to rebook directly from an email or SMS with a single tap so that I can act immediately when I’m reminded."
Description

Embed secure, expiring deep links in email receipts and SMS reminders that open the prefilled Rebook confirmation screen directly. Links must include a signed, tamper‑proof token (HMAC/JWT) that encodes the booking context and user identity; support magic‑link authentication for frictionless access on new devices and respect device trust settings. On mobile, route to the hosted booking page or native app (if present) via universal/app links; on desktop, open the web confirmation view. Preserve attribution parameters for analytics. Tokens should be single‑use or time‑boxed, and gracefully degrade to a login prompt if validation fails or the context is stale, rehydrating the prefill upon successful authentication.

Acceptance Criteria
Secure Token Validation and Prefilled Rebook
Given a user taps a deep link from an email receipt or SMS reminder within the token validity window And the token is a signed HMAC/JWT with whitelisted alg, iss, aud, iat, and exp claims When the link is opened Then the backend verifies the signature and claims and rejects any altered payload And decodes booking context (class ID/time, instructor, attendees, price tier) and user ID And renders the Rebook confirmation view prefilled with class time, instructor, attendees, and preferred payment method And no login prompt is shown if the device is trusted and the current session user matches the token user And the confirmation view becomes interactive within 2s p50 and 4s p95 on baseline mobile and desktop networks
Token Expiry and Single‑Use Enforcement
Given a deep link is issued with a configurable TTL (default 72h) and single‑use enabled When the token is used to complete a rebook Then the token is marked consumed and cannot be reused And any subsequent open of the same link results in an invalid/expired state And the user is routed to a login prompt with a clear message and the rebook context preserved for rehydration after authentication And tokens past their exp are rejected with the same flow
Magic‑Link Auth and Device Trust Respect
Given a user opens a deep link on a device where they are not authenticated When magic‑link authentication is allowed for the tenant and risk checks pass Then the user is seamlessly authenticated without password entry And device trust is applied per policy (trusted if enabled; otherwise session remains temporary) And the Rebook confirmation view appears with the original prefill intact And if an existing session belongs to a different user, the system prompts to switch accounts before proceeding and preserves the prefill after switch And if step‑up verification is required, the user completes it and then sees the prefilled confirmation
Mobile Routing via Universal/App Links
Given a user taps a deep link on iOS or Android When the native app is installed and universal/app links are correctly configured Then the OS routes into the app’s Rebook confirmation screen with full prefill and attribution parameters intact And if the app is not installed or link association fails, the link falls back to the hosted mobile web confirmation view within 1s, preserving prefill and attribution And no intermediate chooser or error dialog is shown
Desktop Web Confirmation Routing
Given a user clicks a deep link on a desktop browser When the token is valid Then the user is taken directly to the web Rebook confirmation view with all fields prefilled and ready to confirm And the canonical URL reflects the confirmation route without exposing PII And browser back/forward navigation preserves the prefilled state
Attribution Preservation and Analytics
Given a deep link contains attribution parameters (e.g., utm_source, utm_medium, utm_campaign, message_id, channel) When the link is opened and the user confirms the rebook Then the parameters are propagated to analytics events (deep_link_open, rebook_confirmed) and stored on the new booking record And parameters survive app routing and web redirects without duplication or loss And personally identifiable data is not logged in client‑side analytics payloads And attribution is correctly associated with the tenant and instructor
Graceful Degradation and Prefill Rehydration
Given a deep link is invalid, expired, already used, or the booking context is stale (class cancelled/full, time changed, price updated, payment method invalid) When the user opens the link Then the user is shown a clear message and, if needed, a login prompt to continue And upon successful authentication or resolution, the system rehydrates the prefill with the most current equivalent options (next available time, waitlist option, updated price) and preserved attendees and payment details And if no equivalent class is available, the user is offered alternatives (waitlist or browse schedule) without blocking And no sensitive token data is exposed in client/UI error messages And a structured error/diagnostic event is emitted for observability
Admin Controls & Eligibility Rules
"As a studio owner, I want to control where Rebook appears and who can use it so that it aligns with my business rules and branding."
Description

Provide studio-level configuration for Turbo Rebook: enable/disable feature globally or per class/category, choose surfaces (class page, past bookings, receipts, reminders), customize button label and copy, and set eligibility criteria (e.g., membership types, minimum time since last attendance, blackout windows before class start, cooldown between rebooks). Define behavior for price changes, discount/credit application precedence, seat-hold duration, and whether to allow instructor substitutions. Expose policy previews and validation messages in the dashboard. Include audit logs of configuration changes and a sandbox mode for testing before enabling in production.

Acceptance Criteria
Global, Category, and Class-Level Enablement with Surface Selection and Precedence
1) Given Turbo Rebook is globally enabled, when I disable it for Category A, then the Rebook button is hidden on all selected surfaces for classes in Category A and remains visible for other categories. 2) Given Turbo Rebook is globally disabled, when I explicitly enable it for Class X, then the Rebook button is visible for Class X and remains hidden for all other classes. 3) Given precedence (class overrides category; category overrides global), when conflicting toggles exist, then the most specific level applies and is reflected across all selected surfaces within 1 minute. 4) Given I select surfaces [Past Bookings, Receipts], when I save, then the Rebook button renders only on those surfaces and is absent on [Class Page, Reminders]. 5) Given I save any configuration change, when I open audit logs, then an entry records admin user, timestamp (UTC), changed fields, old→new values, and originating IP.
Eligibility Rules: Membership Types, Cooldowns, Blackout Windows
1) Given allowed membership types are [Gold, Silver], when a Bronze member views a Rebook surface, then the button is disabled and a message states “Not eligible: membership type.” 2) Given minimum time since last attendance is 7 days, when a user attempts to rebook after 3 days, then the action is blocked and the message shows remaining time (e.g., “Eligible in 4 days”). 3) Given a blackout window of 2 hours before class start, when within that window, then Rebook is disabled and displays “Rebooking closes 2h before start.” 4) Given a cooldown between rebooks of 24 hours, when a user rebooks at 10:00, then they cannot rebook the same class again until after 10:00 the next day. 5) Given the dashboard policy preview with a chosen test profile, when I run eligibility preview, then it shows pass/fail with the exact rule(s) causing failure and the next eligible time if applicable. 6) Given invalid or conflicting rule entries (e.g., negative cooldown), when I attempt to save, then inline validation messages identify the fields and prevent saving.
Custom Button Label and Copy Propagation
1) Given I set the button label to “Quick Rebook” and helper copy to custom text, when I save, then all selected surfaces display the new label and copy on next page load. 2) Given locales en-US and es-ES have distinct label/copy entries, when I switch profile language, then the correct localized text appears; if missing, it falls back to the default locale. 3) Given I click “Reset to default,” when I save, then system default texts are restored across all surfaces.
Price Changes, Discounts, and Credit Precedence
1) Given pricing policy = “Use current price,” when current price ≠ prior booking price, then checkout shows current price and notes the policy in the price details. 2) Given pricing policy = “Honor last paid price,” when the prior price is lower, then checkout reflects the prior price and labels it “Grandfathered price.” 3) Given discount precedence = [Promo Code > Credit Balance > Membership Discount], when all are applicable, then the system applies them in that order with a line-item breakdown. 4) Given I change the precedence order in settings, when I run a preview with a sample booking, then the preview reflects the new order and resulting total. 5) Given a promo code is expired or ineligible, when rebooking, then it is not applied and a message explains the reason.
Seat-Hold Duration and Expiration
1) Given seat-hold duration is set to 2 minutes, when a user taps Rebook, then one seat is held for that class/time for 2 minutes and a countdown is shown. 2) Given the hold expires before payment, when time elapses, then the seat is released, the rebook flow exits to the class page, and the user is informed the hold expired. 3) Given there are no available seats, when a user taps Rebook, then no hold is created and the user is offered to join the waitlist if enabled. 4) Given payment completes within the hold window, when booking confirms, then the hold converts to a confirmed reservation and no extra seats remain held.
Instructor Substitution Policy
1) Given “Allow instructor substitutions” is enabled, when the original instructor is unavailable but the same timeslot has Instructor B, then Rebook offers that slot with a notice “Instructor may vary.” 2) Given “Allow instructor substitutions” is disabled, when the original instructor is not assigned to the timeslot, then Rebook is blocked for that slot with a message “Instructor mismatch.” 3) Given substitution is allowed, when a substitution occurs post-confirmation, then the attendee receives an updated notification per the studio’s notification settings.
Sandbox Mode Activation and Isolation
1) Given Sandbox Mode is enabled, when viewing consumer surfaces, then Turbo Rebook buttons are visible only to admin/test accounts and hidden from the public. 2) Given Sandbox Mode is enabled, when completing a rebook flow, then no real charges are captured, no production inventory is decremented, no live reminders are sent, and an in-flow banner reads “Sandbox—no live bookings.” 3) Given Sandbox Mode is enabled, when I view audit logs, then entries are tagged “Sandbox” and are segregated from production logs. 4) Given Sandbox Mode is disabled and changes are published, when I confirm, then sandbox test data is retained for reference but not migrated, and production behavior resumes.
Rebook Analytics & Experimentation
"As a product/studio owner, I want visibility into Rebook performance and A/B testing so that I can optimize placement and maximize repeat bookings."
Description

Instrument end-to-end Rebook funnels with events for impressions, clicks, prefill success, confirmation success/failure, waitlist enrollments, time-to-confirm, and revenue/credit consumption. Segment metrics by surface (class page, past bookings, receipt, SMS/email), device, class type, and studio. Provide a dashboard with trends and conversion rates, plus exports to existing analytics sinks (e.g., Segment, GA4) and webhooks for BI tools. Enable A/B testing of CTA placement, copy, and default behaviors (e.g., instructor substitution allowed) with statistically sound sampling and guardrails. Use privacy-safe aggregation and respect user consent preferences.

Acceptance Criteria
Event Instrumentation: End‑to‑End Rebook Funnel
Given a user views any Turbo Rebook surface When the page or message is rendered Then an impression event is fired with surface, device_type, class_id, studio_id, instructor_id (if applicable), pseudonymous user_id, session_id, impression_id, and occurred_at timestamp Given the Rebook CTA is visible When the user taps or clicks it Then a click event is fired with event_id, impression_id correlation, and the same context fields Given a previous booking exists When the Rebook flow loads Then a prefill_success event is fired if class time, instructor, attendees, and payment method are pre-populated; Else a prefill_failure event is fired with failure_reason codes (e.g., CLASS_TIME_UNAVAILABLE, PAYMENT_METHOD_MISSING) Given the user attempts to confirm When payment authorization completes Then a confirm_success event is fired with booking_id, amount, currency, payment_method_type or credits_used, time_to_confirm_ms, experiment_variant, and surface Given the class is full When the user opts for waitlist Then a waitlist_enroll event is fired with waitlist_position and notification_channel Given a confirmation attempt fails When an error occurs Then a confirm_failure event is fired with failure_reason code and retriable flag Given events are emitted When they reach the ingestion pipeline Then they are deduplicated by event_id and ordered by occurred_at within session
Segmentation and Dimensions Coverage
Given any Rebook analytics event is recorded Then it includes required dimensions: surface, device_type, os, browser, locale, class_type, class_id, studio_id, experiment_id, experiment_variant, and session_id Given the analytics dashboard is open When a user applies filters for any combination of surface, device_type, class_type, and studio over a date range Then all charts and tables reflect only matching events and update within 5 seconds p95 Given an incoming event payload is missing a required dimension or has a type mismatch Then the payload is rejected with a schema validation error, logged with contextual metadata, and an alert is raised if rejection rate exceeds 0.5% of hourly volume
Rebook Analytics Dashboard: Trends and Conversion Metrics
Given a date range is selected When viewing the dashboard Then it displays for each surface: impressions, clicks, click-through rate, prefill_success rate, confirmation rate, waitlist enrollments, time_to_confirm p50/p90, revenue, and credits consumed Given segment filters are applied Then metrics recalculate correctly and totals equal the sum of segments without double counting Given a daily granularity is selected Then trend charts render one point per day in the studio’s timezone and tooltips show precise counts and definitions Given a validation query is run against the event store for a random sample of 100 sessions Then dashboard counts match within ±1% absolute for event counts and ±2% for revenue for that sample
Analytics Exports: Segment and GA4
Given exports are enabled for a tenant When events are generated Then they are forwarded to Segment as track calls with schema v1 and to GA4 with event names: rebook_impression, rebook_click, rebook_prefill_success, rebook_confirm_success, rebook_confirm_failure, rebook_waitlist_enroll, carrying mapped parameters for dimensions and revenue/credits Given normal operation Then ≥99% of exported events are delivered to each destination within 5 minutes p95, with retry and exponential backoff up to 24 hours on failure Given a destination outage occurs Then events are queued durably and delivered upon recovery without duplication, using event_id for idempotency; export lag is visible on an ops dashboard Given exports are toggled OFF for a tenant Then no events are sent to that destination while internal storage and dashboard remain unaffected
Webhooks for BI Tools
Given a studio registers a webhook endpoint and secret When analytics events are available Then the system POSTs batched JSON payloads (max 500 events or 1 MB per batch) to the endpoint with an HMAC-SHA256 signature header over the raw body Given delivery failures (HTTP 5xx, timeouts, or network errors) Then the system retries with exponential backoff for up to 72 hours; batches that still fail are moved to a dead-letter queue with observable metrics and alerts Given a replay request is made via admin API with batch_id or date range Then the system re-sends the requested batches preserving original occurred_at and event_id, and marks them as replays in headers Given privacy constraints Then webhook payloads exclude raw PII (no email, phone, or full name); user_id and device identifiers are pseudonymous
A/B Testing and Guardrails for Rebook CTA
Given eligible traffic for a defined experiment (CTA placement, copy, or default behaviors) When users enter the experiment Then assignment uses stratified randomization by studio and surface with sticky bucketing by user_id; default allocation is configurable (e.g., 50/50) Given the experiment is running Then exposure, click-through, confirmation conversion, time_to_confirm, payment failure rate, and revenue per session are tracked per variant; SRM checks run continuously and alert on p<0.01 Given guardrail conditions are breached Then the platform auto-pauses the experiment if confirm_success rate drops by ≥5% absolute or payment failures increase by ≥3% absolute vs control at p<=0.05 for two consecutive checkpoints Given the experiment concludes Then results include effect sizes with 95% CIs, achieved power (>=0.8), and decision status; winning variant can be promoted and results exported to Segment/GA4 and webhooks
Privacy, Consent, and Aggregation Controls
Given a user has not consented to analytics Then no analytics events are emitted for that user beyond strictly necessary operational events that exclude user_id and PII Given a user updates consent When they opt in or out Then the change takes effect across all surfaces, exports, and webhooks within 5 minutes and is auditable Given analytics data are displayed on the dashboard Then only aggregated metrics are shown and k-anonymity is enforced (k>=10) for any segmented breakdown to prevent re-identification Given a verified data deletion request Then all associated pseudonymous analytics for that user are deleted from internal stores and redacted from downstream exports within 30 days, with deletion status logged

Wallet Hand-Off

Deep links from SMS/email or web jump straight into the native Apple Pay/Google Pay sheet on trusted devices—no login or card entry required. The familiar wallet flow increases trust, slashes typing on mobile, and lifts completion rates for on-the-go bookings.

Requirements

Signed Deep Link Payloads
"As a mobile customer, I want a secure booking link that carries my class and price details so that I can pay instantly without re-entering information."
Description

Generate short, white-label deep links in SMS/email that encapsulate booking intent (class/session identifiers, start time, seat count, price, currency, tax, discount, tenant brand, locale, and return/cancel URLs) as a compact URL-safe signed token (e.g., JWS). Include issued-at, not-before, and expiration claims; per-tenant key management and rotation (JWKs); and tamper detection server-side before invoking wallet. Links must be unique, traceable, and compatible with iOS/Android default browsers, and support UTM parameters for attribution without leaking PII.

Acceptance Criteria
JWS Token Structure and Claims Validity
Given a deep link containing a URL-safe signed token When the token header and payload are decoded Then header.alg is one of the tenant-approved algorithms (RS256 or ES256) And header.kid references an active tenant key And payload includes jti, tenant_id, class_id or session_id, start_time (RFC 3339), seat_count (>=1), amount, currency (ISO 4217 uppercase), tax_amount, discount_amount, locale (BCP 47), return_url, cancel_url And payload includes iat, nbf, exp where exp - iat <= 30 minutes and nbf <= iat + 2 minutes And return_url and cancel_url use https and match tenant-allowed domains And payload and URL contain no PII (e.g., name, email, phone)
Per-Tenant JWK Key Management and Rotation
Given each tenant exposes a JWKS with unique kids for signing keys When verifying a token signed with the active key Then signature verification succeeds and the used kid is logged When verifying a token signed with a rotated-out key within a defined grace window Then verification succeeds; after the grace window elapses, verification fails with an explicit error When a kid is not found in cache Then JWKS is refreshed once before failing verification Then key rotation events and verification results are audit-logged with tenant_id and kid
Server-Side Tamper Detection and Replay Prevention
Given a token with any modified header, payload, or signature When verification is attempted Then the request is rejected with 401 invalid_token and wallet invocation is not started Given a previously redeemed token (jti) When the same token is presented again Then it is rejected as a replay and no new booking or payment is initiated Given repeated invalid verifications from the same IP/device beyond threshold N within T minutes Then rate limiting is applied and events are logged with correlation_id and reason
Unique, Traceable Links with UTM Attribution (No PII)
Given a deep link is generated for a booking intent Then it includes a unique jti and a click_id to support attribution and deduplication When the link is opened Then a click event is recorded with timestamp, tenant_id, jti, click_id, user_agent, and any UTM parameters present Then UTM parameters are not part of the signed payload and are preserved end-to-end And neither the token nor URL query contains PII
Cross-Platform Wallet Invocation and Fallback
Given a supported device with Apple Pay or Google Pay configured When the deep link is opened in the default browser (iOS Safari/Chrome, Android Chrome) Then the native wallet sheet appears within 1.5 seconds without login or card entry When the device lacks wallet capability or trust Then the user is redirected to a secure web checkout fallback within 2 seconds Then on payment success the user is redirected to return_url and on cancel to cancel_url And a booking hold is established during the wallet session to prevent double-booking
Compact URL-Safe Encoding and White-Label Compatibility
Given a generated deep link Then the URL uses the tenant’s white-label HTTPS domain and base64url encoding without padding for the token And the total URL length is <= 700 characters to ensure SMS/email compatibility And the link renders and opens correctly from default SMS and major email clients on iOS and Android without line-breaking issues And the link remains valid through permitted link-shortening without breaking signature verification
Price, Tax, Discount, and Currency Integrity
Given payload includes amount, tax_amount, discount_amount, currency, and seat_count When the server validates the payload Then all monetary fields are non-negative and conform to currency minor units (e.g., 2 decimals) And currency is ISO 4217 uppercase and allowed by the tenant And computed_total = amount*seat_count + tax_amount - discount_amount; if |payload_total - computed_total| > 1 minor unit, reject with 422 And any referenced discount is active, tenant-owned, and applicable to the class/session before wallet invocation
Wallet Eligibility & Device Trust Check
"As a customer on my phone, I want the system to detect if my device can pay with Apple Pay or Google Pay so that I only see checkout options that work for me right now."
Description

On link open, perform a fast, privacy-preserving preflight to detect Apple Pay/Google Pay availability and merchant eligibility for the current browser, OS, region, and network. Validate that the tenant’s brand/domain is wallet-verified, the amount/currency is supported, and required contact fields are available or requested minimally. Provide immediate branching to wallet sheet where eligible, otherwise route to graceful fallback without exposing technical errors to the user.

Acceptance Criteria
iOS Safari Apple Pay Eligible Handoff
Given a user opens a Wallet Hand-Off link on iOS 16+ Safari with Apple Pay set up and device biometrics enabled And the tenant domain is wallet-verified and merchant is eligible for Apple Pay in the user’s region And the amount and currency are supported by the merchant and Apple Pay When the link loads over HTTPS and the preflight runs Then the preflight completes within 300 ms at p95 without transmitting PII And the native Apple Pay sheet is presented within 1 second at p95 And the user is not prompted to log in or enter card details And the merchant display name matches the tenant brand and the exact amount and currency are shown
Android Chrome Google Pay Eligible Handoff
Given a user opens a Wallet Hand-Off link on Android 11+ in Chrome with Google Pay set up and device lock enabled And the tenant merchant is eligible for Google Pay in the user’s region And the amount and currency are supported by the merchant and Google Pay When the link loads over HTTPS and the preflight runs Then the preflight completes within 300 ms at p95 without transmitting PII And the native Google Pay sheet is presented within 1 second at p95 And the user is not prompted to log in or enter card details And the merchant display name matches the tenant brand and the exact amount and currency are shown
Unsupported Environment Graceful Fallback
Given a user opens a Wallet Hand-Off link on a browser/OS without Apple Pay or Google Pay support, or without a configured wallet, or in a non-secure context When the preflight detects ineligibility for wallet handoff Then the user is routed to the branded fallback checkout within 1 second at p95 And no technical error codes or stack traces are displayed to the user And a generic, friendly message offers alternative payment methods And any previously supplied contact fields are prefilled when available And wallet invocation is not retried more than once per session unless the environment changes
Merchant Domain and Brand Verification Gate
Given the tenant brand and booking domain are included with the payment intent When the preflight validates the domain-brand-to-merchant mapping against the payment provider’s wallet verification Then Apple Pay/Google Pay is offered only if the mapping is verified and active And on verification failure, the user is routed to the fallback without exposing verification details And an internal, non-PII error code is recorded for observability
Amount and Currency Support Validation
Given the payment intent includes amount and currency When the preflight checks currency support and min/max limits for the wallet network and merchant Then eligibility is true only if currency is supported and amount is within allowed bounds and correctly rounded to currency minor units And on unsupported currency or amount, route to fallback with generic copy and do not attempt wallet invocation
Contact Fields Availability and Minimal Prompting
Given the class requires specific contact fields (e.g., email; phone optional) And contact data may be available from a signed link token or an existing session/profile When the preflight runs Then if all required contact fields are available, the wallet sheet is invoked without additional prompts And if any required field is missing, display a single lightweight prompt that collects only the missing required fields (0–2 inputs max) And no optional fields are requested before wallet And time-to-wallet, including the prompt, remains under 2 seconds at p95
Privacy-Preserving Preflight and Data Minimization
Given a preflight is executed to determine wallet eligibility When collecting signals for browser/OS, wallet capability, region, network security, and merchant status Then no PII (email, phone, card details) or persistent device identifiers are transmitted or stored And logs contain only anonymized eligibility outcomes and internal error codes And all network requests occur over HTTPS and transfer less than 100 KB total at p95
One-Tap Wallet Sheet Invocation
"As a busy attendee, I want the wallet sheet to open immediately with the class and total prefilled so that I can complete payment in a single tap."
Description

Invoke the native Apple Pay or Google Pay sheet directly from the deep link with prefilled merchant name (tenant brand), line item (class name and time), total, currency, and minimal contact fields. Use ApplePay JS or Payment Request API with wallet-specific configs to present the familiar native sheet without login or card entry. Ensure fast-first-paint, accessibility, and clear cancel/timeout handling, and emit client telemetry for sheet shown, authorized, canceled, and error events.

Acceptance Criteria
One-Tap Wallet Sheet on Trusted Device
Given a user taps a valid Wallet Hand-Off deep link from SMS/email on a device with an enrolled wallet (Apple Pay on Safari iOS/macOS or Google Pay on Chrome/Android) and a trusted browser profile And the deep link payload includes tenant brand, class id, class name, class start datetime (UTC), total amount, currency (ISO 4217), and a short-lived booking token When the page loads and verifies the token with the server Then the native wallet sheet is invoked using ApplePay JS or the Payment Request API without requiring login or manual card entry And the merchant display name equals the tenant brand exactly And the line item shows "<Class Name> — <localized start date/time>" And the total equals the provided amount in the provided currency And no more than 2 contact fields are requested (email and/or phone), and no postal address is requested
Unsupported/Untrusted Device Fallback
Given a device lacks a supported/enrolled wallet or the device/browser is not trusted, or token verification fails When the deep link is opened Then the native wallet sheet is not invoked And a secure fallback checkout is rendered within 1500 ms of First Contentful Paint And a clear message explains the fallback without exposing sensitive details And telemetry event "wallet_error" is emitted with error_code in {unsupported_device, no_enrolled_wallet, untrusted_context, token_invalid}
Cancel and Timeout Behavior
Given the wallet sheet is shown When the user cancels explicitly or there is no interaction for 60 seconds Then the sheet dismisses and the user returns to the booking screen with all prefilled details intact And any seat hold is released within 5 seconds and no payment is captured And a non-intrusive banner states "Payment canceled" or "Payment timed out" And telemetry event "wallet_canceled" or "wallet_timeout" is emitted with reason and latency_ms
Successful Authorization and Booking Creation
Given the user authorizes payment in the native wallet sheet When the payment token is received client-side Then the token is sent to the server over TLS with an idempotency key unique to the deep link And exactly one booking is created and confirmed, seat inventory is decremented, and double-booking is prevented And a confirmation screen is shown within 2 seconds of server approval with class details and receipt summary And confirmation SMS/email are queued within 5 seconds And telemetry event "wallet_authorized" is emitted with transaction_id, amount, currency, and latency_ms
Performance: Fast First Paint and Sheet Invocation
Given a standard mobile network profile (RTT 150–300 ms, 1–2 Mbps) and a mid-tier device When the deep link is opened Then First Contentful Paint occurs within 1200 ms And the wallet sheet is presented within 700 ms after FCP on supported devices And total JS downloaded before sheet invocation is ≤ 150 KB (gzip), with no render-blocking third-party scripts And requests for wallet config are parallelized and cached with max-age ≥ 5 minutes
Accessibility and Localization
Given users relying on assistive technologies or diverse locales When the wallet sheet is invoked and any supporting UI is displayed Then focus is managed so assistive tech announces the sheet and context change And all interactive elements meet contrast ratio ≥ 4.5:1 and have accessible names And class date/time and currency are formatted in the user's locale and timezone, with RTL layouts supported And keyboard activation and screen-reader actions can trigger the same flow as tap
Telemetry Events and Reliability
Given any wallet flow outcome (shown, authorized, canceled, error) When the event occurs Then a telemetry event in {wallet_sheet_shown, wallet_authorized, wallet_canceled, wallet_error} is emitted with fields: correlation_id, tenant_id, class_id, wallet_type, device_type, amount, currency, latency_ms, error_code (if any) And events are sent using sendBeacon or equivalent to survive tab close, with ≥ 99% delivery within the session under normal network conditions And no PII (full card data, email content, phone number) is included; consent and regional privacy flags are respected And event payloads conform to a versioned schema; rejected events are counted locally for diagnostics
PSP Tokenization & Merchant Routing
"As a studio owner, I want wallet payments to process through my configured gateway and merchant accounts so that funds settle to my business without extra steps."
Description

Integrate Apple Pay/Google Pay payment tokens with the configured payment service provider per tenant (e.g., Stripe, Adyen, Braintree), including gateway merchant IDs, domain verification for Apple Pay on white-label subdomains, and Google Pay gateway configuration. Validate cryptograms, handle network tokenization, and apply SCA/3DS logic according to region and gateway rules while maintaining PCI boundaries. Map processor responses to unified success/failure codes and capture/authorize per business rules.

Acceptance Criteria
Apple Pay Domain Verification on White‑Label Subdomains
Given a tenant’s white‑label subdomain is configured with an Apple Pay merchant ID, When https://{subdomain}/.well-known/apple-developer-merchantid-domain-association is requested, Then the exact association file is served with HTTP 200 and content-type text/plain Given Apple domain verification is initiated for the subdomain, When Apple’s verification API runs, Then verification succeeds and the status is stored as Verified with timestamp in tenant settings Given the association file is missing or incorrect, When verification runs, Then the status is stored as Failed, Apple Pay presentation is suppressed on that domain, and an audit log + alert is emitted
Google Pay Gateway Tokenization Configuration per Tenant
Given a tenant has Google Pay enabled and a PSP (Stripe|Adyen|Braintree) selected, When generating the Google Pay paymentDataRequest, Then tokenizationSpecification.type is PAYMENT_GATEWAY, parameters.gateway matches the PSP adapter id, and gatewayMerchantId/merchantId equals the tenant’s configured MID Given the environment is set to test, When the paymentDataRequest is produced, Then environment=TEST and allowedCardNetworks/authMethods match tenant policy Given tokenization parameters are incomplete or invalid, When the payment sheet is requested, Then the API returns a configuration error and the UI disables Google Pay for that request while emitting an audit log
Merchant Routing and MID Selection by Region/Currency
Given a tenant has routing rules by region and currency, When a wallet payment is initiated for currency=C and region=R, Then the request is sent to the configured PSP and merchant account id matching C and R Given no matching merchant route exists, When the payment is initiated, Then the transaction is blocked pre‑gateway with unified code CONFIGURATION_ERROR and a clear operator message Given concurrent requests for the same tenant, When routing occurs, Then the selected PSP and MID are deterministic and recorded in payment metadata and audit logs
Tokenization and Cryptogram Validation via PSP (Apple Pay and Google Pay)
Given an Apple Pay token is received from a trusted device, When the payment is processed, Then the token payload is forwarded unmodified to the PSP’s Apple Pay endpoint and the cryptogram is validated by the PSP; if invalid, the unified code is DECLINED_SCA_INVALID_CRYPTOGRAM Given a Google Pay tokenized card is received, When the payment is processed via the configured PSP gateway, Then network tokenization is honored and processing uses the token without requesting PAN Given payment processing and logging are executed, When token payloads pass through the system, Then no PAN, full token payload, or cryptogram is persisted or logged; sensitive fields are redacted, and only masked card data, network, and last4 are stored
SCA/3DS Application Rules for Wallet Tokens
Given an EEA transaction using Apple Pay or Google Pay network tokens, When the PSP returns ECI/cryptogram indicating SCA performed, Then the payment is marked SCA_SATISFIED and no additional 3DS challenge is triggered Given the PSP returns a soft decline requiring authentication (e.g., authentication_required), When mapping the response, Then unified outcome is REQUIRES_ACTION and the client is directed to complete the PSP‑native challenge flow (if applicable) Given a non‑EEA transaction or an allowed exemption per tenant policy, When rules are evaluated, Then exemption indicators are set on the authorization request and frictionless outcomes are accepted with reason recorded
Unified Processor Response Mapping
Given a PSP response is received, When mapping to platform outcomes, Then the result is one of {APPROVED, DECLINED, REQUIRES_ACTION, FRAUD_REJECTED, NETWORK_ERROR, GATEWAY_ERROR, TIMEOUT, CONFIGURATION_ERROR} with normalized reason codes Given an APPROVED authorization, When storing the result, Then authorizationId, pspTransactionId, networkReference, and PSP name are persisted; for failures, the raw PSP code/message are captured in support metadata Given an unmapped/unknown PSP code is encountered, When mapping occurs, Then outcome is GATEWAY_ERROR with the raw code preserved for diagnostics and a runbook link included in metadata
Authorization vs Capture per Business Rules with Idempotency
Given tenant settings capture_mode=deferred and capture_offset_minutes=N, When a booking is created for a class starting in more than N minutes, Then an authorization is created and a capture job is scheduled for start_time−N minutes Given capture_mode=immediate or the class starts in ≤N minutes, When the authorization is approved, Then capture is executed immediately in the same flow Given a cancellation occurs before capture and before authorization expiry, When the system processes the cancel, Then the authorization is voided and no capture is attempted; if already captured, a refund is initiated per policy Given transient errors or retries, When the same booking payment is re‑submitted, Then an idempotency key (tenantId+bookingId) ensures exactly one authorization/capture at the PSP
Idempotent Booking & Capacity Lock
"As an attendee, I want my seat confirmed exactly once when I pay so that I’m not double-charged or left unsure about my reservation."
Description

Create or reserve the booking atomically with payment using idempotency keys derived from link and wallet token data to prevent double charges and double-bookings. Place a short-lived capacity hold when the wallet sheet is presented; convert to a confirmed seat on success or release on cancel/timeout. If capacity is exhausted mid-flow, offer automatic waitlist enrollment with clear messaging, and ensure refunds or voids are issued immediately for declined or late-conflict scenarios.

Acceptance Criteria
Idempotent Booking on Repeated Wallet Submissions
Given a booking deep link contains nonce NLK for class_id C and the user is on a trusted device with wallet token W And an idempotency window of 5 minutes is active for (NLK, W, C) When the user submits the wallet sheet multiple times or the client retries due to network errors within the window Then the API returns HTTP 200 with the same booking_id and payment_id on each retry And exactly one booking record exists for class C for the customer And exactly one successful charge is captured And available capacity is decremented only once And subsequent retries after a successful booking do not create additional holds
Capacity Hold on Wallet Sheet Presentation
Given class C has N remaining seats When the wallet sheet is presented for a deep link for class C Then a capacity hold of 1 seat is recorded within 1 second And public capacity displays N-1 across all channels within 2 seconds And the hold TTL is 2 minutes And no more holds are accepted once remaining capacity is 0, returning a "class temporarily full" response within 500 ms
Hold Release on Cancel or Timeout
Given a capacity hold exists for class C for session S When the user cancels the wallet sheet or no payment authorization occurs within 2 minutes Then the hold is released within 5 seconds And public capacity reflects the release within 2 seconds And no authorization or charge remains on the payment method And an audit event "hold_released" is recorded with reason "cancel" or "timeout"
Conversion to Confirmed Booking on Payment Success
Given a capacity hold exists for class C for user U When the wallet payment is authorized and captured successfully Then the hold converts atomically to a confirmed booking in a single transaction And capacity remains decremented by 1 And the booking status is "confirmed" and payment status is "captured" And the confirmation page renders within 2 seconds and SMS/email dispatch within 60 seconds And the idempotency key returns the same booking if re-invoked within 5 minutes
Capacity Exhausted Mid-Flow -> Waitlist Enrollment
Given class C reaches 0 seats available while user U is in wallet flow without a confirmed booking When U attempts to authorize payment or a conflict is detected prior to capture Then U is shown "Class full — join waitlist?" with a single-tap option And if U accepts, U is added to the waitlist within 2 seconds and no charge is captured And any pending authorization is voided within 60 seconds and the API responds with status "voided_conflict" And if U declines, the flow ends with no hold and no charge
Immediate Void/Refund on Decline or Late Conflict
Given a payment authorization exists for booking attempt B for class C When the processor declines the payment or a capacity conflict is detected after capture Then any authorization is voided or captured charge is refunded immediately And the void/refund is initiated within 5 seconds and recorded with reason "decline" or "late_conflict" And the customer sees "Payment reversed — no seat assigned" and is offered waitlist And no booking remains in a confirmed state for B
Cross-Device/Multiple-Channel Idempotency
Given the same deep link nonce NLK for class_id C is opened on two devices using the same wallet account within 5 minutes When both devices submit payment via the wallet sheet Then only one confirmed booking and one captured charge result And the losing attempt returns HTTP 200 with the winning booking_id if the winner succeeds first while capacity remains And if capacity is 0 at the time of the losing attempt, it returns a "conflict_waitlist_offer" without any charge And no duplicate notifications are sent to the customer
Single-Use Link Expiry & Replay Protection
"As a customer, I want my payment link to be safe and single-use so that no one else can use it or charge me by mistake."
Description

Enforce single-use, time-bound deep links with server-side nonce tracking to prevent replay and phishing. After successful payment or explicit cancel, mark the token as consumed and require regeneration for reuse. Provide clear user messaging for expired/consumed links and a safe path to reissue a fresh link, preserving attribution and pricing rules without re-entering data.

Acceptance Criteria
Token Issuance with TTL and Server-Side Nonce
Given a booking invitation requires a wallet deep link When the system generates the link Then the server creates a token with status ACTIVE, a cryptographically secure nonce, a signed payload, and an expires_at of now + configurable TTL (default 15 minutes) And the token record persists booking_id, recipient_id, channel, attribution, and price_snapshot_id And the deep link contains only the token and signature (no PII) And links cannot be generated without a valid booking context and pricing snapshot
First Use: Valid Link Opens Wallet and Atomically Consumes Token
Given a token is ACTIVE and not expired When the recipient taps the deep link on a device with Apple Pay/Google Pay available Then the backend validates signature, TTL, and recipient/channel binding and transitions the token to IN_PROGRESS with an idempotency key And a single payment intent is created and the native wallet sheet opens with correct merchant, amount, and descriptor And upon payment success, the token transitions IN_PROGRESS -> CONSUMED atomically And any subsequent attempt with the same token is blocked per consumed-link rules
Expired Link: Clear Messaging and One-Tap Safe Reissue
Given a token is EXPIRED (now > expires_at) When the deep link is opened Then the wallet sheet is not launched and the user sees an "Link expired" message with error code LINK_EXPIRED and HTTP 410 (or platform equivalent) And a prominent "Get a new link" action is provided And no sensitive PII is displayed; only minimal class details (title, date/time) are shown And the attempt is logged with reason EXPIRED
Consumed Link Replay: Blocked with Regeneration Path
Given a token status is CONSUMED When the link is opened from any device or channel Then the wallet sheet is not launched and the user sees "This link has already been used" with error code LINK_CONSUMED and HTTP 409/410 (or equivalent) And a one-tap "Request new link" action is provided And the event is logged as REPLAY_BLOCKED And rate limiting is applied to repeated attempts from the same IP/device
User Cancel from Wallet: Token Consumed and Reissue Offered
Given a token is IN_PROGRESS and the wallet sheet is shown When the user cancels from the native wallet UI Then the backend marks the token CONSUMED and voids or cancels any pending payment intent And the original link no longer opens the wallet and displays LINK_CONSUMED messaging And the user is offered a "Get a new link" action to reattempt with a fresh token
Reissued Link Preserves Pricing and Attribution Without Re-entry
Given a token is EXPIRED or CONSUMED and the user selects "Get a new link" When the reissue endpoint is called Then a new ACTIVE token is created with a fresh nonce, signature, and TTL And booking_id, recipient_id, attribution (campaign/source/referrer), and price_snapshot_id or pricing rule reference are preserved And any valid promo codes are carried forward; the quoted amount matches the preserved snapshot or recomputed rules as configured And the user is not required to re-enter personal details; the new link deep-launches the wallet sheet And all prior tokens are invalidated
Concurrent Use: Race-Proof Token Consumption
Given the same ACTIVE token is invoked concurrently by multiple taps/devices within TTL When validation and state transition occur Then exactly one request succeeds to transition the token to IN_PROGRESS and create a single payment intent (idempotent by key) And other concurrent requests receive LINK_IN_PROGRESS with HTTP 423/409 (or equivalent) and do not open the wallet And after a terminal outcome (success or cancel), all further attempts receive LINK_CONSUMED messaging And no duplicate charges or duplicate wallet sheets are produced
Fallback Web Checkout & Error Recovery
"As a customer on an unsupported device, I want a quick fallback checkout with my details prefilled so that I can still book without starting over."
Description

When wallet is unavailable or fails preflight, route to the branded web checkout with all details prefilled from the deep link, preserving tracking and preventing data loss. Provide resilient error states for network issues, gateway declines, and domain verification problems, with contextual retry, alternative methods, and support escalation. Log structured errors and correlate with link and tenant IDs for rapid troubleshooting.

Acceptance Criteria
Fallback to Prefilled Web Checkout When Wallet Unavailable
Given a user opens a ClassTap deep link on a device without a supported wallet or with wallet disabled When the wallet availability preflight reports unavailable Then the user is routed within 1.5 seconds to the tenant-branded web checkout And class, timeslot, price, taxes/fees, promo, and attendee name/email/phone from the deep link are prefilled And the user is not required to log in to proceed And an analytics event checkout_fallback_initiated is recorded with link_id and tenant_id And no data is lost across the transition
Wallet Preflight Failure Auto-Recovery
Given the wallet sheet launch is requested from a deep link and preflight returns an error (e.g., domain_not_verified, merchant_capability_missing, unsupported_device) When the preflight error is received Then the native wallet sheet is not presented And the user is routed to the branded web checkout with a non-blocking banner explaining wallet unavailability and suggesting alternative methods And the banner includes a Try Wallet Again action when the condition may be transient And the fallback action is idempotent and preserves the same checkout_session_id And the error is logged with structured fields tenant_id, link_id, checkout_session_id, error_code, error_source=wallet_preflight, severity=warn
Network Error Recovery During Web Checkout
Given the user is on the fallback web checkout with prefilled details When a network timeout or offline condition occurs during payment initiation Then the UI shows a recoverable error state with Retry and Choose Another Method actions And tapping Retry reattempts the same checkout_session_id up to 3 times with exponential backoff starting at 1s And the form state and selection remain intact after each attempt And after 3 failed attempts, a Contact Support link opens the tenant's configured support channel And all attempts and outcomes are logged with error_code, attempt_number, and network_status
Gateway Decline Handling Without Double-Booking
Given the payment gateway returns a decline for the checkout_session_id (e.g., card_declined, insufficient_funds, do_not_honor) When the decline is received Then no booking is created and no seat is reserved And the UI displays a clear decline message mapped from the gateway without exposing sensitive codes And the user can switch to an alternative enabled payment method (e.g., manual card entry) without re-entering attendee details And a subsequent successful payment results in exactly one booking and one charge, enforced via idempotency keys And the decline event is logged with tenant_id, link_id, checkout_session_id, gateway_code, mapped_reason
Domain Verification Failure Degrades Gracefully
Given Apple Pay or Google Pay domain verification fails for the tenant domain during preflight When a user opens a deep link that would normally invoke the wallet flow Then the system immediately routes to the branded web checkout and suppresses any wallet UI And a notice informs the user that wallet is temporarily unavailable and recommends continuing via web checkout And an internal alert is emitted with tenant_id, domain, verification_status, first_seen_timestamp, severity=error And alerts for the same tenant/domain are deduplicated for 1 hour
Deep-Link Metadata and Tracking Preservation
Given a booking is initiated from a deep link containing tracking parameters (utm_source, utm_medium, utm_campaign, gclid, fbclid) and correlation IDs (link_id, tenant_id) When the user is routed to the fallback web checkout and completes payment Then all tracking and correlation parameters persist into the checkout session, booking record, and receipt metadata And the parameters are included in analytics events checkout_fallback_initiated and checkout_completed And the success page URL includes the original tracking parameters for attribution And no additional PII is added to query parameters beyond what was present in the deep link
Structured Error Logging and Cross-Service Correlation
Given any error occurs during wallet preflight, fallback routing, web checkout initialization, or payment processing When the error is handled Then a structured log entry is emitted with fields: timestamp (ISO-8601), tenant_id, link_id, checkout_session_id, user_agent, platform, error_code, error_message, error_source, http_status (if applicable), correlation_id And error logs are queryable within 1 minute in the monitoring system And PII is excluded or masked per policy And traces across wallet and web checkout are linked via correlation_id for end-to-end troubleshooting

FastAuth 3DS

Intelligent authentication that skips CVV/3DS where allowed and triggers step-up only when risk signals require it. Pre-warms network calls to keep taps instant while preserving high approval rates and preventing false declines—speed without sacrificing security.

Requirements

Adaptive Risk Scoring & Exemption Engine
"As a studio owner, I want legitimate payments to be approved without extra friction so that students can book quickly while keeping fraud risk low."
Description

Implement a real-time decision engine that evaluates each payment attempt using device fingerprinting, IP reputation, geolocation, BIN country, historical buyer behavior, booking value, class type, velocity checks, and issuer preferences to determine whether to skip CVV/3DS, request step-up, or block. Support PSD2 SCA exemptions (TRA, low-value, MIT, corporate/commercial) and regional scheme rules, outputting standardized decision codes consumed by checkout and the payment gateway. Enforce a strict latency budget (<50 ms at p95 for the decision), include rules versioning and canary rollout, and ensure privacy by minimizing PII and adhering to PCI scope boundaries. Integrates with ClassTap’s checkout to deliver frictionless approvals while reducing false declines.

Acceptance Criteria
Latency Budget: p95 Decision Under 50 ms
Given the risk engine is deployed in a production-like performance environment with warmed caches and pre-warmed network calls When a 15-minute load test runs at a sustained 200 RPS with a 60/30/10 mix of SKIP/STEP_UP/BLOCK decisions and p50 payload size of 2 KB Then the internal decision_time_ms at p95 is <= 50 ms and at p99 is <= 75 ms and timeouts are <= 0.05% And error_rate (non-2xx) is <= 0.10% during the test window And cold-start after a rolling deploy produces p95 decision_time_ms <= 90 ms for the first 100 requests per instance
PSD2 SCA Exemptions: Selection, Signaling, and Fallback
Given a card issued in the EEA and a merchant/acquirer operating in the EEA When the transaction qualifies for a Low-Value exemption (amount <= 30 EUR) and no issuer preference forbids exemptions Then the engine returns decision_code=SKIP_AUTH and exemption_type=LOW_VALUE and sets the appropriate SCA exemption indicators for the payment gateway Given a transaction eligible under TRA per configured acquirer policy and risk outcome supports TRA When the engine evaluates the payment Then the engine returns decision_code=SKIP_AUTH and exemption_type=TRA and includes reason_codes including TRA_ELIGIBLE Given a transaction classified as MIT or a corporate/commercial card BIN range When the engine evaluates the payment Then the engine returns decision_code=SKIP_AUTH and exemption_type in {MIT, CORP_COMM} as applicable Given an issuer rejects an exemption with a soft decline on the authorization When the checkout retries per gateway guidance Then the engine returns decision_code=STEP_UP_3DS and adds reason_codes including EXEMPTION_REJECTED and SCA_REQUIRED Given a non-EEA region or ineligible scheme context When the engine evaluates the payment Then no PSD2 exemption is set (exemption_type=NONE) and the decision is based on risk and issuer preferences only
Risk Signals-to-Decision Mapping (Skip, Step-Up, Block)
Given a trusted device fingerprint, good IP reputation, geolocation matches BIN country, low booking value, low velocity, and no issuer 3DS requirement When the engine evaluates the payment attempt Then it returns decision_code=SKIP_AUTH with reason_codes including LOW_RISK and signals_used including DEVICE_OK, IP_OK, GEO_MATCH, VELOCITY_OK Given a TOR/VPN or malicious IP reputation, BIN country/geolocation mismatch > 2, high booking value, velocity above configured threshold, or known chargeback device When the engine evaluates the payment attempt Then it returns decision_code=BLOCK with reason_codes including at least one of {IP_REPUTATION_BAD, GEO_BIN_MISMATCH, VELOCITY_HIGH, DEVICE_HIGH_RISK} Given a new device, moderate booking value, medium IP risk, or issuer preference recommending step-up When the engine evaluates the payment attempt Then it returns decision_code=STEP_UP_3DS with reason_codes including MEDIUM_RISK or ISSUER_PREF_STEP_UP Given the same normalized input signals are evaluated multiple times within 5 minutes When the engine processes duplicate requests Then the decision_code and reason_codes are deterministic and identical across evaluations
Standardized Decision Code Contract with Checkout/Gateway
Given the engine returns a response to checkout When the decision is SKIP_AUTH Then the response JSON contains decision_code in {SKIP_AUTH, STEP_UP_3DS, BLOCK}, exemption_type in {NONE, LOW_VALUE, TRA, MIT, CORP_COMM}, reason_codes[], rules_version (semver), correlation_id (UUIDv4), and optional risk_score (0-100) And the checkout suppresses CVV/3DS prompts and proceeds to auth without SCA indicators Given the engine returns STEP_UP_3DS When checkout receives the response Then checkout initiates a 3DS challenge within 1 second and includes SCA indicators to the gateway per scheme format Given the engine returns BLOCK When checkout receives the response Then checkout halts payment submission and surfaces a generic decline message while logging correlation_id and reason_codes Given the gateway receives fields mapped from the engine’s response When the gateway submits authorization Then the exemption/3DS indicators match the decision (as verified in gateway request logs)
Rules Versioning, Canary Release, and Rollback
Given a new rules configuration is published as a higher semantic version When it is promoted to canary at 5% traffic for 10 minutes Then approval_rate delta vs control is within ±2.0 percentage points and decision_time_ms p95 increases by < 10 ms and error_rate does not exceed 0.2% Given canary success criteria are met When traffic is increased to 50% for 10 minutes and then to 100% Then the same thresholds hold and the rollout is marked complete with rules_version updated Given any threshold breach during canary or full rollout When the monitoring alert triggers Then automated rollback to the prior rules_version occurs within 2 minutes and traffic is reverted, with an audit log entry capturing who, what, when, and why Given a merchant or studio requires a pinned rules version When the rules_version override is set for that tenant Then subsequent decisions for that tenant use the pinned version and include it in the response
Privacy, PII Minimization, and PCI Scope Boundaries
Given the risk engine processes a payment evaluation request When inputs are validated Then PAN and CVV are not accepted or stored by the engine; only network tokens, last4, BIN, and non-sensitive metadata are permitted And email, phone, and device identifiers are hashed or tokenized before storage; IP addresses are stored only in truncated or hashed form And logs and traces redact sensitive fields and never include PAN/CVV; debug logs can be enabled without exposing PII Given data retention policies are applied When risk decision artifacts are stored Then PII-minimized artifacts and reason codes are retained for <= 30 days and older records are purged Given a PCI scope review When the engine and data flows are inspected Then the engine is classified out of PCI DSS scope for PAN/CVV storage and adheres to organizational privacy policies
Regional Scheme Rules and Issuer Preference Compliance
Given issuer or BIN-level preferences indicate Requires3DS When the engine evaluates any transaction for that issuer/BIN Then decision_code is STEP_UP_3DS regardless of other low-risk signals Given scheme or regional rules allow CVV/3DS skip for network tokens with strong cryptograms and device binding When the engine evaluates such transactions and no issuer preference forbids skipping Then decision_code is SKIP_AUTH and reason_codes include TOKEN_CRYPTO_OK Given transactions outside EEA (e.g., US, CA) When the engine evaluates PSD2 exemptions Then exemption_type remains NONE and decisioning follows risk signals and regional scheme rules only Given BIN country differs from geolocation by more than the configured threshold and class type is high-risk per configuration When the engine evaluates the payment Then decision_code is STEP_UP_3DS or BLOCK based on configured policy and reason_codes include GEO_BIN_MISMATCH and CLASS_RISK_HIGH
3DS2 Step‑Up Orchestration & Fallback
"As a student paying online, I want a seamless secure challenge only when my bank requires it so that I can complete my booking without confusion or delays."
Description

Provide full EMV 3DS 2.x orchestration for browser and mobile web, including 3DS Method execution, device data collection, frictionless flows, and challenge presentation via secure modal/redirect with graceful timeout handling. Capture and pass ECI/CAVV/DS transaction IDs to the PSP, and automatically fall back to 3DS 1 or decoupled authentication when required by the issuer or region. Ensure PSD2 compliance (EEA/UK) by triggering step-up when exemptions are not applicable, and localize challenge UX and messaging. Maintain accessibility, cross-browser support, and robust error recovery with idempotent submission to avoid double bookings.

Acceptance Criteria
Frictionless 3DS2 with 3DS Method and device data
Given a supported card and issuer that support EMV 3DS 2.x When the shopper submits the payment form and the transaction is assessed as low risk/exempt Then the 3DS Method and device data collection execute and complete, and an ARes is received with transStatus in [Y, A], and the flow remains frictionless without a challenge And the correct scheme-specific ECI is set based on transStatus, and CAVV and DS Transaction ID are captured when transStatus=Y, and all values are forwarded to the PSP in the authorization request And the 3DS Method is attempted within 1000 ms of page render and no visible UI flicker or navigation occurs
3DS2 Challenge via Accessible Localized Modal with Fallback Redirect
Given the issuer requests a challenge (transStatus=C) for a browser/mobile web transaction When step-up is initiated Then a secure modal challenge is presented within 1500 ms with focus trap, keyboard navigation, labeled controls, and screen-reader announcements (WCAG 2.1 AA) And the challenge UI text and ACS frames are localized to the shopper's locale, and numbers/date formats reflect the locale If the modal cannot render or is blocked Then the flow falls back to full-page ACS redirect without data loss On challenge completion Then the authentication result is received (Y/N/U/R), the UI closes, and the payment resumes accordingly; if no completion within 5 minutes, show a timeout message, cancel the challenge, and offer retry or alternative payment
PSD2 SCA Enforcement and Exemptions Handling (EEA/UK)
Given a card issued in EEA/UK and an in-scope e-commerce transaction When no valid exemption applies Then the system requests SCA (3DS2 step-up) and records the SCA reason When an exemption (TRA/LVP/Whitelisting) is requested Then include the correct indicators to the DS/ACS; if the issuer accepts, proceed frictionless; if the issuer rejects (R), trigger a challenge For out-of-scope or exempt flows (e.g., MIT subsequent/3RI, MOTO) Then do not trigger SCA but include the appropriate indicators, and send the correct ECI to the PSP And all SCA decisions and outcomes are logged with timestamps, BIN country, and exemption codes
Automatic Fallback to 3DS1 or Decoupled Authentication
Given 3DS2 is unavailable, unsupported for the BIN, or an error occurs before ARes When authentication is required Then automatically attempt 3DS1 with a compliant redirect/iframe and preserve session context If the ACS indicates decoupled authentication (transStatus=D) Then inform the shopper, poll/await callback for up to 5 minutes, and handle the final result accordingly Post-fallback Then set ECI per scheme rules for the achieved authentication level (authenticated/attempted), include authentication values (e.g., CAVV/XID/DS/3DS1 IDs as applicable), and forward them to the PSP And all fallback decisions and errors are captured with reason codes and retriable vs non-retriable classification
Idempotent Submission and Double-Booking Prevention
Given a shopper refreshes the page, clicks Pay multiple times, or the network triggers retries When the payment is submitted with a stable idempotency key Then only one authorization and one class booking are created And duplicate submissions within a 10-minute window return the same final status and authentication outcome, and the UI displays a single booking confirmation And challenge sessions can be safely resumed after navigation or refresh without restarting the transaction, and partial states are cleaned up after completion And all retries correlate via the idempotency key in logs and audit trail
Cross-Browser and Mobile Web Support for 3DS2 Flows
Given the latest two major versions of Chrome, Safari, Firefox, and Edge on desktop, and Safari (iOS) and Chrome (Android) on mobile When executing 3DS Method, device data collection, frictionless, and challenge modal/redirect Then all flows complete successfully without layout breakage or blocked popups And browsers with ITP/third-party cookie restrictions are handled without requiring third-party cookies; no mixed-content warnings; all communication occurs over HTTPS/TLS 1.2+ And the solution detects unsupported environments and provides a graceful fallback path or clear messaging
CVV/3DS Skip for Tokenized Card‑on‑File
"As a returning student, I want to pay in one tap using my saved card so that checkout is fast and hassle‑free."
Description

Enable CVV and 3DS skipping for returning customers using network tokens or vaulted cards when risk scores and scheme/regional rules allow. Enforce first-transaction SCA/CVV collection, then apply issuer- and scheme-validated exemptions on subsequent transactions based on token trust level, device consistency, and velocity thresholds. Dynamically hide CVV input when skipped, log the exemption rationale for audit, and ensure PCI DSS compliance with secure vaulting and token lifecycle management (provisioning, rotation, de-tokenization).

Acceptance Criteria
First Transaction Requires SCA and CVV
Given a customer without an existing network token or vaulted card on file When they attempt their first payment Then the CVV field is visible and required and a 3DS/SCA flow is initiated per scheme/regional rules And When CVV is missing or the 3DS challenge is not completed Then the authorization is not attempted and the UI displays an actionable error And When the payment is successful Then a network token is provisioned (if supported) and vaulted with token metadata stored and linked to the customer profile
Skip on Trusted Tokenized Return Purchase (Allowed Region)
Given a returning customer with a provisioned network token or vaulted card mapped to the same customer and device and with a risk score below the configured threshold and no velocity rule violations and exemption allowed by issuer/scheme/regional policy When the customer confirms payment Then the CVV input is hidden and not required and no 3DS challenge is initiated And Then the authorization request includes the correct exemption indicator/DS flags per scheme and the CVV value is not transmitted And Then the approval response is received without a step-up requirement and the transaction is marked as "exemption_applied"
Dynamic Fallback to Step‑Up When Issuer Requires Challenge
Given a returning tokenized customer who passed local exemption pre-checks When the issuer responds with a soft decline or step-up required Then the UI reveals the CVV field within 200 ms and initiates a 3DS challenge in-line without page reload and re-submits once with CVV And Then the final authorization either succeeds or fails with a clear user-facing message and no duplicate charges are created And Then the exemption decision log records reason=issuer_required_step_up and links both attempts under one checkout session ID
Risk, Device, and Velocity Gates Prevent Skip
Given a returning tokenized customer When any of the following is true: risk_score >= threshold OR device_fingerprint not consistent beyond tolerance OR more than N successful/failed attempts within 24 hours OR region/scheme disallows exemption for this BIN/MCC Then CVV is required and a 3DS/SCA flow is performed And Then the attempt is tagged as "exemption_blocked" with the failing gate(s) recorded
Exemption Rationale Audit Log and Export
Given any transaction where skip was applied or considered When the payment attempt completes Then an immutable audit record is written containing: transaction_id, customer_id hash, token_id, exemption_type, decision_rule_id, risk_score, model_version, device_id_hash, device_consistency_result, velocity_counters, scheme_indicator, regional_basis, issuer_response_code, timestamp, and actor_id (if manual) And Then audit records are retained for at least 395 days and are exportable in CSV and JSON by admins with permission "payments.audit.read" And Then PII fields exclude full PAN and CVV and all stored values are encrypted at rest
PCI DSS and Token Lifecycle Controls
Given the payments system in production When storing card credentials Then only network tokens or gateway vault references are stored; PAN and CVV are never persisted in ClassTap systems And When token provisioning occurs Then the token is bound to customer, device, and merchant account; rotation occurs on reissuance or per scheme recommendation; deprovision occurs on customer request or inactivity policy; all events are logged And When key management is inspected Then encryption at rest uses AES-256, keys are rotated at least annually, and access is restricted by role with MFA and least privilege And Then an annual PCI DSS assessment or SAQ attestation is available and linked in compliance documentation
Latency and Approval KPIs for Skip vs Step‑Up
Given production traffic with feature enabled at 100% When skip is applied Then click-to-authorization-request median <= 400 ms and p95 <= 800 ms And When step-up is required Then click-to-complete median <= 900 ms and p95 <= 1800 ms And Then approval rate for exempted transactions is within ±1.0 percentage point of the 30-day pre-rollout baseline; otherwise feature auto-rollbacks via a kill switch and alerts are sent to on-call
Pre‑warmed Payment & 3DS Network Calls
"As a student at checkout, I want the pay button to respond instantly so that I feel confident my booking is confirmed without delay."
Description

Reduce perceived latency by preconnecting and prefetching critical endpoints (DNS/TLS preconnect to PSP, DS, and common ACS domains), caching BIN/routing metadata, preloading 3DS Method URLs, and retrieving tokens ahead of button tap under user intent cues. Execute risk prechecks on page load without committing funds, respecting privacy and consent. Target end-to-end frictionless auth decision p95 <300 ms and p99 <500 ms. Implement circuit breakers, backoff, and graceful degradation if pre-warm fails, with parity across desktop and mobile web.

Acceptance Criteria
DNS/TLS Preconnect to PSP, DS, and ACS Domains on Page Load
Given a checkout page is rendered for a supported payment method When the DOM is interactive (DOMContentLoaded) and consent banner has been accepted (where applicable) Then DNS resolution and TLS preconnects are initiated within 100 ms to the configured PSP API, Directory Server (DS), and top ACS/3DS Method hostnames without sending application payloads And p95 time to established TLS session for each target is ≤150 ms on broadband and ≤300 ms on 4G in synthetic tests And ≥99% of eligible sessions attempt preconnects without causing console errors or CORS violations And if a preconnect is blocked by the browser or policy, the system records a non-fatal metric and proceeds without retrying immediately
BIN Metadata Cache Seed and Hit Ratio
Given a user inputs a primary account number and at least the first 8 digits are available When BIN lookup is requested for routing and 3DS decisioning Then the cache returns metadata (scheme, issuer country, product, 3DS exemption eligibility, DS routing) with a p95 response time ≤20 ms in-memory And the BIN cache hit ratio is ≥95% for the top 90% of traffic over a rolling 24h window And cache TTL and SWR are enforced (TTL ≥24h, SWR ≤6h) with automatic background refresh not blocking the UI thread And on cache miss, network fetch completes ≤150 ms p95 without blocking user input; failures fall back to a safe default (no exemptions) and are logged
3DS Method URL Preload and Timeout Handling
Given a 3DS server response includes a threeDSMethodURL for the card range When the payment section becomes visible Then a hidden iframe loads the 3DS Method within 100 ms, with a hard timeout of 10 s and an early-return at 5 s if method completion callback fires And failures or timeouts do not block Pay button interactivity and are flagged to proceed with challenge flow as needed And no personally identifiable information is sent in the method iframe beyond required device data per spec And p95 method completion (or timeout decision) occurs before user tap, with a budget ≤300 ms impact on overall E2E
Risk Precheck on Page Load Without Funds Commitment
Given the checkout page loads and the user has not tapped Pay When risk prechecks execute Then only non-funds-committing calls are made (no authorization, no capture, no hold) and are idempotent And data collection is limited to consented device/risk signals with DPIA controls; if consent is not granted, calls are suppressed And precheck completes p95 ≤200 ms and runs off the main thread without jank (Long Task <50 ms) And risk decisions are cached for the session and invalidated on material changes (amount, BIN, SCA exemption flags)
Token Retrieval Ahead of Tap Under Intent Cues
Given a user demonstrates intent (e.g., focuses card fields, completes CVV, scrolls into Pay button viewport, or hovers/clicks Pay) When prefetch is eligible per risk policy Then a payment token (or client_secret) is requested in advance with p95 latency ≤200 ms and stored securely in memory And tokens are single-use, bound to cart/amount, and have expiry; expired tokens auto-refresh once without UI delay And no funds movement occurs; authorization only initiates after explicit Pay action And concurrent prefetches are coalesced; duplicate requests are deduped via request keying and idempotency keys
Circuit Breakers, Backoff, and Graceful Degradation
Given pre-warm operations experience elevated errors or latency When error rate ≥5% over 1 minute or p95 latency ≥800 ms for a target endpoint Then a circuit breaker trips for that endpoint for 2 minutes, halting pre-warm and switching to on-demand calls And retries use exponential backoff with jitter (base 250 ms, max 4 s, max 3 attempts) without blocking user actions And the UI remains fully functional; only pre-warm benefits are disabled, with clear metrics and logs emitted for observability And recovery automatically closes the breaker when health falls below thresholds for 5 consecutive checks
Mobile/Desktop Parity and Latency Targets
Given a frictionless authentication-eligible transaction When the user taps Pay Then end-to-end decision time (tap to auth decision) meets p95 <300 ms and p99 <500 ms on both desktop and mobile web in real-user monitoring And platform parity gap is ≤10% for p95 between mobile web and desktop across equivalent networks (3G/4G/Wi‑Fi) And if pre-warm is unavailable, the flow still completes with p95 <600 ms using on-demand calls And metrics are segmented by device, network type, and browser, with automated alerts on SLO breach
Soft‑Decline Auto‑Retry & Smart Routing
"As an instructor, I want the system to rescue avoidable declines so that fewer legitimate bookings are lost and my classes stay full."
Description

Automatically recover from soft declines and SCA_required responses by triggering immediate step-up 3DS or retrying the transaction via alternative acquirers/routes when configured. Use idempotency keys to prevent duplicate charges and coordinate with inventory to temporarily hold a class spot during retry. Present a single-tap retry UX with clear status messaging, capture issuer reason codes, enforce retry limits, and log outcomes for analysis. Ensure compliance with scheme retry guidance and avoid cycling doomed transactions.

Acceptance Criteria
Immediate Step-Up on SCA Required or Soft Decline
- Given a payment attempt returns an issuer response indicating SCA required or a soft decline eligible for step-up, When the response is received, Then the UI displays a single-tap "Verify & Pay" within 300 ms and initiates a 3DS2 challenge. - Given the user successfully completes 3DS, When the challenge result is received, Then the system re-attempts authorization within 200 ms using the original idempotency key and no duplicate charge is created. - Given 3DS fails, is abandoned, or times out, When the result is processed, Then the system stops automatic retries and shows a clear failure message with next steps. - Given the step-up flow runs, When events occur, Then audit logs include original decline category/code, 3DS result (frictionless/challenged/outcome), timestamps, and final authorization outcome.
Smart Routing on Soft Decline
- Given a soft decline occurs and issuer indicates retry is allowed without additional authentication, When an alternate acquirer/route is configured and healthy, Then the system retries via the next-ranked route within 1 second while reusing the same idempotency key. - Given multiple routes are available, When selecting a route, Then the router chooses based on configured rank plus recent health signals (last 5-minute success rate and latency) and records the selection rationale. - Given scheme retry guidance requires cooldowns and attempt limits, When applying retries, Then the system enforces configured cooldowns and will not exceed 2 retries within 24 hours for the same transaction. - Given all eligible routes are exhausted or a hard/non-retryable decline code is received, When processing, Then no further retries are attempted and the final failure is surfaced to the user and logs.
Idempotent Retries Prevent Duplicate Charges
- Given the initial authorization is created with an idempotency key, When any retry (3DS step-up or alternate route) is performed, Then the same idempotency key is used across all attempts. - Given concurrent retry triggers (e.g., double-tap or reconnect), When processing, Then only one active retry is allowed; subsequent attempts are deduplicated and receive the same final outcome. - Given the transaction ultimately authorizes, When inspecting payment records, Then exactly one successful authorization/capture exists in the ledger and gateway; no duplicates are created or settled. - Given a prior attempt is in-flight, When a new retry would start, Then the system waits for the in-flight result (up to 30 seconds) and suppresses overlapping retries.
Temporary Seat Hold During Payment Retry
- Given a payment enters the retry flow, When the retry begins, Then the system places a hold on one class seat for that schedule and customer for up to 5 minutes (configurable) and removes it from public availability. - Given the retry succeeds, When authorization is confirmed, Then the seat hold converts to a booking and the waitlist is updated accordingly. - Given the retry fails or times out, When the flow ends, Then the seat hold is released immediately and any waitlisted candidate is notified per notification settings. - Given the user has multiple tabs or devices, When retries are triggered, Then only one seat hold exists per customer per class instance and all sessions reflect the hold status within 2 seconds via real-time updates.
Single-Tap Retry UX with Clear Status Messaging
- Given a soft decline occurs, When presenting the recovery UI, Then the user sees a single primary action labeled "Retry Payment" or "Verify & Pay" (for SCA) without re-entering card details unless the issuer requires re-authentication or data refresh. - Given a retry is in progress, When status changes, Then the UI displays step-wise statuses: "Retrying via bank...", "Verification required" (if 3DS), "Processing...", followed by "Success" or a specific failure message with issuer reason. - Given accessibility requirements, When interacting with the retry UI, Then controls are keyboard accessible, labeled, provide visible focus, announce status changes via ARIA live regions, and meet WCAG 2.1 AA contrast. - Given network variability, When retry starts, Then initial visual feedback appears within 500 ms and the overall operation times out at 60 seconds with a clear fallback message and no duplicate attempts.
Comprehensive Retry Logging and Metrics
- Given any retry attempt occurs, When logging, Then the system records transaction ID, idempotency key, route/acquirer ID, attempt number, issuer response category and reason code, 3DS method/result, end-to-end latency, and outcome. - Given logs are stored, When queried by authorized admins, Then entries are available within 15 minutes and exportable as CSV and JSON. - Given privacy constraints, When persisting logs, Then no PAN, CVV, or full PII is stored; only tokenized identifiers and truncated metadata per policy. - Given analytics dashboards, When viewing FastAuth metrics, Then soft-decline recovery rate, average retry count, approval uplift vs. baseline, per-route success, and 3DS challenge conversion are displayed with filters by date, merchant, class, and BIN.
Retry Limits and Anti-Cycling Enforcement
- Given a decline is received, When evaluating retry eligibility, Then the system consults a configurable allowlist/blocklist of issuer response codes and will not retry on hard declines, suspected fraud, or do-not-retry codes. - Given a transaction is eligible for retry, When performing retries, Then the system enforces a maximum of 2 retries per transaction and a per-card per-merchant cooldown of 30 minutes after two consecutive declines. - Given retries are exhausted or a non-retryable code is received, When finalizing, Then the user is shown a final failure state with guidance to use a different card or contact their bank; no background retries occur. - Given scheme rules or policies change, When configuration is updated, Then new limits take effect immediately without code changes and the update is audit-logged with actor, timestamp, and before/after values.
Merchant Policy Controls & Safeguards
"As a studio owner, I want to adjust authentication aggressiveness and exemptions so that I can balance conversion and risk for my business context."
Description

Provide an admin console for configuring FastAuth policies per merchant/location/region: risk thresholds, allowed exemptions, CVV-skip eligibility, issuer/region allowlists, velocity limits, and blocklists. Include safe defaults, role-based access control, change history, preview/simulation mode, and guardrails that prevent non-compliant configurations (e.g., disabling SCA where mandated). Expose feature flags for gradual rollout and per-segment tuning.

Acceptance Criteria
Scoped Policy Configuration (Merchant/Location/Region)
Given a merchant with locations in multiple regions When an Admin sets merchant-level defaults for risk thresholds, exemptions, CVV-skip eligibility, issuer and region allowlists, velocity limits, and blocklists Then the defaults are persisted as a draft version and are retrievable via UI and API Given a region-level override is created for a specific region When the override is saved Then the effective policy for that region uses the override for specified fields and inherits unspecified fields from the merchant level Given a location-level override exists within that region When conflicting values exist across levels Then effective policy precedence is Location > Region > Merchant and is reflected in the computed policy Given a transaction context (merchant, location, region, issuer) When requesting the effective policy via API Then the API responds within 200 ms with the computed policy and a field-origin map indicating the source level of each field
Safe Defaults and Initialization
Given a newly onboarded merchant with no custom policies When the admin console is opened for FastAuth configuration Then safe defaults are auto-populated: SCA mandated regions enforce step-up, CVV-skip eligibility is disabled unless permitted, velocity limits use system defaults, and allow/block lists are empty Given the merchant operates in an SCA-mandated region When defaults are generated Then exemptions are disabled by default unless low-value/TRA is supported and documented, and 3DS step-up is enabled Given the defaults are displayed When the Admin saves without changes Then the policy is saved as Version 1 (Default) with timestamp and owner and is available for simulation and publication
Role-Based Access Control for FastAuth Policies
Given a Viewer role user When accessing FastAuth policy pages or APIs Then the user can view effective policies, simulations, and audit logs but cannot create, edit, publish, or toggle feature flags (actions result in 403 and audit entries) Given a Manager role user When editing policies Then the user can create and edit drafts and run simulations but cannot publish or modify guardrail settings or feature flags Given an Admin role user When managing policies Then the user can create/edit drafts, publish, configure guardrails, and manage feature flags Given API tokens scoped by role When using the policy management API Then permissions mirror UI capabilities and unauthorized attempts are blocked with 403 and audited
Compliance Guardrails and Validation
Given the merchant includes EEA countries When an Admin attempts to disable SCA or enable CVV-skip where regulations prohibit it Then the system blocks the change, displays a compliance error with citation, and prevents save and publish Given velocity limits outside permissible ranges are entered When saving the policy Then validation fails with specific messages indicating the allowed range and the field is not persisted Given issuer/region allowlists conflict with blocklists When saving Then the system highlights conflicts and requires resolution before publish can proceed Given client-side validation is bypassed via API When submitting a non-compliant policy Then the server rejects with HTTP 400, structured error codes, and an audit log entry is created
Policy Change History and Audit Trail
Given a policy is created, edited, published, or rolled back When the action completes Then an immutable audit entry records actor, role, timestamp, IP, scope (merchant/region/location), action type, optional reason, and before/after field diffs Given audit entries exist When filtering by date range, actor, scope, or action type Then results return within 1 second for up to 10,000 entries and can be exported as CSV Given a specific policy version is selected When requesting a diff against current Then the UI shows field-level differences and the responsible user for each change
Preview/Simulation of Policy Impact
Given at least 30 days of historical data or a connected sandbox When running a simulation for a draft policy over a selected scope and time range Then the system returns projected approval rate, 3DS challenge rate, SCA compliance rate, false decline estimate, and p95 latency impact with confidence intervals Given simulation results are available When viewing breakdowns Then the UI ranks top issuers and regions by impact delta and supports CSV export of results Given simulation mode When executing simulations Then no production traffic, flags, or live policies are altered and the run is logged in the audit trail Given simulation detects non-compliance When attempting to publish the draft Then publishing is blocked with specific reasons linked to the offending settings
Feature Flags and Gradual Rollout Controls
Given a draft policy version When creating a rollout feature flag Then targeting can be configured by merchant, location, region, issuer BIN range, device type, payment method, and risk score band Given a percentage-based rollout plan When setting exposure to 5%, 25%, 50%, or 100% Then traffic is allocated within ±2% of the target and exposure counts are reported in near real time Given monitoring thresholds for approval-rate drop and 3DS-challenge spike are defined When thresholds are breached during rollout Then the system auto-pauses the flag, reverts to last stable policy, sends alerts, and records an audit entry Given an emergency stop is required When the Admin activates the kill switch Then the feature flag disables globally and reverts to baseline within 60 seconds and the action is audited Given rollout status is requested When viewing the flag Then the UI shows current percentage, active segments, start time, last change, and linked policy version
Authentication Observability & A/B Experimentation
"As a product manager, I want deep visibility and safe experimentation with authentication settings so that we can continuously improve conversion without increasing risk."
Description

Deliver real-time dashboards and alerts covering approval rate, challenge rate, completion rate, soft-decline recovery, false-decline indicators, and p95/p99 latency by issuer, BIN, country, device, and class category. Provide experiment tooling to A/B test risk thresholds, pre-warm strategies, and CVV-skip policies with statistically sound analysis and automatic rollback on regression. Support export to BI tools, privacy redaction, and retention policies suitable for compliance audits.

Acceptance Criteria
Real-Time Auth Dashboard Segmented Metrics Accuracy
Given live auth events across issuers, BINs, countries, devices, and class categories with a verified ground-truth aggregate for the selected time window When a user loads the dashboard and applies any combination of filters and time ranges (last 15m/1h/24h/7d/custom) and timezone Then approval rate, challenge rate, completion rate, soft-decline recovery, false-decline indicator rate, and p95/p99 authentication latency match ground truth within ±0.1 percentage points for rates and ±10 ms for latency per visible segment And metrics refresh within 5 seconds of ingestion; late-arriving events are backfilled within 2 minutes and labeled as backfill And zero-data segments render "No data" without affecting totals; missing data is distinguished from zeros And exporting the current view as CSV includes applied filters, time range, and segment columns
Approval/Latency Threshold Alerts with Dedup and Auto-Resolve
Given alert rules configured per metric and dimension (issuer, BIN, country, device, class category) with thresholds and evaluation windows When a rule’s metric breaches its threshold for the full evaluation window (e.g., approval rate drop >2 pp over 15 m; p99 latency > 800 ms over 10 m) Then one alert notification is sent per rule per unique dimension set with deduplication for 30 m, containing metric, dimension values, current value, baseline, threshold, link to dashboard, and incident ID And alerts deliver to configured channels (email, Slack, PagerDuty) within 60 s; PagerDuty uses the active on-call schedule And the alert auto-resolves after the metric remains within threshold for one full evaluation window And scheduled maintenance windows suppress alerts; historical backfills do not trigger alerts
A/B Testing Risk/CVV-Skip/Pre-Warm with Auto-Rollback
Given an experiment configured to A/B test risk thresholds, pre-warm strategy, or CVV-skip policy with randomization at payment-attempt level, 50/50 split, and sticky assignment by device+BIN+issuer When the experiment runs and traffic passes sample-ratio-mismatch checks (chi-square p ≥ 0.01) and meets the pre-specified MDE and power requirements Then the platform computes approval, challenge, completion, soft-decline recovery, false-decline indicator, and p95/p99 latency per arm and per segment with sequential analysis and 95% confidence intervals And if any guardrail is breached (e.g., approval rate −1.0 pp or worse vs control, p95 latency +100 ms or more) for 10 consecutive minutes with significance (p ≤ 0.05), auto-rollback disables the variant within 2 minutes and routes all traffic to control And an immutable experiment report is generated with configuration, timeframe, per-metric outcomes, per-segment deltas, statistical method, and CSV export; configuration is locked post-start And experiment APIs and UI expose assignment logs and support pause/resume; stopping preserves data integrity and sets end time
Secure BI Export (Snowflake/BigQuery/Redshift/S3) with Redaction
Given an authorized user with Export permission and a configured destination (Snowflake share, BigQuery dataset, Redshift schema, or S3 bucket) When the user schedules daily full exports and 15-minute incrementals, or triggers an on-demand export Then datasets deliver within SLA (full ≤ 24 h, incremental ≤ 15 m), are idempotent, and include high-watermarks and schema version And PII is redacted per policy (no CVV; PAN tokenized; BIN+last4 only; email/phone hashed; IP truncated /24 IPv4 or /48 IPv6) before export; data is encrypted in transit (TLS 1.2+) and at rest (AES-256/SSE-KMS) And column definitions and data dictionary are accessible; breaking schema changes are versioned with a 30-day deprecation window And all export actions are logged (who, when, what, destination) and visible in audit trails
Privacy Redaction and Data Retention Controls
Rule: No CVV is stored or exported; full PAN is never stored; only token, BIN, and last4 may be retained Rule: Device identifiers are pseudonymized with rotating salt; IPs are truncated; free-text fields are scanned and redacted for PII before persistence and export Rule: Access to raw authentication events is RBAC-controlled; all reads/writes are audited immutably for 7 years Rule: Default retention is 13 months for auth events and aggregates; per-tenant overrides allowed (3–24 months); deletions propagate to BI exports and backups Rule: Data residency is enforced per tenant (e.g., EU-only storage when selected); backups respect residency and retention Rule: Data subject deletion requests are fulfilled within 30 days; subject access requests are fulfilled within 7 days; completion is logged and independently verifiable
Soft-Decline Recovery and False-Decline Indicators Visibility
Given a dashboard module focused on soft-declines and false-decline indicators When a user selects any issuer, BIN, country, device, class category filter and time range Then soft-decline recovery rate = recovered approvals / soft-declines is displayed per segment with 95% Wilson confidence intervals; target thresholds are configurable for alerts And false-decline indicator rate = declines followed by success within 24 h for the same PAN token and merchant is displayed per segment with 95% confidence intervals And users can drill down to a sample of 20 redacted events per segment; event detail shows reason codes, step-up outcome, timestamps, and excludes PAN/CVV And the metrics API returns identical values to the UI with p95 ≤ 300 ms and p99 ≤ 500 ms response time; metric definitions are linked from the UI

One-Tap Upsells

Offer lightweight add-ons (mat rental, guest pass, class pack) inside the wallet confirmation step. Users toggle extras with a single tap before paying, increasing average order value without adding friction or redirecting away from the flow.

Requirements

Upsell Toggle Panel in Checkout
"As a student booking a class, I want to quickly add optional extras with one tap before I pay so that I can complete checkout faster without leaving the flow."
Description

Present a compact, brandable upsell panel on the final review step immediately before invoking the native wallet sheet (Apple Pay/Google Pay), enabling users to add or remove lightweight extras (e.g., mat rental, guest pass, class pack) with a single tap. The panel updates order totals in real time, shows incremental price deltas, and preserves checkout continuity with no redirects or additional forms. It supports accessibility (WCAG AA), fast load (<150 ms), responsive layouts, and localization. The component must also render in non-wallet checkout flows with identical behavior to maintain a single UX pattern.

Acceptance Criteria
Wallet Checkout: One-Tap Upsell Panel Visibility and Toggle Behavior
Given the user is on the final review step with Apple Pay or Google Pay available When the step is displayed Then a branded upsell panel renders directly above the Pay button before the native wallet sheet is invoked and within 150 ms of step display And each available upsell is shown with a toggle control defaulting to off unless merchant configuration sets a default And upsells that are out of stock or inapplicable are hidden or shown disabled with an explanatory note When the user toggles an upsell on or off Then the selection is applied immediately without opening any new dialogs or forms And the toggle state persists if the user opens and cancels the wallet sheet and returns to the review step And selection states are saved in the session so a refresh restores them
Real-Time Price Update and Incremental Delta Display
Given the upsell panel is visible When the user toggles any upsell Then the order subtotal, taxes/fees (if applicable), and grand total recalculate and visually update within 150 ms And a per-upsell incremental price delta (e.g., +$5.00) is displayed adjacent to each upsell and reflects its on/off state And the aggregated total change equals the sum of selected deltas with no more than ±$0.01 rounding variance When multiple upsells are toggled rapidly Then totals remain accurate and do not display intermediate stale values If an upsell is free due to promotion Then the delta shows +$0.00 and totals remain unchanged If an upsell becomes unavailable at confirmation Then it is removed from the order and the total adjusts with a non-blocking inline notice
Checkout Continuity: No Redirects or Extra Forms
Given the user interacts with the upsell panel on the final review step When toggling upsells Then no navigation, page reload, route change, or modal dialog occurs And no additional input fields are required to add the upsell And the primary Pay button remains visible and enabled (subject to standard validation) When the user taps Pay Then the native wallet sheet opens exactly once with the updated total including selected upsells And browser history is unchanged by upsell interactions (no new entries) And network activity from toggling is limited to background pricing updates without full page refresh
Accessibility: WCAG AA for Upsell Panel
Given a keyboard-only user When tabbing through the review step Then all upsell toggles and the Pay button are reachable in a logical order and operable via Enter/Space And each toggle exposes role "switch" (or native equivalent) with accessible name, on/off state, and price delta announced by screen readers And focus indicators are visible with at least 3:1 contrast; text meets 4.5:1 contrast And total changes are announced via an aria-live polite region within 1 second of a toggle change And there are no keyboard traps; content remains usable at 200% zoom and with prefers-reduced-motion enabled
Responsive Layout and Tap Targets
Given viewport widths from 320 px to 1440+ px and both portrait and landscape orientations When rendering the upsell panel Then content fits without horizontal scrolling; one-column layout is used on narrow viewports and multi-column where space allows And interactive controls meet a minimum 44x44 dp tap target with at least 8 px spacing And the panel respects safe areas/notches and does not overlap the Pay button or fixed elements And toggling items does not cause cumulative layout shift greater than 0.1
Localization and Currency Formatting
Given the checkout locale and currency are set When rendering the upsell panel Then all copy, deltas, and totals display in the selected language and currency format (symbols, separators, precision) And right-to-left locales render mirrored layouts and correct reading order And long translations (P95 length) do not overflow; text wraps or truncates with tooltip without losing critical information When the locale is changed prior to payment Then the panel updates copy and number formats without a page reload
Non-Wallet Checkout Parity
Given the user is in a non-wallet checkout flow (e.g., card form) When the final review step is displayed Then the upsell panel renders with the same layout, copy, toggle behavior, and price update logic as in wallet checkout When toggling upsells Then totals update in real time and the Pay/Submit button amount reflects the new total And no additional steps, redirects, or forms are introduced compared to the wallet flow
Add-on Catalog and Eligibility Rules
"As a studio owner, I want to define which add-ons appear for each class and who can buy them so that offers stay relevant and increase revenue without confusing customers."
Description

Provide an admin-configurable catalog of add-ons scoped by studio, instructor, class, or category, including name, description, icon, price, tax category, currency, inventory linkage, and display order. Support rules for visibility and purchase constraints (e.g., first-time customers only, members only, class-type specific, date/time windows), default toggle state (off/on), single vs multi-quantity, min/max per booking, and mutually exclusive groups. Ensure white‑label text/imagery overrides and allow per-brand copy. Expose configuration via dashboard and API with versioning and audit logs.

Acceptance Criteria
Scoped Add-on Creation and Catalog Fields
Given an admin with permissions is on the brand dashboard add-on creator When they create an add-on with name, description, icon, price, currency, tax category, scope (studio/instructor/class/category), inventory linkage (SKU/stock source), display order, quantity mode (single or multi with min/max), and default toggle state (on/off) Then the system validates required fields and types, showing inline errors for any missing/invalid values without saving And on save the add-on is persisted with a unique ID and current version number And the add-on appears in the catalog list with correct scopes and display order And the public API GET /addons reflects the same fields and values for the chosen scope within 2 seconds
Visibility and Purchase Constraints Enforcement
Given an add-on configured with visibility/purchase rules (first-time customers only, members only, class-type specific, and a date/time window) When a first-time, non-member books a qualifying class within the window Then the add-on is visible and selectable in the one-tap upsell step And when a returning, non-member or a booking outside the window views the same flow Then the add-on is hidden and not selectable And any attempt to attach the add-on via API when rules are not satisfied returns 422 with a rule-violation code and no add-on is attached
Default Toggle State and Quantity Constraints in One-Tap Flow
Given an add-on with default toggle state = on and quantity mode = multi (min=1, max=3) When the wallet confirmation sheet renders Then the add-on appears pre-selected with quantity = 1 and quantity controls limited to 1–3 And increasing/decreasing quantity updates the order total and tax within 200 ms without navigating away And setting quantity outside bounds is prevented in UI and via API (422)
Mutually Exclusive Groups Behavior
Given two add-ons assigned to the same mutually exclusive group When the user selects Add-on A in the one-tap upsell Then Add-on B is automatically deselected/disabled with an explanation message And attempting to select both via API results in 422 with a mutually-exclusive constraint code And removing Add-on A re-enables Add-on B
Inventory Linkage, Reservation, and Stockout Handling
Given an add-on linked to inventory with available quantity = N When M users concurrently attempt to add the add-on where M > N Then no more than N successful confirmations include the add-on, with atomic reservation at payment confirmation And out-of-stock state hides or disables the add-on with a Sold Out label And canceling a booking or removing the add-on restores inventory within 60 seconds
White-Label Per-Brand Copy and Imagery Overrides
Given a tenant with two brands A and B using the same base add-on When the admin overrides name, description, and icon for brand B only Then brand A continues to display base copy/imagery and brand B displays the overrides across dashboard previews and public checkout And prices/tax category remain sourced from the base add-on unless explicitly edited for that brand is supported And API responses include brand-specific fields where overrides exist and base fields otherwise
Dashboard/API Configuration, Versioning, and Audit Logs
Given add-on configurations are edited via dashboard and API When an admin updates fields and saves Then a new version is created with incremented version number and ETag And the audit log records actor, timestamp, changed fields (old→new), and scope And the API requires If-Match with the latest ETag for updates; stale ETags return 412 without applying changes And audit entries are filterable and exportable (CSV) by actor, date range, and scope
Real-time Pricing, Tax, and Discount Calculation
"As a buyer, I want the total to update immediately when I add or remove extras so that I know exactly what I will be charged before I confirm payment."
Description

When an add-on is toggled, recalculate totals instantly, including itemized subtotals, taxes by jurisdiction, fees, and discounts, and display the incremental change. Respect coupon rules (e.g., applies to class only, excludes add-ons), handle tax-inclusive/exclusive pricing, multi-currency rounding, and class pack proration where applicable. Expose a consistent price breakdown to the wallet payment sheet and receipts, ensuring parity between displayed and charged amounts to avoid declines.

Acceptance Criteria
Real-Time Add-On Toggle Recalculation and Incremental Delta
Given a visible class price, taxes, fees, and available add-ons, When the user toggles an add-on on or off, Then subtotal, taxes, fees, discounts, and total recalculate and visually update within 500ms without page reload. Given an add-on is toggled, When totals update, Then an incremental change indicator (e.g., +$X.XX or -$X.XX) appears and equals the net difference attributable to that add-on including applicable taxes and fees. Given multiple add-ons are toggled in succession, When totals update after each toggle, Then each intermediate total is correct and the final total equals the sum of the itemized breakdown displayed.
Coupon Application Rules: Class-Only vs Excluding Add-Ons
Given a coupon configured to discount the class price only and exclude add-ons and fees, When the coupon is applied and an add-on is toggled, Then the discount applies only to the class line, add-ons and fees remain undiscounted, and the total reflects these rules. Given an invalid or expired coupon is entered, When applied, Then no discount is applied, an inline error is shown, and the totals remain unchanged. Given a fixed-amount coupon larger than the class price, When applied, Then the discount caps at the class amount, the total never goes below zero, and no discount is applied to add-ons or fees.
Jurisdictional Taxes: Tax-Inclusive and Tax-Exclusive
Given tax configuration is tax-exclusive at the shopper's jurisdiction, When an add-on is toggled, Then tax is computed on the taxable base after discounts and proration, displayed as a separate tax line, and total equals subtotal + tax + fees. Given tax configuration is tax-inclusive at the shopper's jurisdiction, When an add-on is toggled, Then the included tax component is updated and displayed, unit prices remain constant, and total equals displayed price plus fees without double-taxing. Given mixed taxability (e.g., class taxable, add-on non-taxable), When both are in the cart, Then only taxable items contribute to the tax line and the tax calculation reflects item taxability flags.
Multi-Currency Precision and Rounding
Given currency USD (2 decimal places), When totals are computed with add-ons, taxes, fees, and discounts, Then all amounts are rounded to the currency's minor unit per ISO 4217 and the sum of rounded lines equals the rounded total. Given currency JPY (0 decimal places), When totals are computed, Then amounts are rounded to whole yen, no fractional values are displayed or charged, and the wallet sheet shows an integer amount. Given a currency with 3-decimal minor units (e.g., BHD), When totals are computed, Then rounding follows the currency's minor units and the wallet sheet amount equals the backend authorized amount exactly with zero discrepancy.
Class Pack Proration and Tax Interaction
Given the user adds a class pack upsell and has an existing eligible pack, When proration is applied, Then a proration credit line appears and is applied before tax in tax-exclusive jurisdictions and as a reduction of the included tax base in tax-inclusive jurisdictions. Given a coupon is applied alongside a proration credit, When totals are computed, Then the application order follows the defined business rule and totals remain non-negative with correct tax recomputation. Given an add-on that is not subject to proration, When toggled, Then proration affects only the class pack line and does not alter add-on amounts beyond tax recalculation where required.
Parity Across Display, Wallet Sheet, Backend Charge, and Receipt
Given the canonical server-calculated price breakdown is available, When the wallet payment sheet is presented, Then the amount and itemization shown match the breakdown displayed in the UI. Given payment is authorized and captured, When the receipt is generated, Then the charged amount and itemized breakdown exactly match the wallet sheet and UI with zero discrepancy tolerance. Given a discrepancy is detected between client-displayed and server-calculated totals, When the user attempts to pay, Then the Pay action is blocked, a recalculation is triggered, and no authorization is attempted until parity is restored.
Inventory Reservation and Validation
"As an operations manager, I want inventory to be checked and held when customers add extras so that we avoid double-booking limited resources."
Description

Validate add-on availability on toggle and reserve inventory with a short time-to-live hold during checkout to prevent overselling (e.g., limited mat rentals). Re-verify availability on payment confirmation, gracefully handle race conditions with clear inline errors and auto-revert toggles, and release holds on timeout, cancellation, or payment failure. Log events for reconciliation and support capacity limits tied to class enrollment and multi-quantity purchases.

Acceptance Criteria
One-Tap Add-On Toggle Creates TTL Inventory Hold
Given a user is at the wallet confirmation step with an add-on available to upsell When the user toggles the add-on on Then the system performs an atomic availability check for the requested quantity And if available, creates a reservation hold with a short configurable TTL associated to the user’s checkout session And the order total and line items reflect the add-on immediately And the global available inventory decreases by the held quantity until the hold is released And when the user toggles the add-on off before payment, the hold is released immediately and totals/inventory update accordingly
Concurrent Toggle Race Condition Is Gracefully Handled
Given two or more users toggle the same limited add-on within the TTL window with fewer units remaining than requested When the server processes the toggle requests Then holds are granted only up to the true remaining inventory with no oversell And rejected requests receive an inline error message in the wallet step explaining the add-on is no longer available And the add-on toggle auto-reverts to off and totals are restored without leaving the flow And the ordering of accepts/rejects is deterministic and idempotent per request (no duplicate holds or double decrements)
Payment Confirmation Re-Verification Adjusts Order Safely
Given a user has one or more add-ons on hold and is confirming payment When the system re-verifies add-on availability immediately before authorizing payment Then if all held quantities are still available, the holds convert to final allocations and payment is authorized successfully And if any held quantity is no longer available, those line items are removed or reduced to available quantity, totals are recalculated, and an inline message explains the change And payment does not proceed for unavailable quantities, and the user can continue to pay the updated total without leaving the wallet step And no oversell or duplicate charges occur
Automatic Hold Release on Timeout, Cancellation, or Payment Failure
Given a reservation hold exists for an add-on When the hold TTL expires, the user cancels checkout, or payment authorization fails or times out Then the hold is released promptly, inventory is restored, and the add-on is removed from the order summary And the UI reflects the release (toggle off, totals updated) within the current session And releasing a hold is idempotent and safe to retry
Multi-Quantity Add-Ons Respect Inventory and Class-Linked Capacity
Given an add-on supports multi-quantity selection and may be capacity-linked to the class enrollment When the user increases quantity from the wallet step Then the system validates against the true remaining inventory (and any class-linked capacity constraints) and creates or expands holds per unit And the user cannot increase beyond the available quantity and receives inline feedback if a limit is reached And when the user decreases quantity or toggles the add-on off, the corresponding held units are released immediately and totals/inventory update accordingly And if the user changes the selected class or the class capacity changes server-side during checkout, the add-on quantities are revalidated and adjusted with inline messaging
Event Logging for Inventory Validation and Holds
Given an availability check, hold create/update/release, payment re-verification, or final allocation occurs When the event is processed Then an audit log entry is recorded with timestamp, event type, class ID, add-on ID, user/session ID, quantity, hold ID, outcome (success/fail), reason, and resulting available inventory And entries are correlated to the checkout session via a correlation ID for reconciliation And logs exclude sensitive payment data and are retrievable for support within retention policy
Payment Gateway Line-Itemization and Refunds
"As a finance admin, I want add-ons to appear as separate line items in payments and refunds so that reporting is accurate and customer service can process partial refunds easily."
Description

Include add-ons as distinct line items in payment intents for Apple Pay/Google Pay via the processor (e.g., Stripe) with correct tax breakdowns and metadata for reconciliation. Ensure a single authorization captures class plus add-ons, support partial and full refunds at the line-item level, and expose itemization to accounting exports and webhooks. Maintain idempotency and ensure charge amounts match the displayed total to minimize declines.

Acceptance Criteria
Wallet Payment Intent Line-Itemization
Given a user selects a class and one or more add-ons in the wallet confirmation step When a PaymentIntent is created for Apple Pay or Google Pay via Stripe Then the PaymentIntent contains distinct line items for the class and each add-on with correct name, quantity, currency, unit_amount, subtotal, tax_amount, and total per item And each line item includes a tax breakdown (rate id, jurisdiction, inclusive/exclusive flag) matching the configured tax profile And PaymentIntent metadata includes order_id, booking_id, user_id, class_id, addon_ids, tax_profile_id, and source=wallet And the PaymentIntent amount equals the sum of item totals (subtotals + taxes − discounts) and matches the UI-displayed total exactly And all monetary values are stored and transmitted in minor currency units with consistent rounding rules
Single Authorization and Capture for Class + Add‑ons
Given a wallet checkout containing a class and selected add-ons When the user confirms payment via Apple Pay or Google Pay Then a single authorization is created for the combined total and captured in one transaction And no additional authorizations or separate charges are created for add-ons And the captured amount equals the displayed total at confirmation time with zero variance And retrying the confirmation with the same idempotency key produces no duplicate authorizations or captures
Item‑Level Partial and Full Refunds
Given a settled charge that includes a class line item and one or more add-on line items When an admin requests a refund for specific line items Then a Stripe Refund is created for the exact refundable amount of the selected items and metadata includes order_id and refunded_line_item_ids And refunding an add-on does not cancel the class booking; refunding the class cancels the booking per policy And multiple partial refunds are supported up to the total captured amount; attempts to over-refund return a validation error and no refund is created And webhooks for the refund include item-level attribution and updated refundable balances
Accounting Export with Itemization
Given completed orders within a selected date range When the accounting export is generated Then the export contains one row per line item with columns: order_id, booking_id, user_id, item_type (class|addon), item_name, quantity, currency, unit_amount, subtotal, tax_rate_id, tax_amount, discount_amount, line_total, processor_charge_id, refund_amount, refunded_at, metadata And the sum of line_total per order equals the captured amount reported by the processor And numeric amounts are in minor units; timestamps are ISO 8601 in the account’s timezone; column headers are stable and documented
Webhook Itemization and Idempotency
Given payment_intent.succeeded, charge.captured, and refund.succeeded events occur When webhooks are delivered (including retries) Then each payload includes an itemization array with item_id, item_type, quantity, unit_amount, tax_amount, discounts, and for refunds the refunded_line_item_ids and amounts And each event includes order_id and an idempotency_key enabling consumers to deduplicate And the platform treats duplicate deliveries as idempotent: processing the same event twice does not duplicate records or alter balances
Displayed Total vs Processor Charge Consistency
Given a wallet checkout review displaying subtotal, taxes, discounts, and total When the user pays via Apple Pay or Google Pay Then the processor capture amount must equal the displayed total exactly And if a mismatch is detected pre-confirmation, the payment is aborted, the user is shown an error with a prompt to retry, and a telemetry event is logged with variance data And end-to-end tests validate zero variance across supported currencies and tax configurations
Post-Purchase Communication and Redemption
"As a customer, I want my confirmation to show the extras I bought and how to use them so that I know what to expect when I arrive."
Description

Include purchased add-ons in confirmation emails/SMS and receipts with clear labels, quantities, and redemption instructions. Surface add-ons in staff check-in views and attendee manifests, optionally generating scannable tokens (QR/barcode) for items like guest passes. Sync to calendar invites where relevant and support reminders that reference add-ons (e.g., “Mat reserved”). Ensure localization and white-label branding across channels.

Acceptance Criteria
Add-Ons Included in Confirmation Email and SMS
Given a completed purchase with one or more add-ons When the transaction is confirmed Then a confirmation email and SMS are sent within 2 minutes And both messages contain an "Add-ons" section listing each add-on by name and quantity And each listed add-on includes concise redemption instructions (SMS ≤ 200 chars; essential info not truncated) And if no add-ons were purchased, no "Add-ons" section appears
Receipts Display Add-Ons with Labels and Quantities
Given a purchase includes add-ons When the receipt (web and email) is generated Then each add-on appears as a separate line item with label, quantity, unit price, and line total And the order subtotal, taxes/fees/discounts, and grand total accurately include add-ons And the receipt includes a "How to redeem" note per add-on
Staff Check-In and Manifest Surfacing
Given staff opens the class check-in view for a session When an attendee has purchased add-ons Then add-ons are displayed adjacent to the attendee with clear labels and quantities And staff can mark each add-on as redeemed/not redeemed with a single tap And redemption status syncs to all staff sessions and attendee manifests within 5 seconds And the exported attendee manifest CSV includes columns for add-ons and redemption status
Scannable Tokens for Tokenizable Add-Ons
Given an add-on is configured as tokenizable (e.g., guest pass) When it is purchased Then the system generates unique QR and barcode tokens per unit quantity And each token is single-use; scanning marks it redeemed and prevents reuse And tokens can be scanned from the staff app or web check-in using a device camera And staff can manually mark as redeemed without scanning, with a timestamped audit entry And tokens expire at the class end time and cannot be redeemed afterward
Calendar Invite Sync References Add-Ons
Given a booking with calendar-relevant add-ons (e.g., mat rental) When the user adds the event to their calendar (ICS or native) Then the event description includes an "Add-ons" section with item names and quantities And changes to add-ons before the start time trigger an updated ICS and notification to refresh the calendar entry And the calendar entry uses the purchaser’s locale for date/time formatting
Automated Reminders Reference Add-Ons
Given reminders are enabled for the class When a reminder is sent for a booking with add-ons Then the message includes a concise add-on summary (e.g., "Mat reserved x1") And for tokenizable items, the reminder includes a "Show your pass" note and a link to view tokens And reminder content is channel-optimized (SMS ≤ 160 GSM chars) while preserving add-on info
Localization and White-Label Branding Across Channels
Given a studio’s branding configuration and the purchaser’s locale When generating confirmations, receipts, reminders, calendar invites, and staff views Then all user-facing text is localized to the purchaser’s locale (min supported: en, es, fr) with correct currency and date/number formats And all communications and pages display the studio’s name/logo/colors and omit ClassTap branding And all links use the studio’s custom domain if configured; otherwise use the default white-label domain
Analytics, Experimentation, and Personalization
"As a product manager, I want to test and measure which upsells perform best for different segments so that we can increase revenue without hurting conversion."
Description

Instrument events to measure attach rate, AOV lift, per-add-on conversion, and drop-off. Provide a dashboard and exports, and support A/B testing of default toggle states, placement, and copy. Enable simple personalization rules (e.g., suggest class pack to repeat attendees) with guardrails to avoid dark patterns. Ensure privacy compliance (GDPR/CCPA), honor consent, and anonymize data where required.

Acceptance Criteria
Upsell Funnel Event Instrumentation
Given a buyer views the wallet confirmation step with upsell options visible, when the step renders, then an event upsell_impression is logged with session_id, user_id (if authenticated), upsell_group_id, locale, device_type, experiment_variant, and timestamp. Given a buyer toggles an add-on on or off, when the toggle state changes, then an event upsell_toggle is logged with add_on_id, previous_state, new_state, price, currency, and timestamp and is emitted exactly once per change. Given a buyer completes payment, when the payment is authorized, then events order_completed and upsell_purchase (if any add-ons were purchased) are logged with order_id, add_on_ids, subtotal, add_on_total, total, currency, and experiment_variant. Given a buyer exits before payment, when the session ends or payment is abandoned, then an event checkout_dropoff is logged with last_step and last_interaction within 60 seconds of inactivity. Then attach_rate, per_add_on_conversion, AOV, and step_dropoff can be computed from events; event duplication rate is <0.5% and event loss rate is <0.5% within 15 minutes of event time.
Analytics Dashboard Metrics and Segmentation
Given a date range is selected, when an admin opens the Upsells Analytics dashboard, then metrics show attach_rate, AOV, AOV_lift vs baseline, per_add_on_conversion, revenue_from_add_ons, and funnel drop-off by step. Then filters exist for location, instructor, class_type, device_type, experiment_variant, and user_segment (new vs returning) and apply with P50 latency ≤2s and P95 ≤5s. Then a data freshness indicator shows last update time and metrics are within 15 minutes of event time. Then dashboard totals for a given period match the CSV export within 0.5% for the same filters and time zone. Then time zone can be selected (account default, viewer local) and persists per user preference.
Experimentation on Toggle State, Placement, and Copy
Given an experiment targeting the wallet confirmation step is created, when variants define default toggle state, upsell placement, and copy, then traffic is randomly assigned at user_id level (fallback session_id) and assignment persists for 30 days. Then a control holdout of 10% can be configured; sample ratio mismatch is auto-detected and alerted if observed proportions significantly deviate (p<0.01). Then experiments can be started, paused, and stopped without code deploys, and only one active experiment per surface is allowed at a time. Then primary metrics attach_rate and AOV_lift and secondary per_add_on_conversion are computed with 95% confidence intervals and minimum sample size guardrails before declaring a winner; results are segmentable by device_type and location.
Personalization Rules and Guardrails
Given a repeat attendee (≥2 prior bookings) with personalization consent, when viewing the wallet confirmation step, then a class pack upsell is displayed with a "Recommended" label and rationale "You’ve booked 2+ classes." Then default toggle for personalized upsells is Off unless an explicit experiment variant sets it otherwise; any pre-selected add-on is clearly indicated and reversible with a single tap. Then users can dismiss a personalized upsell and it remains suppressed for 30 days or until eligibility changes. Then personalization never hides mandatory fees or misrepresents pricing; total price updates immediately and clearly when an add-on is toggled; no countdown timers or coercive copy are used for personalized upsells.
Privacy, Consent, and Data Minimization
Given a user declines analytics or selects CCPA Do Not Sell/Share, when they proceed through checkout, then no analytics, experimentation, or personalization events are recorded and a default, non-tracked experience is shown. Then IP addresses are not stored; analytics identifiers are pseudonymous; EU-sourced events are geo-routed, anonymized per GDPR, and stored in an EU region. Then consent states are honored and auditable; withdrawing consent stops tracking immediately and historical identifier linkages are erased within 30 days. Then DSAR access/delete requests export and delete all analytics identifiers and linked events within 30 days of verified request. Then raw analytics event retention is capped at ≤13 months and PII fields are blocked by schema validation from event payloads.
Exports and Data Access
Given an admin selects a date range and metrics, when requesting an export, then a CSV is generated within 2 minutes containing line-level upsell events and a data dictionary. Then scheduled daily exports can be configured to S3 or SFTP with PGP encryption; failures trigger email alerts and retry up to 3 times. Then export row counts and key aggregates match dashboard metrics within 0.5% for the same filters and date range. Then only users with Analytics Viewer or higher can access exports; all exports are audit-logged with requester, filters, and timestamp.
Reliability and Data Quality Monitoring
Given normal traffic, when events are ingested, then 99.5% are processed and available for analytics within 15 minutes; retries occur up to 3 times with idempotent keys to prevent duplicates. Then schema versioning enforces required fields; malformed events are rejected with error codes and logged to an ops dashboard. Then weekly data quality reports compute duplicate_rate, loss_rate, and field_null_rate with thresholds and alerting to Slack/email when exceeded. Then staging and production analytics pipelines are isolated and staging data never appears in production dashboards or exports.

Smart Seat Hold

Automatically holds a seat for 60 seconds the moment the wallet sheet opens, preventing double-bookings during peak demand. Releases on timeout or failure and confirms instantly on success—ensuring fairness and clean rosters for staff.

Requirements

Real-time Seat Lock on Checkout Initiation
"As a student booking a popular class, I want my seat held the instant I start checkout so that no one else can take it while I complete payment."
Description

When a user opens the wallet/payment sheet, the system immediately creates a server-side provisional seat hold for the selected class occurrence with a 60-second TTL. The hold is keyed by class occurrence, account, and user/session plus payment intent to ensure one active hold per user per class and to prevent over-capacity bookings. The lock must be atomic, globally consistent across regions, and visible to capacity checks in all booking endpoints. It must be idempotent (reopening checkout or network retries must not create duplicate holds) and resilient to client disconnects (server TTL governs release). Works across web and mobile SDKs with a single API (CreateHold) returning hold_id, expiry timestamp (server time), and remaining capacity including held seats. Emits metrics and logs for hold_created and hold_collision events.

Acceptance Criteria
60-Second Countdown and Auto-Release
"As a student, I want a clear countdown showing how long my spot is reserved so that I can decide quickly and understand when the seat will be released."
Description

Expose a synchronized 60-second countdown to the client using the hold’s server expiry to avoid clock drift. The server automatically releases the hold at expiry and publishes a hold_released event that updates capacity and UI subscribers. The client displays a prominent timer and disables confirm actions upon expiry. If the user navigates back or dismisses the wallet sheet, the client calls ReleaseHold; the server must handle double-release safely. Holds must never outlive the TTL, even if the client is offline. Provide localized messaging and accessibility support for screen readers for the countdown and expiry states.

Acceptance Criteria
Server-Synchronized Countdown Display
Given a user opens the wallet sheet and a hold H is created with server expiry timestamp T When the wallet sheet loads Then the client initializes the countdown from T (server time), not the device clock And the countdown updates every 1 second and never displays negative time And the displayed remaining time deviates from server time by no more than ±1 second when checked at 0s, 30s, and 59s And the timer is visible above the confirm action and shows mm:ss format
Automatic Hold Expiry and Event Publication
Given an active hold H with expiry T When T is reached on the server Then the server releases H within 1 second of T And the class capacity is incremented by 1 on the server And exactly one hold_released event is published containing hold_id, class_id, and released_at ≥ T And all subscribed clients receive the event and update available seats within 2 seconds And no duplicate hold_released event is emitted for H
Client Behavior on Expiry: Disable Confirm and Show Expired State
Given the active hold H reaches expiry locally or a hold_released event for H is received When the client state updates Then the Confirm action becomes disabled within 500 ms and cannot be activated And any attempt to confirm via API after expiry returns HTTP 409 with code HOLD_EXPIRED And the UI displays an "Hold expired" message and presents a retry or waitlist option when available And if a hold_released event arrives before the local countdown reaches 00:00, the UI immediately reflects expiry and stops the timer
Release on Back Navigation or Wallet Dismissal
Given the wallet sheet is open with an active hold H When the user navigates back or dismisses the wallet sheet Then the client calls ReleaseHold(H) once within 300 ms And the server responds 200 OK whether H is active or already released And a hold_released event is published only if H was active at the time of processing And subscribed clients update displayed capacity within 2 seconds of the event
Idempotent Double-Release Handling
Given ReleaseHold(H) is invoked multiple times due to retries or rapid user actions When the server processes these requests Then the first valid call releases H and increments capacity once And subsequent calls return 200 OK with no side effects And at most one hold_released event is published for H And server logs or metrics indicate deduplication using hold_id or request_id
TTL Enforcement While Client Is Offline
Given a hold H with a 60-second TTL and the client goes offline or the app is terminated before expiry When the server time reaches the expiry T Then the server releases H and publishes hold_released regardless of client connectivity And the seat is available for other users immediately after release And upon client reconnection, H is reported as expired and no countdown is displayed And the client reconciles state by processing missed events or fetching current capacity
Localization and Screen Reader Accessibility for Countdown and Expiry
- Countdown and expiry messages are localized to the user’s locale; verified for en-US, es-ES, and fr-FR with correct numerals and pluralization - If a locale is unavailable, fallback to en-US with no missing message keys - The countdown exposes an accessible name "Seat hold: {mm:ss} remaining" to screen readers - A polite live region announces at start and at 30s and 10s remaining; on expiry, an assertive announcement "Hold expired" is made - Focus moves to the primary next action on expiry without trapping focus; interaction remains keyboard-accessible - Visual timer meets WCAG 2.1 AA (contrast ratio ≥ 4.5:1) and does not flash more than 3 times per second
Instant Confirmation and Roster Sync on Success
"As an instructor, I want successful payments to instantly confirm the booking and update my roster so that I have an accurate headcount without manual reconciliation."
Description

Upon payment success callback/webhook, atomically promote the hold to a confirmed booking, decrement capacity, remove the hold, and add the attendee to the roster in a single transactional operation. Ensure idempotency for duplicate callbacks using payment intent IDs. Return immediate confirmation to the client and send branded confirmation email/SMS. Update instructor dashboards and staff rosters in real time, showing the attendee as Confirmed and removing any visible hold annotation. Publish booking_confirmed events for downstream integrations (analytics, CRM) and record audit entries linking hold_id to booking_id.

Acceptance Criteria
Atomic Promotion of Hold to Confirmed Booking on Payment Success
Given an active seat hold with hold_id and associated payment_intent_id and available capacity When a payment success webhook/callback is received with that payment_intent_id Then the system performs a single atomic transaction that: - creates or updates a booking to status=Confirmed linked to hold_id and attendee_id - decrements class capacity by one - removes the seat hold - inserts the attendee into the class roster And no partial changes are committed if any step fails (all-or-nothing rollback) And the final persisted state shows no hold for that attendee and capacity decreased by 1 And the booking record includes references: hold_id, payment_intent_id, and class_id
Idempotent Handling of Duplicate Payment Success Callbacks
Given a prior successful processing of payment_intent_id X resulting in booking_id B When a duplicate success callback for payment_intent_id X is received any time within 24 hours Then the system must not create a new booking or decrement capacity again And it returns HTTP 200 with the existing booking_id B and status=Confirmed And no additional confirmation email/SMS is sent And no additional booking_confirmed event is published And an audit entry records the duplicate callback with a reference to booking_id B
Immediate Client Confirmation Response
Given the server has successfully applied the atomic transaction for the payment intent When the client awaits confirmation after checkout or polls/fetches booking status Then the API responds with 2xx within 2 seconds of processing completion And the response body includes booking_id, class_id, attendee_id, status=Confirmed, start_time, venue (or join_link), and capacity_remaining And the client-visible hold annotation is absent in the response payload
Branded Confirmation Notifications Sent Once
Given a booking transitions to Confirmed from a seat hold When processing completes successfully Then a branded email and SMS are queued within 5 seconds and delivered to the attendee's verified contacts And the content includes brand name/logo, class title, date/time, location or virtual link, cancellation policy, and booking_id And notifications are sent exactly once per payment_intent_id even if duplicate callbacks arrive And notification delivery failures are retried per policy and logged without blocking booking confirmation
Real-Time Instructor Dashboard and Roster Sync
Given an instructor dashboard session subscribed to the class When a booking is confirmed from a hold Then within 3 seconds the dashboard updates to show the attendee in the roster as Confirmed And any visible hold annotation for that attendee is removed And the displayed capacity/available seats count reflects the decremented capacity And no ghost holds or duplicate roster entries appear after refresh
Booking Confirmed Event and Audit Trail
Given a booking is confirmed from a hold via payment_intent_id X When the transaction commits Then a booking_confirmed event is published to the event bus within 2 seconds with payload including booking_id, class_id, attendee_id, hold_id, payment_intent_id, timestamp, and source=smart_seat_hold And the event includes an idempotency key equal to payment_intent_id to prevent downstream duplication And an immutable audit record is written linking hold_id to booking_id with actor=system, action=promote_hold, and correlation_id=payment_intent_id And both event and audit records are queryable by booking_id and payment_intent_id
Concurrency Safety Under Peak Load
Given multiple workers/processes receive the same payment success notification concurrently for payment_intent_id X When they attempt to promote the same hold Then at most one transaction succeeds in confirming the booking and decrementing capacity And all other attempts return a successful idempotent response referencing the existing booking without side effects And no deadlocks occur and per-attempt lock wait or timeout does not exceed 5 seconds And system metrics record zero over-decrements of capacity
Failure and Abandonment Handling
"As a student, I want the seat to be released right away if my payment fails so that I can retry or let someone else book without confusion."
Description

If payment fails, is canceled, or the wallet sheet is dismissed, immediately release the hold and restore capacity with a hold_released reason code. Surface a clear error to the user with retry options. Reconciliation must handle races between timeout and failure to avoid double release. Integrate with payment processor failure webhooks to catch late failures and clean up any lingering holds. Log structured failure reasons, user agent, and latency to support supportability and funnel analysis. Ensure holds from crashed clients are safely released by TTL without human intervention.

Acceptance Criteria
Immediate Release on Payment Failure/Cancellation/Dismissal
Given a user has an active 60-second seat hold initiated by opening the wallet sheet When the payment processor returns a failure OR the user cancels payment OR dismisses the wallet sheet Then the system releases the hold within 200 ms of receiving the event And the class capacity is incremented by 1 atomically And the hold record is updated to state=Released with reason_code=hold_released and cause in [payment_failed, user_canceled, wallet_dismissed] And no booking confirmation is created And the seat appears available to other users within 1 second
User Error Messaging and Retry Path
Given a payment attempt ends in failure, cancellation, or wallet dismissal When the user returns to the booking screen Then an inline error banner is displayed within 300 ms with a human-readable message mapped to the failure cause And a primary "Retry payment" action is available that reopens the wallet and acquires a new 60-second hold only if capacity >= 1 And a "Choose another method" secondary action is available if multiple payment methods are supported And analytics events payment_error_shown and payment_retry_clicked are emitted with hold_id and failure_type
Idempotent Release on Timeout vs Failure Race
Given a seat hold with TTL=60s and a distinct hold_id and idempotency_key And a failure event arrives within ±500 ms of the TTL expiration When release is processed Then exactly one release occurs (no double increment of capacity) And the final hold state is Released with a single audit log entry containing release_path in [timeout, client_failure, webhook_failure] and the earlier of the two timestamps And no duplicate notifications are sent
Processor Failure Webhook Cleanup
Given the client did not send a failure signal due to termination or network loss And a payment.processor.failure webhook is received for a payment intent linked to an active or stale hold When the webhook is processed Then the hold is released if not already, with reason_code=hold_released and cause=webhook_failure And any pending provisional booking records are canceled And capacity is accurate post-processing (no negative or over-capacity states) And the webhook handler is idempotent using payment_intent_id and can be safely retried And processing latency p95 <= 1 second from webhook receipt
Crash/Offline Client TTL Auto-Release
Given a user opens the wallet sheet creating a hold and then closes the app or loses connectivity without completing payment When 60 seconds elapse without a success signal Then the server automatically releases the hold with reason_code=hold_released and cause=timeout And capacity is restored and visible in the availability API within 1 second And no manual intervention is required
Structured Failure Logging and Metrics
Given any failure, cancellation, dismissal, or timeout that triggers a release When the release operation completes Then a structured event is produced with fields: hold_id, payment_intent_id, user_id (if available), class_id, timestamp, failure_type, cause, release_path, user_agent, client_version, latency_ms_wallet_open_to_release, idempotency_key, and trace_id And the event is delivered to the analytics pipeline with at-least-once semantics and appears in dashboards within 5 minutes (p99) And missing mandatory fields cause the operation to retry logging without blocking the release
Capacity Restoration and Waitlist Promotion on Release
Given a class at full capacity with an active waitlist and a seat hold is released due to failure, cancellation, dismissal, or timeout When the release is committed Then displayed capacity increases by 1 within 1 second And if waitlist is enabled, the next eligible waitlisted user is promoted and notified within 2 seconds according to fairness rules And the promotion consumes the newly freed seat atomically, ensuring no double-booking
Waitlist Auto-Promotion on Hold Expiry
"As a waitlisted student, I want to be offered a seat the moment one becomes available after someone’s hold expires so that I have a fair chance to join the class."
Description

When a class is full and a hold is released due to timeout or explicit failure, publish a seat_available event to the waitlist service. The waitlist service promotes the next eligible person according to configured rules (e.g., FIFO, membership tier), sends an offer notification, and optionally places a short offer hold separate from checkout holds. Ensure that waitlist promotions do not trigger while any seat holds are active that keep capacity effectively zero. Provide guardrails to prevent oscillation when multiple holds expire concurrently and expose admin reporting on promotions triggered by hold expiries.

Acceptance Criteria
Seat available event on checkout hold timeout
Given a class is at full capacity and a checkout hold H is active for that class When hold H expires due to timeout and the seat release is persisted Then exactly one seat_available event is published within 2 seconds with fields {class_id, seat_delta:1, hold_id:H, reason:"hold_timeout", event_id (unique), occurred_at} And duplicate deliveries of the same event_id are ignored by the waitlist service And the event is retried until acknowledged by the waitlist service
Seat available event on explicit checkout failure
Given a class is at full capacity and a checkout hold H is active When checkout is explicitly failed (e.g., payment authorization fails or user cancels) and hold H is released Then exactly one seat_available event is published within 2 seconds with fields {class_id, seat_delta:1, hold_id:H, reason:"checkout_failure", event_id (unique), occurred_at} And no additional seat_available event is emitted for the same hold release
Waitlist promotion and offer hold creation on seat availability
Given the waitlist service receives a seat_available event for class X with seat_delta:1 and there is at least one eligible waitlisted user When the service applies the configured prioritization rule (e.g., FIFO, membership tier) Then the next eligible user is promoted with promotion_reason:"hold_expiry" and the decision is recorded with rule_applied And an offer notification is sent via all configured channels containing a claim link And a distinct offer hold is created for the configured offer_hold_duration (in minutes), separate from checkout holds And class effective capacity decreases by 1 while the offer hold is active And if the user claims within the offer_hold_duration, checkout is initiated and the offer hold transitions appropriately And if the offer hold expires, it is released and a new seat_available event is published within 2 seconds
Gating promotions when effective capacity is zero
Given a class has active holds (checkout and/or offer) and effective_capacity = capacity - confirmed_bookings - active_checkout_holds - active_offer_holds equals 0 When any single hold expires but effective_capacity remains 0 due to other active holds Then no seat_available event is published and no waitlist promotion is attempted And when effective_capacity transitions from 0 to >=1 with no conflicting holds Then exactly one seat_available event is published per newly available seat and promotions proceed accordingly
Concurrency guardrails and idempotent promotions
Given K (K>=2) holds for the same class expire within a 1-second window When the system processes the hold releases Then at most K seats of availability are exposed and no more than K promotions are initiated And promotions are computed against an atomic availability snapshot to prevent over-promotion or oscillation And each promotion carries a unique promotion_key (e.g., class_id + event_id) enabling idempotent handling across retries And no user receives more than one simultaneous offer for the same class instance
Admin reporting for hold-expiry-triggered promotions
Given promotions have been triggered by hold expiries When an admin queries the Hold-Expiry Promotions report/API for a time range and class Then each promotion record includes {class_id, class_start_at, seat_release_reason, hold_id, event_id, promoted_user_id, rule_applied, notification_channels, offer_hold_duration, outcome, timestamps} And aggregate counts (promotions, accepted, expired, declined) reconcile with underlying events for the selected range And results are filterable by class, rule_applied, and seat_release_reason and exportable to CSV
No duplicate promotion on duplicate seat_available events
Given the waitlist service receives duplicate deliveries of the same seat_available event_id due to at-least-once messaging When processing the duplicates Then at most one promotion is created for that event_id And no duplicate notifications are sent And metrics and reporting reflect a single promotion for that event_id
Offer decline or expiration re-publishes availability
Given a user was promoted via hold-expiry and an offer hold is active When the user declines the offer or the offer hold expires Then the offer hold is released within 1 second And exactly one seat_available event is published within 2 seconds with reason:"offer_released" and event_id (unique) And the next eligible waitlisted user is considered according to the configured rule
Admin Controls and Audit Analytics
"As a studio owner, I want to configure hold behavior and see analytics on holds and conversions so that I can balance fairness with revenue and reduce no-shows."
Description

Provide per-organization and per-class settings to enable/disable Smart Seat Hold, set hold duration (default 60 seconds, with enforceable min/max), and toggle waitlist integration. In staff views, display held seats with remaining time and reason. Offer analytics on hold creation rate, conversion rate, time-to-pay distribution, expiries, and failures, segmented by class and channel. Include an audit log linking user, hold_id, booking_id, timestamps, and state transitions for support and compliance. All settings must be available via dashboard UI and Admin API with role-based access control.

Acceptance Criteria
Org/Class Dashboard Controls for Smart Seat Hold
Given I am an Organization Admin on the dashboard, When I open Organization > Smart Seat Hold settings, Then I can enable/disable Smart Seat Hold, set hold_duration_seconds with default prefilled to 60, and toggle waitlist integration Given the hold duration input is displayed, When I focus the field, Then the UI shows the allowed range (MIN_HOLD_SECONDS–MAX_HOLD_SECONDS) and enforces it with inline validation Given I enter a value outside the allowed range, When I click Save, Then the Save action is blocked and I see an error message indicating the allowed range Given I open a specific Class > Smart Seat Hold settings, When I toggle "Override organization defaults", Then I can independently enable/disable holds, set hold_duration_seconds, and toggle waitlist integration for that class Given I save changes at the class level, When the save succeeds, Then the class shows it is overriding org defaults and the new settings are applied to new holds started after the save time Given I click "Use organization defaults" at the class level, When I confirm, Then class-level overrides are cleared and the class inherits org settings Given I am a Staff role (non-admin), When I view these settings, Then fields are read-only and any attempt to edit returns 403 Forbidden
Admin API for Seat Hold Settings with RBAC
Given I have an Admin API token, When I GET /admin/orgs/{orgId}/seat-hold-settings, Then I receive 200 OK with JSON including enabled, hold_duration_seconds, waitlist_enabled, min_hold_seconds, max_hold_seconds Given I have an Admin API token, When I PUT /admin/orgs/{orgId}/seat-hold-settings with values within [min_hold_seconds, max_hold_seconds], Then I receive 200 OK and subsequent new holds use the updated values Given I send a PUT with hold_duration_seconds outside the allowed range, When the request is processed, Then I receive 422 Unprocessable Entity with a field error describing the allowed range Given I have a Staff API token, When I PUT seat hold settings at org or class endpoints, Then I receive 403 Forbidden; When I GET, Then I receive 200 OK with readOnly=true in the payload Given I have an Admin API token, When I GET/PUT /admin/classes/{classId}/seat-hold-settings, Then the payload supports inherit=true/false and per-class overrides for enabled, hold_duration_seconds, and waitlist_enabled Given I include Idempotency-Key on PUT, When I resend the same payload with the same key, Then the response is identical and only one audit entry is written Given I update settings via API, When the operation succeeds, Then an audit log entry records actor_id, source="api", old_values, new_values, and timestamp
Staff Roster View Shows Active Holds and Countdown
Given a class has active seat holds, When a Staff user opens the roster view, Then each held seat is labeled "Held" with a mm:ss countdown and a reason (e.g., "Checkout in progress" or "Manual hold") Given the roster view is open, When time elapses, Then each countdown decrements every second without a full page refresh Given a held seat expires, When the timer reaches 00:00, Then the held indicator disappears or reverts to "Available" within 2 seconds of expiry Given a held seat converts to a booking, When payment succeeds, Then the entry transitions from "Held" to "Booked" without duplicate seat entries Given the class capacity is N, When viewing the roster, Then booked + held never exceeds N in the display Given Smart Seat Hold is disabled for the class, When viewing the roster, Then no "Held" labels or countdowns are shown
Waitlist Integration Controlled by Toggle
Given a class is at capacity and waitlist integration is enabled for that class, When a seat hold expires, Then the next eligible waitlisted user is automatically notified/promoted per policy and the seat is reserved or offered to them Given waitlist integration is disabled for the class (or inherited disabled from org), When a seat hold expires, Then no auto-promotion occurs and the seat returns to available inventory Given an admin changes the waitlist toggle, When the change is saved, Then the new behavior applies to expirations that occur after the save timestamp, and any already-sent promotions are unaffected Given a hold expiration occurs, When waitlist behavior is evaluated, Then an audit log event is written indicating whether promotion occurred and the reason (e.g., toggle_off, no_waitlist)
Seat Hold Analytics Dashboard and Export
Given an Organization Admin opens Analytics > Seat Holds, When a date range and filters (class, channel) are selected, Then the dashboard shows holds_created, conversion_rate, time_to_pay (median, p90, histogram), expiries, and failures Given class and channel segment controls are used, When segments are toggled, Then charts/tables update to reflect segmentation by class and channel (e.g., web, mobile, POS) Given new seat hold events occur, When viewing analytics, Then data freshness is ≤15 minutes between event time and appearance on the dashboard Given an Admin validates a sample window, When comparing dashboard counts to raw events, Then differences are ≤0.5% for counts and ≤0.5 percentage points for rates Given the dashboard is visible, When the user clicks a metric segment, Then a drill-down appears listing the underlying hold_ids/booking_ids matching that segment and filters Given the user clicks Export CSV, When the export completes, Then a CSV downloads within 10 seconds containing the same aggregates and filters as displayed
Comprehensive Audit Log for Seat Holds and Bookings
Given any seat hold lifecycle event occurs, When the system processes it, Then an audit record is written with hold_id, booking_id (nullable), user_id or session_id, class_id, channel, actor_id (nullable), event_name, and UTC ISO 8601 timestamp with millisecond precision Given common lifecycle events occur, When they are logged, Then the following event_names are supported at minimum: hold_created, hold_released_timeout, hold_released_failure, hold_converted_booking, payment_attempted, payment_succeeded, payment_failed, settings_changed Given audit records are stored, When queried, Then records are append-only (immutable); corrections appear as new events linked by correlation_id; hard deletes are disallowed Given an Admin or Compliance user searches the audit log, When filtering by hold_id, booking_id, user identifier, class_id, event_name, or time range, Then results return within 2 seconds for the 95th percentile of queries up to 10,000 records Given a Staff user attempts to access the audit log, When requesting via UI or API, Then access is denied with 403 Forbidden Given an Admin uses the API, When calling GET /admin/audit/holds with pagination parameters, Then results are paginated deterministically and include next/prev cursors

Reminder Paylinks

Automated SMS/email reminders include secure, expiring TapPass links that open directly to one-tap confirm. Supports quick rebooks, waitlist promotions, and last-minute fills—cutting no-shows and keeping classes at capacity with zero admin effort.

Requirements

Secure Expiring Paylink Tokens
"As a student, I want reminder links that safely expire and are single-use so that my payment and booking can be confirmed securely with one tap without risking misuse."
Description

Generate cryptographically signed, single-use TapPass URLs embedded in SMS/email reminders that expire after a configurable TTL and are scoped to a specific user, class instance, and price. Tokens must be tamper-evident (e.g., HMAC with rotating keys), support optional device/IP binding, and be invalidated after first successful use or upon manual revoke. Include short-linking with studio-branded domains and deep-linking into the mobile web/PWA checkout. Server validates claims (user, class, seat, price, expires_at, nonce) and returns friendly states for expired/invalid links with safe recovery paths (e.g., regenerate or navigate to class page). All events (issued, opened, redeemed, expired) are logged for audit and analytics. Outcome: secure, trustable links that mitigate fraud and misuse while enabling true one-tap flows.

Acceptance Criteria
Signed Token Issuance with Required Claims and Configurable TTL
Given a reminder is generated for a specific user, class instance, seat, and price When the server issues a TapPass token Then the token is HMAC-signed using the active key and includes claims: user_id, class_instance_id, seat_id, price_id or price_amount, expires_at, nonce And the token includes a key identifier to support rotation and validates with any unexpired active or grace-period key And the TTL is set from studio configuration within allowed min/max bounds And the nonce is globally unique and has at least 128 bits of entropy And any modification to any claim causes signature verification to fail and the server returns an "invalid link" friendly state with a safe recovery path
Single-Use Redemption, Idempotency, and Manual Revoke
Given a valid, unexpired, unrevoked TapPass token When the user opens the link and confirms Then validation and redemption complete atomically and the token is marked redeemed immediately And exactly one of any concurrent requests succeeds; all others return an "already used" friendly state with a link to the class page or to request a new link And subsequent attempts to use the token after redemption return "already used" with no additional charge or seat allocation And if the token is manually revoked before redemption, any attempt returns a "revoked" state with options to regenerate a new link or navigate to the class page
Expiration Handling with Friendly Recovery
Given a TapPass token with an expires_at claim When the current server time is greater than expires_at Then the server rejects redemption and displays an "expired" friendly state explaining the timeout window And, if the user is authenticated as the token's user and a seat is still available, an option to generate a fresh link is presented And, if not authenticated or no seats remain, the user is directed to the class page or waitlist without processing payment
Scope and Price Enforcement
Given a TapPass token scoped to a specific user, class instance, seat, and price When the link is accessed by a different authenticated user or the class instance/seat/price in the request does not match the token claims Then the server rejects the attempt with an "invalid scope" friendly state and provides navigation to the correct class page or sign-in as the intended user And the server uses only the price in the token for charging and rejects any client-supplied price overrides
Optional Device/IP Binding
Given device/IP binding is enabled for the studio When a TapPass link is first opened Then the server binds the token to the opening device fingerprint and/or IP per configuration And later redemption from a device/IP outside the configured tolerance is rejected with a "device mismatch" state and an option to request a new link And when binding is disabled, the token redeems regardless of device/IP
Branded Short Links and Deep Linking
Given a TapPass token is issued for reminder delivery When the system generates the link for SMS/email Then the link uses the studio-branded HTTPS short domain and resolves within 300 ms to a deep link And the deep link opens the PWA/mobile web checkout directly to one-tap confirm for the target class instance And if the PWA is unavailable, the user is routed to the responsive mobile web checkout with the same state And tracking parameters (e.g., channel) are preserved end-to-end without exposing PII in the URL
Event Logging and Analytics
Given TapPass tokens are issued and interacted with When events occur (issued, opened, redeemed, expired, revoked) Then an immutable event is written within 2 seconds with fields: token_id or nonce, user_id, class_instance_id, timestamp, channel, user_agent, IP (if available), event_type, outcome And duplicate deliveries are deduplicated by token_id+event_type within a 10-minute window And events are queryable via audit API and included in analytics aggregates within 15 minutes
One-Tap Confirm & Payment
"As a student, I want to confirm and pay for a class with a single tap from a reminder so that I don’t miss out due to a lengthy checkout process."
Description

Provide a frictionless flow where opening a TapPass instantly loads a confirmation screen with pre-filled class details and a single confirm action that triggers payment and booking. Use stored payment methods (Stripe Customer) or native wallets (Apple Pay/Google Pay) and seamlessly handle SCA/3DS challenges within the flow; fall back to standard checkout if no method is available. Ensure idempotent bookings via Payment Intents and idempotency keys, support taxes, promo codes, credits/passes, and send receipt plus calendar attachment upon success. Present clear error/retry states for declines or interrupted authentications without losing the reservation. Outcome: higher conversion and fewer abandoned checkouts from reminders.

Acceptance Criteria
Stored Card One-Tap Success with Taxes and Promo
Given the recipient opens a valid, unexpired TapPass to a class with at least one available seat and has a Stripe Customer with a default saved card and an applicable promo code When they tap Confirm Then a Stripe Payment Intent is created with the discounted subtotal, taxes computed per venue jurisdiction, correct currency, and a unique idempotency key tied to the TapPass And if SCA is required, an in-flow 3DS challenge is presented and, upon completion, the payment is confirmed without redirect And upon successful confirmation, exactly one booking is created and linked to the Payment Intent And the success screen renders within 3 seconds of payment confirmation with class details and a View Booking action And a receipt via email/SMS including itemized taxes and discount is sent within 60 seconds And a .ics calendar attachment with accurate start/end time, timezone, and location is included in the receipt And the TapPass is marked consumed and cannot be reused
Native Wallet with SCA Challenge
Given the device supports Apple Pay or Google Pay with at least one active card and the issuer requires SCA When the user taps Confirm and chooses the native wallet Then the wallet sheet displays the correct payee name, amount, currency, and line-item summary if supported And the 3DS/SCA challenge occurs within the wallet flow and can be completed without leaving the app or page And on success, the Payment Intent is confirmed and the booking is created within the same session And on cancel or timeout, an explicit error message with Retry and Change Payment Method options is shown And the seat hold persists for at least 5 minutes from initial open
No Saved Method Fallback to Standard Checkout
Given no saved payment methods exist for the user and native wallets are unavailable When the TapPass is opened and Confirm is tapped Then the user is routed to the standard checkout with class, price, promo, and taxes pre-filled and non-editable class details And after successful checkout, a booking is created and a receipt + .ics are sent within 60 seconds And if checkout is abandoned, no booking is created, no charge occurs, and the TapPass remains usable until its expiration
Idempotent Confirm and Network Retry
Given the same TapPass triggers multiple Confirm requests within a 60-second window due to double tap or network retry When the backend processes the requests Then the same idempotency key is used to ensure a single Payment Intent confirmation And at most one booking record exists; subsequent requests return the same booking ID and status And only one charge appears in the payment processor and on the customer's statement And telemetry logs include a single success event and deduplicated retries
Declines and Recovery Without Losing Reservation
Given the payment attempt fails due to decline, authentication failure, or processor error When the failure occurs Then the user sees a clear error message with an actionable reason category and a masked reference code And a Retry Payment option is available without reloading the page And a Change Payment Method option allows selecting another saved card or opening standard checkout And the seat remains reserved for at least 5 minutes from the first confirm attempt; upon expiry, a message indicates the hold has ended And no booking or receipt is created until a successful payment occurs
Credits/Passes Application in One-Tap
Given the user has available credits or an active pass applicable to the class When the TapPass is opened and Confirm is tapped Then credits/passes are applied before charging any card per business rules And if the total is $0.00 after application, the booking is created immediately with no payment confirmation required And if a partial balance remains, only the remainder is charged to the default method or wallet And taxes are computed on the correct taxable base after credits and discounts And the receipt itemizes credit/pass usage and remaining balances
TapPass Expiry, Single Use, and Security
Given a TapPass is expired, already used, or the token is invalid When the link is opened Then an Expired or Already Used state is shown with class details and a CTA to view availability or join the waitlist And the link cannot initiate payment or create a booking And all accesses are logged with timestamp and IP for audit And excessive invalid token attempts (>=10 within 5 minutes from one IP) are rate limited with HTTP 429
Reminder Orchestration & Delivery (SMS/Email)
"As an instructor, I want automated reminders with embedded paylinks sent at the right times and channels so that my classes stay full without extra admin work."
Description

Automate scheduling and delivery of reminders that include TapPass links for upcoming classes, last-minute fills, abandoned confirmations, and re-engagement. Support timezone-aware scheduling, quiet hours, per-studio send windows, rate limits, and per-contact channel preferences with compliant opt-in/opt-out (STOP/unsubscribe). Provide templating with personalization (name, class, location, time, spots left), per-brand assets, and safe link insertion. Integrate with Twilio (SMS) and SendGrid/SES (email), with delivery status tracking and graceful retries. Ensure deduplication across channels and enforce frequency caps to avoid spam. Outcome: timely, relevant reminders that reduce no-shows and drive confirmations.

Acceptance Criteria
Timezone-Aware Quiet Hours & Send Windows
Given Studio A has a send window of 08:00–20:00 local and quiet hours enabled, and a learner in America/Chicago timezone, When a reminder is scheduled at 07:30 local, Then it is deferred and sent at 08:00 local. Given the same settings, When a reminder is scheduled at 19:59 local, Then it is sent before 20:00 local and not deferred to the next day. Given the same settings and class start at 07:45 local, When the next allowable send time (08:00) is after class start, Then the reminder is suppressed and logged as "suppressed: missed window". Given per-studio override "allow last-minute outside window" is false, When a vacancy alert would be sent at 22:00 local, Then it is deferred to 08:00 local. Given a daylight saving time transition, When a reminder is scheduled across the DST shift, Then the scheduled local time is preserved relative to wall-clock time.
Channel Preferences, Opt-In/Opt-Out Compliance
Given a contact with SMS=opted-in, Email=opted-out and preferred channel=SMS, When a reminder is orchestrated, Then only an SMS is attempted. Given a contact sends "STOP" to any ClassTap number, When the STOP MO is received, Then the contact's SMS consent is set to opted-out within 1 second and a single compliance confirmation is sent, and no further SMS are sent until "START" is received. Given a contact clicks the email unsubscribe link, When the webhook confirms unsubscribe, Then Email consent is set to opted-out and future emails are suppressed. Given a contact re-subscribes via double opt-in email, When confirmed, Then Email consent is set to opted-in and orchestration may use Email again. Given a per-contact do-not-disturb window, When inside DND, Then no channel is used even if opted-in, and the attempt is rescheduled or suppressed per policy.
Secure TapPass Link Generation and Behavior
Given a reminder requires a TapPass link, When generating the message, Then a unique, signed, non-guessable HTTPS link on the brand domain is inserted without breaking template formatting. Given link TTL is class_start_time or a configured expiry (e.g., 120 minutes after send), When the link is opened before expiry, Then one-tap confirmation succeeds and attendee status updates to Confirmed within 2 seconds. Given the link is opened after expiry, Then the user sees a "Link expired" screen with options to view schedule or join waitlist and no confirmation is applied. Given the recipient has already confirmed or canceled, When the link is opened, Then an idempotent response is shown and no duplicate state change occurs. Given PII constraints, When generating the link, Then no email, phone, or name is embedded in the URL path or query string.
Cross-Channel Deduplication and Frequency Caps
Given both SMS and Email are eligible, When SMS is delivered (DLR=delivered) within 5 minutes of send, Then the corresponding email reminder for the same intent is canceled. Given SMS fails permanently (e.g., Twilio 30003) or no DLR after 5 minutes, When Email is allowed, Then an email is sent as fallback once. Given frequency caps configured (e.g., max 2 reminders per contact per 24h; max 1 reminder per class per contact), When a send would exceed a cap, Then the reminder is suppressed and the suppression reason is recorded. Given separate caps for promotional vs transactional, When the reminder is transactional (upcoming class), Then it does not count against the promotional cap, but still respects the per-class cap.
Provider Integration, Throttling, Retries, and Status Tracking
Given a Twilio SMS send rate limit of 1 msg/sec per sender, When a batch of 120 messages is queued, Then throughput is throttled to the configured rate with jitter and completes without provider 429 errors. Given a transient provider error (HTTP 5xx or Twilio 30001), When sending, Then retries occur with exponential backoff (e.g., 1s, 4s, 16s) up to 3 attempts before marking as Failed. Given SendGrid/SES webhooks, When events Delivered, Bounce, Deferred, SpamReport, and Unsubscribe are received, Then the message status timeline is updated and email consent is adjusted (Bounce/SpamReport -> suppress future sends). Given permanent SMS errors or carrier blocks, When detected, Then SMS consent is not changed automatically but the number is flagged for review and SMS is suppressed for 7 days. Given a message is sent, When viewing delivery logs, Then the system shows provider message ID, send time, attempts, final status, and error codes.
Template Personalization and Brand Assets
Given a template with placeholders {{first_name}}, {{class_title}}, {{location}}, {{start_time_local}}, and {{spots_left}}, and brand header/footer, When rendering, Then all placeholders resolve correctly using the recipient's locale and the brand assets are applied. Given a missing optional field (e.g., spots_left), When rendering, Then a safe default or omission is used without broken syntax. Given SMS content length and encoding, When the personalized SMS exceeds 160 GSM-7 chars or includes Unicode, Then it is segmented per standard and total parts do not exceed the configured max (e.g., 2 parts); otherwise the message is truncated with ellipsis and the TapPass link is preserved. Given HTML email templates, When rendering, Then HTML is sanitized, inline CSS applied, images have alt text, and a visible unsubscribe link is present. Given preview mode, When a studio admin previews a template for a selected recipient and class, Then the preview matches the final rendered output and flags any unresolved variables.
Event Triggers: Upcoming, Vacancy Fills, Abandoned Confirmation, Re-Engagement
Given an upcoming class reminder rule (e.g., 24h and 2h before start), When a class is scheduled, Then reminders are enqueued at the correct local times respecting quiet hours and caps. Given a booked seat is released within 3 hours of class start and a waitlist exists, When vacancy detection runs, Then a TapPass vacancy SMS/email is sent to the top waitlisted contact with a 15-minute hold; if unclaimed, it cascades to the next without double-booking. Given a booking is started but not confirmed within 10 minutes, When abandonment is detected, Then a single TapPass reminder is sent via the preferred channel. Given a contact has had no bookings for 30 days and is opted-in, When re-engagement rules run, Then a personalized TapPass offer is sent once, respecting promotional caps.
Waitlist Auto-Promotion Paylinks
"As a waitlisted student, I want an instant paylink when a spot opens so that I can claim it quickly before someone else does."
Description

When a seat becomes available, automatically notify the next eligible waitlisted users with prioritized, expiring TapPass links and a clear hold window. Enforce first-confirmed-first-served rules, fairness (e.g., FIFO with tie-breakers), and maximum active offers per user. Cascade notifications to subsequent waitlisters if the paylink expires or is declined, and update waitlist/roster states in real time. Provide instructor-facing controls for promotion pace and capacity thresholds. Outcome: rapid, hands-off conversion of waitlists into paid bookings.

Acceptance Criteria
FIFO Promotion With Tie-Breakers and Hold Window
Given a class session has ≥1 open seat and a waitlist with N users, When auto-promotion runs, Then candidates are selected strictly by waitlist_position ascending (FIFO); if created_at timestamps are identical, the lower waitlist_entry_id is prioritized. Given promotion pace batch_size = K, When notifications are sent, Then only the top K eligible users receive TapPass links in this wave. Given a TapPass is sent, When the user receives the SMS/email, Then the message includes the explicit expiration timestamp and the remaining minutes of the hold window in the class’s local timezone. Given a TapPass has hold_window_minutes = H, When current_time ≥ sent_at + H minutes, Then the TapPass becomes invalid and cannot be used to confirm.
Max Active Offers Per User
Given max_active_offers_per_user_for_session = 1, When a user already has an active TapPass for the session, Then the system does not send another offer to that user in subsequent waves and logs skip_reason = "active_offer_exists". Given an active TapPass is confirmed, expired, or declined, When eligibility is next evaluated, Then the user’s active offer count for that session is updated within 3 seconds accordingly. Given the instructor sets max_active_offers_per_user_for_session to X (1..3), When promotion runs, Then no user receives more than X simultaneous active TapPass links for that session.
First-Confirmed-First-Served Enforcement
Given multiple active TapPass offers exist for the same session, When any recipient taps confirm and payment authorization succeeds, Then that user is added to the roster immediately and all other active offers for that session are auto-invalidated with a "seat_taken" notice within 5 seconds. Given a user taps confirm but payment authorization fails, When the attempt completes, Then the seat is not allocated and all other active offers remain valid. Given the roster reaches capacity, When any outstanding TapPass link is opened, Then it displays "class full" and cannot be used to book.
Cascading Promotions After Expiry or Decline
Given an active TapPass expires or is explicitly declined, When the cascade job runs, Then the next eligible waitlisted user(s) are notified within 60 seconds, respecting the configured batch_size. Given there are fewer waitlisted users than batch_size, When the cascade triggers, Then offers are sent only to the remaining eligible users and the wave completes without error. Given all waitlisted users have been offered and all offers expire/decline with no confirmations, When the seat remains open, Then the instructor is notified according to notification preferences.
Real-Time Waitlist and Roster Synchronization
Given any of these events occur: offer_sent, offer_expired, offer_declined, confirm_success, confirm_failure, cancellation, When viewed on the instructor dashboard or student waitlist page, Then waitlist positions and roster counts reflect the event within 3 seconds end-to-end. Given two users open their TapPass pages concurrently, When one confirms, Then the other user’s page reflects a "seat taken" state within 5 seconds without requiring a full page reload. Given audit logging is enabled, When promotions and confirmations occur, Then immutable audit entries record user_id, session_id, event_type, timestamp, prior_state, and new_state.
Instructor Promotion Pace and Threshold Controls
Given the instructor sets promotion pace batch_size = K and wave_delay_minutes = D, When seats are available, Then offers are sent in waves of K every D minutes until seats fill or the waitlist is exhausted. Given the instructor sets a start condition of at_least_open_seats = S and a last_minute_cutoff_minutes = C, When conditions are evaluated, Then auto-promotion only runs while open_seats ≥ S and never sends offers less than C minutes before class start. Given the instructor updates any of these settings, When saved, Then subsequent promotion decisions reflect the change within 1 minute and settings persist across sessions and browser refresh.
Secure, Expiring TapPass One-Tap Confirm
Given a TapPass is generated, When inspected, Then it is single-use, cryptographically signed, scoped to {user_id, session_id}, and expires at sent_at + hold_window_minutes. Given a TapPass is opened by a non-matching user or after it is used/expired/invalidated, When accessed, Then the system returns an invalid state (API: HTTP 401/403/410; UI: "Link invalid or expired") and never creates or holds a booking. Given the user has a stored payment method, When they open the TapPass, Then they can confirm with one tap and receive booking confirmation and receipt within 10 seconds; if no stored method, Then a minimal, prefilled payment form is presented and confirmation succeeds end-to-end under 90 seconds at P95.
Inventory Hold & Double-Booking Prevention
"As a student, I want the seat held when I open a paylink so that I don’t lose it to someone else while I’m confirming payment."
Description

Reserve a seat for a short, configurable hold period when a TapPass is opened, displaying a countdown to the user. Use atomic reservations (e.g., Redis locks plus DB transactions) to prevent race conditions across multiple paylinks and channels, and release the hold on timeout or failed payment. Ensure the booking operation is idempotent and only the first successful confirmation finalizes the seat; others see a friendly ‘seat taken’ or waitlist option. Log state transitions for traceability and reconciliation. Outcome: no double-bookings and a trustworthy checkout experience during high demand.

Acceptance Criteria
Seat Hold on TapPass Open
Given a class with N available seats and hold duration configured to 5 minutes And a valid, unexpired TapPass link for that class When the user opens the TapPass page Then a seat is reserved exclusively for that user for 5 minutes And public availability decreases by 1 within 1 second And a visible countdown timer displays 5:00 and decrements every second And the countdown persists across page refresh within the hold window And no payment is captured until confirmation
Hold Release on Timeout and Payment Failure
Given a seat is on hold for a user via TapPass When the countdown reaches 0 without successful payment Then the hold is released within 2 seconds And public availability increases by 1 And the user sees "Hold expired" with options to retry or join the waitlist Given a seat is on hold and the user submits payment that is declined When the processor returns a non-success status Then the hold is released within 2 seconds And the user sees a clear error message with a retry option And no booking record is finalized And no charge is captured
Idempotent Booking and Duplicate Click/Webhook Protection
Given a seat is on hold for a user When the user clicks Confirm multiple times within the hold window Then exactly one booking record is created And subsequent duplicate requests return the same booking reference with a 200 idempotent response And the user is charged exactly once Given the payment provider sends duplicate webhook events for the same transaction When the system processes the events Then no duplicate booking is created And duplicate events are acknowledged without side effects
Atomic Reservation Under Concurrent Confirmation Attempts
Given exactly one seat remains in the class And two different users have active holds via separate TapPass links When both users attempt to confirm within 100 ms of each other Then exactly one confirmation succeeds and finalizes the booking And the other attempt fails gracefully with a "Seat taken" message And the final class roster contains no duplicate attendees And retries by the losing attempt remain prevented until availability increases
Friendly Seat Taken Messaging with Waitlist Option
Given a user's confirmation attempt fails because the last seat was taken by another user When the failure response is returned Then the user sees a "Seat taken" message within 1 second with a CTA to join the waitlist And selecting the waitlist CTA adds the user to the correct class waitlist And the user receives SMS/email confirmation of waitlist status And any existing hold for the user is immediately released
Reservation Lock TTL and Consistency Across Services
Given the hold duration is configured to 5 minutes When a reservation hold is created Then the reservation lock TTL equals 5 minutes And the lock is automatically released at TTL expiry even if services restart And additional holds for the last remaining seat are prevented until the lock expires And creating a second hold for the same user and class before expiry is rejected And system metrics expose counts of active holds, confirmations, and expirations
State Transition Logging and Audit Trail
Given a TapPass flow from link open through confirmation or expiry When any state change occurs (opened, hold_created, payment_submitted, payment_succeeded, payment_failed, hold_released, booking_confirmed) Then an audit log entry is written with timestamp, correlation_id, user (or anonymous token), class_id, previous_state, and new_state And log entries can be queried by correlation_id to reconstruct the ordered sequence And each transition is recorded once (no duplicates) with monotonic timestamps And a reconciliation report over a time range lists counts of holds created, expired, and confirmed that match database records within 0.1%
Quick Rebook Paylinks
"As a returning student, I want a one-tap link to rebook my next class so that I can maintain my routine without searching the schedule."
Description

Enable generation of TapPass links that pre-select the next logical session or a preferred schedule series for a student, using rules like same day/time next week, same instructor, or recommended progression. Include membership/credit eligibility checks, pricing application, and calendar-friendly timings. Trigger after attendance, successful completion, or instructor prompts, and send via preferred channel with clear one-tap confirm. Provide fallback browsing for alternate dates if the suggested slot is unavailable. Outcome: increased repeat bookings with near-zero friction.

Acceptance Criteria
Auto-send next-week same-slot quick rebook after attendance
Given a student is marked Attended for a class instance and the class has a recurring schedule When the attendance event is saved Then the system computes the next logical session using the rule “same day and start time next week” in the student’s timezone, skipping blackout dates and instructor time-off, and limiting search to the next 8 weeks And the suggested session preserves location and format (in-person/hybrid/online) and prefers the same instructor when available And the suggestion is only created if the target session has available capacity and is bookable per studio rules And a Quick Rebook suggestion record is created with metadata (classId, sessionId, rule, generatedAt) within 2 minutes of attendance save And a TapPass link is generated referencing the suggested session with a default expiry of 72 hours and marked single-use And an event QuickRebookSuggested is logged with studentId, classId, sessionId, rule, channel-intent
Instructor-prompted quick rebook for same-instructor series
Given an instructor opens today’s roster and selects Prompt Rebook for a student When the instructor confirms the rule “same instructor recommended progression” Then the system suggests the next session in the defined series or progression map taught by the same instructor within the next 6 weeks And if no same-instructor session exists, the system suggests the same class type with closest time slot only if cross-instructor rebook is enabled for that class And the TapPass link is generated in under 5 seconds and associated to the instructor-initiated campaign type And the suggestion includes an optional note from the instructor rendered in the message template And audit logs capture instructorId, studentId, rule, suggestedSessionId
Membership/credit eligibility and pricing application on quick rebook
Given a student has one or more active products (membership, class pack, credits) and may have a saved payment method When the student taps the TapPass link Then eligibility is validated against the target session date/time and product rules (credit balance, blackout classes, grace windows, membership validity) And if eligible credit exists, the one-tap confirm applies the credit and sets price to $0 at confirm time And if no eligible credit exists but a saved payment method is on file, the one-tap confirm charges the correct price (including taxes/fees/discounts) using the student’s default method and issues a receipt And if payment requires SCA/3DS, the flow seamlessly prompts for authentication and completes the booking upon success, or gracefully returns to a payable state upon failure without creating a booking And all pricing and deductions occur atomically with booking creation and are reversed automatically if booking creation fails
Secure expiring TapPass and one-tap confirm flow
Given a student receives a TapPass link for a suggested session When the link is opened before expiry Then the system validates a signed, tamper-evident token (HMAC or equivalent), student-session binding, and single-use status And on one-tap confirm, if capacity and eligibility checks pass, a booking is created, confirmation page is shown, and email/SMS confirmation is sent within 2 seconds p95 And repeat taps or duplicate opens return the existing booking confirmation without creating duplicates (idempotency key enforced) And if the token is expired, revoked, or invalid, the user is redirected to the fallback browsing experience with a non-technical message And no personally identifiable information is encoded in the link; tokens expire by default in 72 hours and can be overridden per studio policy
Fallback browsing when suggested slot is unavailable
Given a student opens a TapPass link and the suggested session is full, canceled, or no longer valid When the system detects unavailability or rule failure Then the student is redirected to a filtered class browser showing the same class type, location, and instructor (if applicable) with the next 10 upcoming dates/times And the student can select an alternative date in one click and confirm in one additional tap using the same eligibility and payment rules And if no alternatives exist within the next 8 weeks, the interface offers to join the waitlist or browse related classes And analytics track fallback_opened and fallback_converted events And the experience preserves timezone-correct times and any applicable pricing or credits
Conflict and double-booking prevention during quick rebook
Given a student opens a TapPass link When they confirm the rebook Then the system validates there is no time overlap with the student’s existing bookings (including buffer rules) and that the class capacity will not be exceeded And if a conflict exists, the student is shown a clear message with options to view alternatives; no booking is created and no charges or credit deductions occur And booking creation, capacity decrement, and payment/credit deduction occur in a single atomic transaction with row-level locking to prevent oversell And concurrent confirmations on the last remaining seat result in exactly one successful booking and a graceful Sold Out message for others
Preferred channel delivery and messaging clarity
Given a student has a recorded communication preference (SMS, Email, or Both) and valid contact info When a quick rebook suggestion is generated Then the message is sent via the preferred channel within 2 minutes (p95), with automatic fallback to the alternate channel if a bounce or hard failure occurs And the message includes class name, date, local time with timezone abbreviation, instructor, venue, price/credit usage summary, CTA label “Confirm in 1 tap”, and opt-out instructions (STOP for SMS / unsubscribe link for email) And messages respect DND/quiet hours policies and opt-outs; no message is sent outside allowed windows And open and conversion events are tracked and attributed to the delivery channel with campaign identifiers
Admin Controls & Performance Analytics
"As a studio owner, I want to configure paylink reminders and see conversion metrics so that I can optimize attendance and prove ROI."
Description

Offer a dashboard where studios configure reminder schedules, TapPass expiry/hold durations, templates, channels, quiet hours, and targeting rules. Provide preview/test-send tools, role-based access, and brand customization (logos, colors, sender IDs, link domains). Deliver analytics on sends, opens, clicks, paylink redemption, conversion, revenue uplift, fill rate, and no-show reduction with cohort and A/B comparisons. Include export APIs, data retention controls, GDPR/CCPA compliance, and audit logs for link lifecycle events. Outcome: studios can tune paylink strategy and prove impact on attendance and revenue.

Acceptance Criteria
Configure Reminder Schedules, Quiet Hours, Channels, and Targeting
Given a studio admin with permission "Reminders:Configure" When they create reminder rules (e.g., T-24h, T-3h, T-30m) and select channels per rule (SMS, Email) And they set quiet hours (e.g., 21:00–08:00) with defer policy "send at 08:00" And they add targeting rules (e.g., unpaid reservations only, first-time attendees, waitlist-promoted) Then the UI validates conflicts and consent (no rule causes >3 messages per reservation, channels require user opt-in) And the configuration is versioned with an effective-from timestamp and saved successfully And a preview shows the next 7 days’ projected send counts by class/location/channel And a test-send to a verified contact returns within 10 seconds and includes correct merge fields and a valid TapPass link And reminders queued during quiet hours are deferred and delivered at the configured resume time
TapPass Expiry and Hold Duration Settings with Audit Trail
Given expiry duration (e.g., 30 minutes) and seat hold duration (e.g., 10 minutes) are configured When a reminder containing a TapPass is sent Then the link expires exactly at send_time + expiry_duration and returns an "expired" page if opened after expiry And the first "Confirm" click applies a seat hold immediately and auto-releases precisely at hold_duration if payment is not completed And a redeemed link cannot be reused or replayed (subsequent attempts show "already redeemed") And all lifecycle events (generated, delivered, opened, clicked, confirmed, paid, expired, hold_set, hold_released) are recorded with ISO timestamps, channel, and user/session in an immutable audit log And multiple links for the same reservation are uniquely tokenized and independently auditable
Brand Customization and Sender Identity Verification
Given a studio uploads a logo and sets brand colors and typography defaults And configures sender IDs (registered SMS sender/number and verified email domain) and a branded link domain (CNAME) When previewing SMS and email templates Then the preview and test-send render brand assets correctly, apply configured colors, and pass WCAG AA contrast checks And links resolve under the branded domain via HTTPS with a valid certificate (no mixed content) and redirect to the one-tap confirm page And email passes SPF/DKIM/DMARC alignment; SMS uses the registered sender; if not verified, the system blocks sending and surfaces actionable errors And fallbacks (default domain/sender) are applied only when configured and recorded in the audit log
Role-Based Access and Permissions Enforcement
Given roles Admin, Manager, Instructor, and Analyst with defined permissions When a user without "Reminders:Configure" attempts to modify schedules, templates, or expiry settings Then the action is blocked with HTTP 403/UI error and an audit entry is recorded (actor, action, resource, timestamp) When an Analyst accesses Analytics Then KPIs are viewable but configuration pages and PII (phone/email bodies) are hidden or masked And permission changes take effect within 1 minute and are reflected in access tokens/sessions upon refresh And all settings edits show who/when/what changed with before/after diffs
Analytics KPI Accuracy and A/B Cohort Comparison
Given reminders have been running for at least 7 days When viewing Analytics by dimension (date, class, instructor, location, channel, template, cohort, A/B variant) Then KPIs display: sends, delivered, opens, clicks, unique paylink redemptions, conversions (paid), revenue uplift vs control, fill rate, no-show reduction, with visible metric definitions And data freshness is ≤15 minutes; dashboard totals reconcile with the export within ±1% for the same filters and time range And an A/B test with a configured holdout/control computes uplift and shows a significance flag when p < 0.05 and sample size ≥ configured minimum And filters and timezones apply consistently across all charts and tables; CSV download reflects active filters
Export API and Data Retention (GDPR/CCPA) Compliance
Given an API key with scope "Analytics:Export" When calling the exports endpoint with date range, dimensions, and metrics Then the API responds within 60 seconds for up to 100k rows or returns an async job handle for larger requests, with paginated results and a documented schema And opt-outs/consent states are respected (no messages sent to opted-out users; analytics flags opt-out events) When a data subject deletion request is executed Then personal data in reminders, analytics, and message bodies is deleted or irrevocably pseudonymized within 30 days, while audit logs retain non-PII event metadata And admin-configured retention periods purge message content and event data on schedule, with purge jobs logged and verifiable by record counts

Smart Expiry

Automatically adjusts each booking’s join-link validity window to open just before class and expire shortly after, with time zone awareness and class-type rules. Prevents early link leaks and late access issues so attendees join at the right moment without confusion.

Requirements

Dynamic Validity Window Engine
"As an attendee, I want my join link to become active shortly before class and expire soon after so that I can join at the right time without confusion or unauthorized reuse."
Description

Implement a backend service that computes and enforces open/expire times for each booking’s access link based on scheduled class start/end times and configurable offsets (e.g., open 10 minutes before, expire 15 minutes after). Store validity windows per booking (valid_from, valid_to) and reevaluate upon schedule changes. Enforce the window at the API layer and in link resolution so links cannot be used outside the active interval. Provide configuration at account and class levels, with sensible defaults, and ensure high reliability under peak traffic. Expected outcome: links activate just before class and close shortly after, reducing early access and post-class misuse.

Acceptance Criteria
Per-Booking Validity Window Computation on Creation
Given an account default of open_before=10m and expire_after=15m, and a class scheduled 10:00–11:00 in IANA time zone 'America/New_York' with no class-level override When a booking transitions to Confirmed Then valid_from = class_start - 10m (localized to the class time zone) and valid_to = class_end + 15m And valid_from/valid_to are persisted per booking, retrievable via GET /bookings/{id}, non-null, and immutable except via schedule/config changes And timestamps are stored in UTC with the class time zone id retained; precision ≥ 1 second; no rounding beyond 1s
Access Enforcement at API and Link Resolver
Given a booking link is requested before valid_from When resolving the link Then respond 403 LINK_NOT_ACTIVE and do not redirect Given a booking link is requested within [valid_from, valid_to] When resolving the link Then respond 302 to join_url and record an access event with timestamp and booking_id Given a booking link is requested after valid_to When resolving the link Then respond 410 LINK_EXPIRED and do not redirect And all API endpoints that gate access by booking_id enforce the same window semantics And a clock-skew tolerance of ±60s is applied consistently; attempts outside tolerance are denied And CDN/cache respects per-booking TTL ≤ 5s and never serves a redirect outside [valid_from, valid_to]
Automatic Recalculation on Schedule Changes
Given class start and/or end times are updated When the change is saved Then all non-canceled bookings recompute valid_from/valid_to within 30s And if a class is canceled Then all associated booking links expire immediately (valid_to set to now) and further access returns 410 LINK_EXPIRED And if a booking has lock_window=true override Then its window is not modified by schedule changes And an audit log entry is recorded per booking with old/new values and change cause (schedule_update|cancel|config_update) And re-computation is idempotent and resilient to retries, yielding exactly one final state per booking
Time Zone and DST Correctness
Given a class time zone is stored as an IANA identifier (e.g., America/New_York) When computing windows on DST start day where 02:00–03:00 is skipped and class start is set to 02:30 local Then the engine normalizes start to the next valid wall time (03:30 local) and computes valid_from/open_before and valid_to/expire_after from that normalized time When computing windows on DST end day with an ambiguous time (e.g., start 01:30) Then the engine resolves using the stored offset for the scheduled occurrence and computes windows accordingly And no computation yields invalid/NaT timestamps; unit tests cover spring-forward and fall-back cases with expected valid_from/valid_to assertions
Configuration Precedence and Validation
Given system default offsets (open_before=10m, expire_after=15m), account-level override (5m, 20m), class-level override (0m, 30m), and class-type rule 'Online' min_open_before=2m When computing a booking for an 'Online' class with a class-level override present Then precedence is class-level > class-type rule > account-level > system default, yielding (0m, 30m) but honoring min_open_before by clamping to 2m if override < 2m And offset values must be integers in minutes within [0, 120]; invalid configs are rejected with 400 and do not alter existing bookings And configuration changes affect only future computations and any re-evaluations triggered by explicit schedule/config change events
Peak Traffic Performance and Reliability
Given 500 RPS sustained link resolutions with 10k concurrent active windows and cache hit rate ≥ 85% When enforcing validity windows under load Then p95 latency ≤ 150ms and p99 ≤ 400ms for link resolution endpoints And ≥ 99.95% of requests are enforced correctly with no false accepts beyond the ±60s skew tolerance during a 60-minute soak test And zero data loss of valid_from/valid_to during node restarts or deploys; replication lag ≤ 1s does not produce incorrect allows And on cache outage the system degrades safely, enforcing from the source of truth with p95 ≤ 600ms
Waitlist Promotion and Reschedule Handling
Given a learner is promoted from waitlist to confirmed When promotion occurs Then compute and persist a new validity window based on the current schedule and configuration And any previous booking link for the same learner and class instance is invalidated immediately (returns 410) When a confirmed booking is rescheduled to a different class time Then the old link expires immediately and a new link resolves only within the new window And all changes propagate to link resolution within 30s; no window overlaps allow access to both old and new sessions
Global Time Zone & DST Safety
"As a global attendee, I want the link timing to respect my local time zone and DST so that I never miss class due to time conversion errors."
Description

Normalize all class schedules to UTC and use IANA time zones to compute per-booking validity windows with full daylight-saving awareness. Ensure windows display and function correctly across attendee locales, including edge cases like DST transitions, windows spanning midnight, and cross-border classes. Provide consistent local-time messaging in UI, email, and SMS. Include automated tests for major time zones and DST boundaries to prevent regressions.

Acceptance Criteria
UTC Normalization & IANA Zone Enforcement
Given a class is scheduled using an IANA time zone (e.g., Europe/Paris) with class-type rules (open N minutes before start, expire M minutes after start) When a booking is created and the validity window is computed Then the class start and both validity window bounds are stored in UTC with the original IANA zone id recorded And fixed-offset time zones (e.g., UTC+01:00) are rejected at scheduling time with a validation error And all API responses include both the UTC instants and the IANA zone id for start/open/expiry And the join-link activation/inactivation logic uses the stored UTC instants and is invariant to the viewer’s device or locale time settings
DST Spring Forward Window Correctness (Europe/Berlin)
Given a class scheduled at 03:05 on the local day of DST start in Europe/Berlin with rules open 10 minutes before and expire 15 minutes after start When the pre-class open time (03:05 − 10 minutes) falls into the DST spring gap (a non-existent local time) Then the join-link open time is set to the first valid local instant after the gap and its exact UTC instant is persisted And UI, email, and SMS all display the same local open and expiry times with the correct zone abbreviation (e.g., CEST) and without contradictory times And automated tests assert that the open time is not earlier than the first valid post-gap instant and that activation matches the persisted UTC instant
DST Fall Back Ambiguity Resolution (America/New_York)
Given two classes scheduled at 01:30 on the local day of DST end in America/New_York where one is explicitly resolved to the first occurrence (EDT) and the other to the second occurrence (EST) When bookings are created with rules open 10 minutes before and expire 15 minutes after start Then each class’s validity window open/expire are computed from their resolved UTC instants (fold-aware) with no cross-assignment And UI, email, and SMS display the correct local offset/abbreviation (EDT vs EST) for each class consistently And automated tests verify both folds produce distinct UTC instants and correct activation/inactivation in real time
Validity Window Spanning Midnight
Given a class scheduled at 00:05 local time with rules open 15 minutes before and expire 10 minutes after start When the computed open time is on the previous local calendar day and the expiry is on the class day Then the join-link activates at the previous local day’s time and deactivates at the correct local time on the class day And all surfaces (UI, email, SMS) display the correct local dates and times including day-of-week to avoid ambiguity And automated tests assert correct date roll-over and matching UTC instants for open/expiry across attendee locales
Cross-Zone Attendee Messaging Consistency
Given a class scheduled in the instructor’s IANA zone (e.g., Europe/London) and an attendee with profile/browser time zone America/Los_Angeles When the system renders the booking confirmation, reminder email, and SMS Then all surfaces display the attendee-local open and expiry times with explicit zone labels/abbreviations and a link to view in the class’s zone And the join-link activation/inactivation occurs according to the UTC instants derived from the class zone, independent of the attendee’s zone And no surface shows conflicting times; automated UI and content tests assert equality of the displayed instants across channels
Automated Regression Suite for Time Zones & DST Boundaries
Given the Smart Expiry service When the test suite runs in CI Then it includes parameterized tests for at least: America/New_York, Europe/London, Europe/Berlin, Asia/Tokyo, Australia/Sydney, Pacific/Auckland, and Africa/Johannesburg And for each zone it tests non-DST days, DST start day, and DST end day (where applicable) for various N/M rule values and classes near midnight And tests assert: correct UTC storage; correct open/expiry activation in a time-mocked clock; consistent formatting in UI/email/SMS renderers; and no off-by-one-hour errors And the pipeline blocks merges on any failure and records code coverage ≥ 80% for time computation modules
Class-Type Rules & Instance Overrides
"As an instructor or admin, I want to set default validity windows by class type and override them per class so that the policy matches each format’s operational needs."
Description

Enable default validity policies by class type (virtual, in-person, hybrid) and allow per-class-instance overrides. Support distinct windows for each modality (e.g., virtual: open 10 min prior; hybrid: virtual link opens 5 min prior, in-person QR opens at start). Provide admin UI to set account-level defaults, class-template rules, and one-off overrides with preview of resulting windows. Persist audit notes for overrides to aid support.

Acceptance Criteria
Account-Level Default Windows by Class Type
Given I am an account admin on Smart Expiry settings When I set default windows: Virtual open=-10m expire=+15m; In-person QR open=0m expire=+5m; Hybrid Virtual open=-5m expire=+10m; Hybrid In-person QR open=0m expire=+5m and click Save Then the values are validated to ensure each expire offset is greater than its corresponding open offset and all offsets are integers between -1440 and +1440 minutes And the settings are persisted and retrievable on reload And new class templates created thereafter pre-populate with these defaults
Class-Template Rules Override Account Defaults With Preview
Given a class template using account defaults When I toggle "Custom validity windows" and enter Virtual open=-15m expire=+20m and In-person QR open=0m expire=+10m Then the account defaults are not applied to this template And the template preview shows computed open and expire timestamps for a selected example session date/time and time zone And classes scheduled from this template inherit these template windows by default
Per-Class-Instance Override With Mandatory Audit Note
Given a scheduled class instance created from a template When I open Smart Expiry for this instance, enable "Override for this class only", change Virtual open from -15m to -5m, and enter an audit note "Coach requested tighter window" Then Save is enabled only if an audit note of at least 5 characters is present And upon saving, the instance stores the override offsets, the audit note, the actor, timestamp, previous values, and new values And future instances from the template remain unaffected
Hybrid Classes Have Distinct Virtual and In-Person Windows
Given a Hybrid class instance with template windows Virtual open=-5m expire=+10m and In-person QR open=0m expire=+5m When the clock is 7 minutes before start Then the virtual join link is not yet valid And the in-person QR code is not yet valid When the clock reaches 5 minutes before start Then the virtual join link becomes valid while the in-person QR remains invalid until start When the clock reaches class start Then the in-person QR becomes valid When the clock is 11 minutes after start Then the virtual link is expired And when the clock is 6 minutes after start Then the in-person QR is expired
Time Zone Awareness in Windows and Preview
Given the class template time zone is America/Los_Angeles and class start is 10:00 AM PT When the template sets Virtual open=-10m expire=+15m Then the preview shows open at 9:50 AM PT and expire at 10:15 AM PT And an attendee in America/New_York sees the window in their local time as 12:50 PM ET to 1:15 PM ET And access enforcement uses the class time zone to determine the absolute UTC window
Reschedule Recomputes Windows
Given a scheduled class instance with Virtual open=-10m expire=+15m and start at 10:00 AM local When I reschedule the instance to start at 11:30 AM local Then the computed absolute open moves to 11:20 AM local and expire to 11:45 AM local And the preview and any admin-visible window timestamps update immediately And access enforcement honors the new times
Access Enforcement Prevents Early and Late Access
Given a virtual class instance with open=-10m expire=+15m When a user attempts to access the join link 11 minutes before start Then access is denied with a message indicating when access opens When a user attempts to access at 5 minutes before start Then access is granted When a user attempts to access 16 minutes after start Then access is denied with a message indicating the link has expired
Secure Tokenized Access Links & Anti‑Sharing Controls
"As an instructor or admin, I want secure, unique, non-shareable access links so that only valid bookings can join and leaked links don’t allow unauthorized access."
Description

Issue per-booking, signed, time-scoped tokens (e.g., JWT/HMAC) embedded in access links, validating class ID, booking ID, and validity window. Enforce single-booking access with optional constraints such as one active session at a time, device/IP heuristics, and rate limiting to deter sharing. Invalidate tokens on cancellation/refund and rotate on reissue. Provide tamper-proof error responses and safe fallbacks that never disclose class details when invalid or expired.

Acceptance Criteria
Per‑Booking Signed Token Issuance
Given a confirmed booking for a scheduled class When the system generates an access link for that booking Then the link contains a signed token whose claims include booking_id, class_id, valid_from (UTC), valid_to (UTC), and a unique jti And the token is signed using the configured algorithm (HS256 or RS256) and validates against the current server-side secret/key And the token is rejected with 403 if any claim is missing, malformed, or the signature is invalid And the token's valid_from and valid_to exactly match the computed Smart Expiry window for that booking
Time‑Scoped Validity Aligned with Smart Expiry
Given Smart Expiry is configured with open_offset_minutes=10 and close_offset_minutes=15 for the class type And a class is scheduled in its class timezone When a token is presented before valid_from Then access is denied with HTTP 403 and a generic message that reveals no class details When a token is presented between valid_from and valid_to (inclusive) Then access is granted (HTTP 200) and the join endpoint is returned When a token is presented after valid_to Then access is denied with HTTP 410 and a generic message that reveals no class details And valid_from/valid_to are computed using the class's timezone rules (including DST) and enforced in UTC without off-by-one-minute errors
Single Active Session Enforcement
Given one-active-session policy is enabled for the class type And an attendee has an active session established using their booking token When a second session using the same booking token attempts to connect from a different device or browser Then the second session is blocked with HTTP 409 and a generic message, while the first session remains active And once the first session ends or has been idle for 2 minutes, a new session using the same token is allowed
Device/IP Heuristics and Rate Limiting
Given anti-sharing heuristics are set to Standard When the same token is presented from more than 3 distinct device fingerprints within 30 minutes Then subsequent attempts are blocked with HTTP 403 and a generic message When the same token is validated more than 10 times by the same IP within 60 seconds Then further validations from that IP for that token are rate-limited with HTTP 429 for 5 minutes And all blocks and rate limits are recorded in audit logs with timestamp, token jti, and reason
Cancellation/Refund Revokes Access Immediately
Given a booking with an issued token is canceled or refunded When the cancellation/refund event is processed Then all tokens for that booking are invalidated within 60 seconds And any active sessions established with those tokens are terminated within 30 seconds And subsequent access attempts with those tokens return HTTP 410 with a generic message revealing no class details
Token Rotation on Link Reissue
Given a user requests a new access link for an existing confirmed booking When the system issues a new link Then a new token with a new jti is generated and the previous tokens for that booking are revoked immediately And the new token inherits the remaining Smart Expiry window (or recomputed window if not yet open) per configuration And attempts to use any revoked token return HTTP 410 with a generic message
Tamper‑Proof Error Responses and Safe Fallbacks
Given a token is missing, expired, invalid, or does not belong to the specified class/booking When the token is validated Then the API responds with appropriate status codes: 401 (missing), 403 (invalid/signature/claim mismatch), or 410 (expired/revoked) And the response body and any rendered fallback page display a generic message that does not include class name, time, location, instructor, or booking identifiers And no stack traces, token contents, or server internals are exposed in the response And a support link and a "Request new link" action are provided when permitted by configuration
Edge Case Automation: Reschedules, Late Bookings, Waitlist Promotions
"As an operations manager, I want link windows to auto-adjust for reschedules, late bookings, and waitlist promotions so that attendees always have timely access without manual intervention."
Description

Automatically recalculate validity windows when classes are rescheduled, when attendees book after the window would normally open, or when waitlisted attendees are promoted shortly before start time. Ensure immediate issuance or extension of links as needed, with safeguards against stale links. Handle instructor substitutions and class overruns with rules-based adjustments and provide event hooks for downstream systems.

Acceptance Criteria
Reschedule: Recalculate and Refresh All Join-Link Windows
Given a class with active bookings and existing join-link windows When the class start time is rescheduled to a new time T' (any time zone) Then open and expire times for all attendee links are recalculated from T' per class-type rules within 60 seconds Given prior links exist When recalculation completes Then previously issued links that no longer align are invalidated within 60 seconds and return "link_replaced" if visited Given multiple reschedules occur within 5 minutes When the final edit is saved Then only the last-saved schedule determines link windows and duplicate notifications/hooks are suppressed via idempotency Given the reschedule crosses a DST boundary When recalculating windows Then open/expire use UTC times for T' and do not shift by the DST delta Given attendees have different local time zones When presenting times Then attendee-facing times reflect their local zone while enforcement uses UTC
Late Booking: Immediate Issuance and Correct Window
Given now is on or after the configured open-window time relative to class start When a new booking is confirmed Then a join link is issued within 5 seconds and is immediately usable Given a class-type expiration offset E When issuing a late-booking link Then the expiration is set to class end time plus E and never earlier than now Given transient issuance failures When they occur Then retries happen up to 3 times over 30 seconds; after that an error is surfaced and no duplicate links are created Given attendee communications are enabled When issuance succeeds Then confirmation is sent via configured channels with correct local start time and validity window Given a booking is attempted after class end time When processing Then issuance is blocked and a "class_ended" error is returned
Waitlist Promotion Near Start: Instant Access Without Stale Links
Given an attendee on the waitlist When promoted at or within the configured open-window threshold before start (or after the window has opened) Then a join link is generated within 5 seconds and is immediately open Given the attendee previously received any tentative access token When promotion occurs Then prior tokens are revoked within 30 seconds and visiting them returns "link_replaced" Given promotion occurs within the configured minimum remaining time before class end When processing Then promotion is denied with reason "too_late" and no link is issued Given capacity constraints When promotion triggers link issuance Then the roster count does not exceed capacity and no overbooking occurs
Instructor Substitution: Preserve or Replace Links per Meeting Details
Given a substitution changes only the instructor identity and not the meeting join details When the change is saved Then attendee link tokens are not regenerated and all existing links remain valid Given a substitution changes the meeting join location/provider (e.g., room ID or URL) When the change is saved Then all attendee links are regenerated within 60 seconds, previous tokens are invalidated, and old links return "link_replaced" Given start/end times are unchanged When regenerating links due to substitution Then open and expire windows are preserved per class-type rules Given downstream integrations are configured When substitution is saved Then events "class.updated.instructor" (identity change) and/or "class.updated.meeting" (join details change) are emitted within 30 seconds with correlation IDs
Class Overrun: Extend Expiry Without Allowing New Entrants
Given an active class approaches scheduled end time When the instructor flags an overrun or the system detects overrun up to the configured cap Then attendee link expirations extend accordingly within 30 seconds Given expirations are extended due to overrun When enforcing access Then only attendees on the roster at scheduled end retain access; new bookings and waitlist promotions remain blocked after the start cutoff Given the overrun exceeds the configured cap When enforcing Then links expire at the cap limit and no further extension occurs Given class-type overrun rules differ When applying extensions Then the configured per-class-type behaviors are respected
Event Hooks: Accurate, Timely, and Idempotent Emissions
Given any recalculation or regeneration due to reschedule, late booking, waitlist promotion, instructor substitution, or overrun When processed Then an event is emitted within 30 seconds containing type, reason, booking/class IDs, prior and new open/expire timestamps (UTC), and a correlation ID Given retries or duplicate processing When emitting events Then idempotency keys ensure at-most-once delivery per change or duplicates carry the same event_id and are marked "duplicate": true Given webhook receivers return non-2xx When delivering events Then retries occur with exponential backoff for up to 24 hours before marking failed with audit log entry Given security requirements When delivering events Then requests are HMAC-signed with tenant secret and include a timestamp, and signatures older than 5 minutes are rejected
Link State Messaging & Notifications Integration
"As an attendee, I want clear messages and reminders indicating when my link will activate or why it’s expired so that I know exactly what to do at any moment."
Description

Expose clear link-state messaging across booking pages and notifications: countdown and local activation time before open, friendly guardrail message when not yet active, and guidance on what to do post-expiry. Integrate with email/SMS so reminders include links that activate at the correct time and display the activation time if tapped early. Provide real-time status on the attendee portal and consistent error codes for support diagnostics.

Acceptance Criteria
Booking Page Pre-Activation Countdown and Guardrail
Given an attendee opens their booking page before the join-link activation window When the page loads Then it displays the message "Join link activates at {local_time}" using the attendee’s current time zone and a live second-level countdown And the Join action is disabled, labeled "Not yet active", and includes a friendly guardrail explanation with aria-disabled=true And the countdown remains accurate without page refresh by syncing to server time and tolerates up to ±2 minutes of device clock drift And the page shows the time zone abbreviation with a tooltip clarifying conversion from class time And analytics event link_state_viewed is emitted with state=pre_active and includes class_id and booking_id
Early Tap on Reminder Link Prior to Activation
Given a recipient taps the join link in an email or SMS reminder before the activation window When the landing page opens Then it shows the local activation time and a live countdown instead of allowing access And a user-friendly message explains that access will open automatically at activation, with a single primary CTA to "Refresh at activation" And the email/SMS content includes the text "Join link activates at {recipient_local_time}" generated at send time using the recipient’s time zone And server-side validation prevents access regardless of device clock changes and responds with error code LNK-001 (NotYetActive) in telemetry And UTM/tracking parameters are preserved without breaking link-state logic and are logged with link_state_viewed
Real-Time Activation State Change at Window Open
Given the activation window start time is reached When the server time crosses the threshold Then the join action becomes enabled on booking pages and attendee portal within 5 seconds And any open pages automatically transition from countdown to "Join now" without manual refresh And the state is consistent across devices and channels (booking page, portal, email deep link) with no stale cache responses And analytics event link_state_changed is emitted with from_state=pre_active and to_state=active And access attempts prior to activation are denied and after activation are permitted per class-type rules
Post-Expiry Messaging and Next-Step Guidance
Given the expiry window has elapsed for a booking When an attendee opens the link Then the page does not expose the join action and displays a clear "Link expired" message And it provides next-step guidance per class configuration (e.g., contact host, request recording if enabled, rebook, join waitlist) with working links And server returns standardized error code LNK-002 (Expired) in logs/telemetry while showing friendly copy to the user And the attendee portal marks the session as Expired and removes active join controls And analytics event link_state_viewed is emitted with state=expired
Attendee Portal Real-Time Link State Badges
Given an attendee views their portal with upcoming and current classes When link states are evaluated server-side Then each class tile shows one of: Not Yet Active (with local activation time), Active (with remaining time), or Expired (with expired timestamp) And badge states auto-refresh within 5 seconds via websocket or polling without full page reload And screen reader users receive state announcements when a status changes And time zones on all timestamps reflect the attendee’s selected local time zone with an option to view class time zone And filtering/sorting by state works and is accurate to within 1 second of server time
Consistent Error Codes and Support Diagnostics
Given any link access results in a non-active state or error When the system renders the user-facing message Then a stable, documented machine-readable code is attached in telemetry and page metadata (e.g., data-error-code), chosen from LNK-001 NotYetActive, LNK-002 Expired, LNK-003 InvalidBooking, LNK-004 PermissionDenied, LNK-005 TimeWindowNotConfigured And the user-facing copy is friendly and does not expose raw codes unless support mode is enabled And all errors log correlation_id, booking_id, class_id, and state with PII minimized and compliant with regional data policies And support can reproduce the state from logs using the correlation_id within 60 days retention And error code usage appears consistently across booking page, attendee portal, email deep link landings, and API responses
Class-Type Rules, Time Zone, and Reschedule Edge Cases
Given class-type rules define custom activation/expiry windows and delivery mode (online, hybrid, in-person) When a class is in-person or hybrid with no join link required Then booking pages and reminders omit the join action and instead show venue details while still showing schedule time And when a class is rescheduled after reminders were sent, the landing page reflects the new activation time immediately and shows a prominent Rescheduled badge And daylight saving transitions and non-hour offsets are handled correctly so local activation times remain accurate And if the attendee changes their profile time zone, all future views recalculate activation times accordingly And waitlisted or unpaid bookings never show an active join state and surface code LNK-004 PermissionDenied with friendly payment/waitlist guidance

Device Lock

Binds a join link to the attendee’s first verified device. If the link is opened elsewhere, ClassTap triggers a quick step-up check or blocks access based on your policy. Stops link forwarding while still allowing legitimate device changes with a tap-to-verify flow.

Requirements

First-Device Binding Fingerprint
"As an attendee, I want my join link to work seamlessly on my first device so that I can access class without extra steps and prevent others from using my link."
Description

Bind each attendee’s join link to the first verified device by generating a privacy-respecting device fingerprint at first successful access (post identity check) and associating it server-side with the attendee’s session/token. Support web, iOS, and Android with resilient matching that tolerates minor fingerprint drift (browser/version updates) while rejecting materially different devices. Store only hashed/rotated identifiers; avoid prohibited tracking techniques and third-party cookies. Enforce binding during the event’s access window, with configurable pre-class grace periods. Seamlessly integrates with ClassTap’s booking records and reminder links so the initial tap from SMS/email establishes the device without added steps. Handles incognito/private modes gracefully with a fallback prompt to persist a lightweight key where permitted.

Acceptance Criteria
Auto-Bind on First Verified Tap from Reminder Link
Given an attendee opens a valid reminder/join link and passes the configured identity check When it is their first successful access on a device Then the system generates a privacy-respecting device fingerprint and binds it server-side to the attendee's session/token within 500 ms And Then subsequent opens from the same device within the event access window succeed without additional prompts And Then the binding event is recorded with timestamp, event ID, booking ID, and a hashed device identifier; no raw device identifiers are stored
Cross-Platform Fingerprint Generation & Matching (Web/iOS/Android)
Given Web (Chrome, Safari, Firefox, Edge), iOS, and Android clients When first verified access occurs Then a device fingerprint/key is generated using first-party storage only (no third-party cookies) and persisted via: Web = first-party storage, iOS = Keychain, Android = Keystore And Then the same device on each platform is recognized on subsequent opens with ≥95% success across the supported matrix in QA tests And Then if persistence fails on a platform, a step-up verification flow is offered rather than silently allowing access
Tolerant Matching for Minor Fingerprint Drift
Given the same device has minor changes (e.g., browser minor version, OS patch, IP change, timezone change ≤1 hour) When the attendee reopens the join link Then a similarity score ≥0.85 results in an automatic match with no step-up required And Then similarity score <0.85 triggers policy action as configured (step-up or block) And Then drift factors contributing to the score are logged for analytics without storing PII
Block or Step-Up on Materially Different Device
Given a join link is opened on a device whose similarity score is <0.6 to the bound device When the policy is set to "step-up" Then a one-time verification is required via 6-digit code or tap-to-verify link sent to the booking contact, valid for 3 minutes and up to 3 attempts And Then on successful verification, access is granted and per policy either rebinds to the new device or grants one-time access only When the policy is set to "block" Then access is denied with a clear error and support link, the attempt is logged, and no event content is revealed And Then repeated failed attempts are rate-limited to a maximum of 5 per 10 minutes per booking
Incognito/Private Mode Fallback Key Persistence
Given the join link is opened in a private/incognito context that restricts persistence When first verified access occurs Then the user is shown a single-screen prompt to allow storing a lightweight first-party key (single tap to continue) If the user consents or storage is permitted Then the key is stored and subsequent opens in the same context recognize the device If the user declines or storage is not permitted Then a session-scoped key is issued, initial access proceeds, and subsequent opens require step-up verification And Then initial access is not blocked solely due to private/incognito mode
Configurable Access Window and Pre-Class Grace Enforcement
Given an event with an access window (start/end) and a pre-class grace period in minutes When a user binds before the start but within the grace period Then the device binding is created but content access is gated until the start time When access occurs after the end time plus the configured grace Then access is denied regardless of device match And Then changes to event times take effect immediately; reschedules retain the binding if the booking ID is unchanged, otherwise rebind is required
Privacy Compliance: Hashed/Rotated Identifiers and No Prohibited Tracking
Given device fingerprinting and persistence are required Then only hashed and salted identifiers are stored; salts rotate at least every 30 days without breaking bindings for active events And Then no prohibited tracking techniques (e.g., third-party cookies, cross-site tracking, canvas fingerprinting without consent) are used; privacy review and static analysis checks pass And Then logs contain no PII beyond booking ID, event ID, and hashed device ID; device-binding records are purged N days after event end (configurable, default 30)
Step-Up Challenge on Unrecognized Device
"As an organizer, I want suspicious link opens to require quick verification so that forwarded links don’t grant unauthorized access."
Description

When a join link is opened on an unbound or high-risk device (new fingerprint, geo/IP anomaly, rapid multi-attempts), trigger a lightweight verification flow based on organizer policy: SMS one-time code, email magic link, or in-app push. Provide configurable escalation (e.g., require two factors on high risk, block after N failures) with rate limiting, lockouts, and human-verification CAPTCHAs. Present clear, localized UX with retry timers and support contact. On success, optionally rebind to the new device per policy; on failure, block and log. Integrates with ClassTap’s messaging providers and honors per-class access windows and waitlist promotions.

Acceptance Criteria
Step-Up Trigger on Unrecognized/High-Risk Device
Given an attendee opens a valid join link within the class access window And the device has an unrecognized fingerprint OR a geo delta > 500 km within 30 minutes OR >= 3 join attempts in 5 minutes from different device fingerprints/IPs When the link is accessed Then the system blocks direct entry and presents the step-up verification per organizer policy And no class content is accessible until verification succeeds And the trigger event is logged with a correlation ID
Policy-Driven Verification Method and Escalation
Given organizer policy method = SMS OTP When step-up is invoked Then send a 6-digit OTP via the configured SMS provider, mask the phone number (e.g., +1 •••• ••34), accept codes for 10 minutes, and accept only SMS as a factor Given organizer policy method = Email Magic Link When step-up is invoked Then send a signed single-use magic link valid for 10 minutes to the attendee’s email, and accept only the magic link as a factor Given organizer policy method = In-App Push When step-up is invoked Then send a push approval request to the ClassTap app with a 60-second expiration, and accept only the push approval as a factor Given calculated risk score >= 80 (high risk) per policy When step-up is invoked Then require two distinct factors (e.g., SMS + Email) in the same session; else require a single factor And record method(s) used in the audit log
Rate Limiting, Lockout, and CAPTCHA Enforcement
Given an attendee enters 5 incorrect OTPs or fails 5 verification attempts within 15 minutes When the next attempt is initiated Then lock verification for 15 minutes, display a visible countdown timer, and present a support contact link Given the attendee requests more than 3 OTP/resend/magic-link sends within 5 minutes When an additional resend is requested Then throttle and show the next available time Given 3 failed attempts within 2 minutes When the attendee tries again Then require a human-verification CAPTCHA before proceeding Then all lockouts, throttles, and CAPTCHA challenges are logged with timestamps and device fingerprints
Localized UX With Retry Timers and Support Contact
Given the user’s locale resolves to a supported language When the step-up screen renders Then all labels, error messages, helper texts, and time formats are localized to that language; otherwise, fall back to English And sensitive fields (email/phone) are masked in the UI, and accessibility attributes (aria-labels) exist for all interactive elements When a lockout or resend cooldown is active Then the UI shows a live countdown timer in mm:ss and a localized explanation, and provides a visible link/button to contact support as configured by the organizer
Successful Verification Grants Access and Optional Rebind
Given verification completes successfully within the class access window When organizer policy RebindOnSuccess = true and the device is unbound Then bind the link to the new device fingerprint and revoke any prior binding Given verification completes successfully within the class access window When organizer policy RebindOnSuccess = false Then do not change the existing binding Then grant class access immediately, update the attendee session state, and log success including device fingerprint, method(s) used, and binding outcome
Access Window and Waitlist Promotion Enforcement
Given a class access window configured (e.g., open 15 minutes before start to 10 minutes after start) When a verification attempt occurs outside this window Then block access regardless of verification outcome and display the next eligible window Given an attendee on the waitlist who is not yet promoted When they open a join link Then present a localized message explaining access is not yet granted due to waitlist status and do not initiate step-up Given an attendee has been promoted from the waitlist When they open a join link within the access window Then proceed with step-up per policy
Messaging Delivery Handling and Fallback
Given method = SMS or Email When a code or magic link is sent Then use the configured primary messaging provider and record delivery status (queued, sent, delivered, bounced/failed) When the primary provider returns a send failure Then automatically retry once and, if a secondary provider is configured, fail over within 5 seconds; otherwise, show retry guidance to the user Given method = Push When a push is sent Then display in-app status (sent/pending/expired) and allow a single in-session resend after 30 seconds Then all send attempts, statuses, and failures are written to the audit log with provider identifiers
Tap-to-Verify Device Switch
"As an attendee, I want an easy way to move my access to a new device so that I can switch without being locked out."
Description

Allow legitimate device changes via an attendee-initiated flow. From the currently bound device, provide a “Move to new device” action that displays a QR code or short code to confirm on the new device; alternatively send a one-tap email/SMS approval. After confirmation, atomically transfer the binding so only one active device remains. Include a short grace period where both devices can access to prevent drops during live switches. Offer a recovery path if the original device is lost (email verification plus organizer override). Limit switch frequency and log all transitions. Ensure minimal friction and consistency across web and mobile deep links.

Acceptance Criteria
Initiate Device Switch From Bound Device
Given an attendee is signed in on the currently bound device and viewing a booking or join page When they tap "Move to new device" Then a modal displays a scannable QR code and a 6-digit numeric short code, each valid for 5 minutes, and an option to "Send one-tap link" Given the modal is open When 5 minutes elapse or the attendee cancels the flow Then all issued tokens (QR and short code) are invalidated server-side and the UI reflects an expired or cancelled state Given policy requires recent re-authentication When the attendee has not authenticated within the last 10 minutes Then the system prompts for re-auth (password/biometric) before showing any codes or links
Verify New Device Using QR or Short Code
Given the QR code is scanned on a new device When the link opens Then a verify screen shows class name, start time, and masked attendee identity with "Bind this device" and "Cancel" actions Given the short code is entered at classtap.com/switch on a new device When the code matches an active token Then the same verify screen is shown; otherwise an error "Code invalid or expired" is displayed Given the attendee confirms on the verify screen When the token is valid and unused Then the device binding transfers to the new device within 2 seconds and the token is consumed (single-use) Given the QR or code flow completes within the token validity window When the attendee uses the QR flow Then the switch completes in no more than 2 taps on the new device and does not require credential entry Given a token is replayed after consumption or expiry When the link is visited again Then access is denied and an audit event with outcome=replay is recorded
One-Tap Email/SMS Approval
Given the attendee selects "Send one-tap link" from the bound device When they confirm the masked destination (email/SMS) Then a one-tap approval link is sent within 10 seconds and is valid for 15 minutes Given the one-tap link is opened on a new device When the attendee taps "Approve" Then the device is bound, the link is immediately invalidated, and subsequent opens show "Link already used" Given the one-tap link is opened after expiry When the page loads Then the attendee is prompted to restart the switch flow and no binding occurs
Atomic Transfer With Grace Period
Given a device switch is confirmed by any method (QR/code/one-tap) When the binding operation executes Then both old and new devices retain access for a grace period of 120 seconds, after which only the new device remains authorized Given the grace period has ended When the old device accesses protected join routes Then access is denied with a "Device switched" message and guidance to initiate a new switch if needed Given multiple switch attempts are initiated concurrently When a binding is in progress Then no additional switch tokens are issued until the current attempt completes or expires, and the attendee sees a message indicating an active switch is in progress
Switch Frequency Limits and Audit Logging
Given an attendee account When device switches are completed Then enforce a limit of at most 3 successful switches per rolling 24 hours and at most 1 active pending switch token per attendee Given the attendee exceeds the switch limit When they attempt another switch Then the request is blocked and the UI shows the time remaining until the next allowed switch window Given any switch event (created, confirmed, expired, failed, blocked) When it occurs Then an immutable audit log entry is recorded with timestamp, user_id, class_id, from_device_id, to_device_fingerprint, method (QR/code/link/recovery/override), initiator IP, and outcome
Lost Device Recovery and Organizer Override
Given the original device is unavailable When the attendee selects "I lost my device" on the switch page Then they must verify their email via a 6-digit code and confirm recent booking details before proceeding Given recovery verification succeeds When the attendee confirms on the new device Then the binding transfers immediately and the old device is revoked without a grace period Given the attendee cannot complete recovery When the organizer uses the dashboard "Override bind" action Then they must provide a reason, the binding is reset to the selected device, notifications are sent to the attendee and organizer, and the action is fully logged with actor identity and reason
Cross-Platform Deep Link Consistency
Given the new device has the ClassTap mobile app installed When the QR/link is opened Then the verify screen opens in-app via deep link with identical copy and steps to the web flow; otherwise it opens the mobile web verify screen Given a desktop browser is used to start the switch When the QR is scanned with a phone Then the desktop shows a real-time status banner and updates to "Switched" within 2 seconds of completion on the phone Given transient network issues on the new device When the verify screen loads Then it retries for up to 30 seconds with clear status and provides a fallback to manual short code entry
Admin Policy Controls and Overrides
"As a studio owner, I want to configure how Device Lock behaves for my classes so that I can balance security with attendee convenience."
Description

Provide granular controls for studios/instructors to configure Device Lock per organization and per class: enforcement mode (strict block, step-up then rebind, allow up to N devices, time-window-limited), allowed verification methods, retry limits, risk thresholds, and exception rules (e.g., venue IP allowlist for hybrid/in-person check-in). Expose an instructor console to reset an attendee’s device binding, temporarily lift restrictions, and view current device status. Supply API endpoints and webhooks for automation. Defaults prioritize security with sensible UX. All changes are audited and can be applied to upcoming sessions via templates.

Acceptance Criteria
Organization Policy Configuration and Secure Defaults
Given I am an organization admin with the Device Lock:Manage permission When I open the Device Lock settings Then I can select an enforcement mode from [Strict Block, Step-Up Then Rebind, Allow up to N devices, Time-Window-Limited] Given I configure allowed verification methods to [SMS OTP, Email Link, TOTP, Push] subset When a step-up is triggered Then only the selected methods are offered Given I set retry limit to 3 and cooldown to 15 minutes When an attendee fails step-up 3 times within the cooldown window Then further attempts are blocked until cooldown expires and an audit entry is created Given a new organization is created When no custom policy is set Then Device Lock defaults are applied such that enforcement is not "Allow unlimited devices", the retry limit is <= 3, and at least one out-of-band verification method is enabled Given I save a valid policy change Then it persists, is reflected in the Admin UI, and is retrievable via GET /v1/device-lock/policies/{orgId} Given a non-admin user attempts to modify Device Lock policy When they submit changes Then the system returns 403 and no changes are persisted
Class-Level Overrides and Template Application to Upcoming Sessions
Given an org-level policy template exists When I apply the template to Class A Then upcoming sessions of Class A inherit the template and the effective policy is shown in the class details and GET /v1/device-lock/policies/{classId} Given Class A has a class-level override When I remove the override Then the class reverts to the organization default policy Given I publish changes to a template When sessions are already in progress Then those sessions are not affected; only not-yet-started sessions are updated Given I schedule new sessions of Class A after applying the template Then those sessions inherit the current template without additional steps Given I confirm changes in the UI Then an audit entry is created with actor, timestamp, affected class/session IDs, and before/after policy values
Enforcement Modes Execute as Configured
Given enforcement mode = Strict Block When a verified attendee opens the join link on a second device Then access is denied with a clear policy message, no rebind occurs, and an audit event device_lock.blocked is recorded Given enforcement mode = Step-Up Then Rebind When the attendee opens on a second device and passes an allowed verification method Then the binding updates to the new device, the prior device loses access immediately, and an audit event device_lock.rebound is recorded Given enforcement mode = Allow up to 2 devices When the attendee has verified on two devices Then both can join; when attempting a third device Then access is denied and the message indicates the device limit was reached Given enforcement mode = Time-Window-Limited with window = 15 minutes When the attendee opens on a different device within 15 minutes of first verification and passes step-up Then access is allowed and the new device is bound; when after 15 minutes Then access is denied per policy Given any enforcement mode When access is denied Then the response includes a reason code and correlation_id for support
Exception Rules: Venue IP Allowlist and Hybrid/In-Person Check-In
Given a venue IP allowlist contains CIDR 203.0.113.0/24 When an attendee opens a join link from 203.0.113.10 Then device lock enforcement is bypassed as configured (no step-up) and the session is marked with a Venue Exception badge Given a request originates from a non-allowlisted IP When the attendee opens the join link Then the standard enforcement policy applies with no exception Given both a strict policy and an IP allowlist exception apply When the request is from an allowlisted IP Then the exception takes precedence as configured and an audit entry records the rule applied and source IP Given a registered kiosk device is used for check-in When an attendee scans their QR at the venue Then they are checked in without consuming a personal device slot and an audit entry links the kiosk device ID
Instructor Console: Overrides and Device Reset
Given an instructor opens the attendee detail panel for a session Then they can view device status including current device fingerprint, last seen timestamp, last IP/CIDR, verification method used, and effective enforcement mode Given the instructor has Override permission When they click Reset Device Binding and confirm Then the attendee’s current binding is cleared, the next verified device becomes bound, prior device access is revoked, and a webhook device_lock.override.applied is emitted Given the instructor sets a Temporary Lift for 30 minutes When the attendee joins during the window Then they can access from any device; when the window ends Then the original policy is automatically reinstated Given an instructor without Override permission attempts any override action Then the action is blocked, a UI error is shown, and an audit entry records the denied attempt
Automation APIs and Webhooks
Given valid API credentials with scope device_lock:write When I PUT /v1/device-lock/policies/{scope} with a valid payload Then the policy updates and the 200 response returns the canonical policy document including etag Given I PUT /v1/device-lock/policies/{scope} with an invalid payload (e.g., device_limit > 5 or unknown verification method) Then the API responds 400 with field-level error codes and no change is persisted Given I POST /v1/device-lock/overrides/reset with attendee_id and session_id Then the attendee’s device binding is cleared and a webhook device_lock.override.applied is delivered with a signed signature and correlation_id Given a step-up challenge result occurs Then webhooks device_lock.challenge.passed or device_lock.challenge.failed are emitted within 5 seconds, are retriable with exponential backoff, include a monotonically increasing delivery id, and can be verified via HMAC signature Given I GET /v1/device-lock/status?attendee_id=...&session_id=... Then the API returns device_count, last_verification_method, effective_policy_scope (org|class|override), and last_updated
Audit Trail and Change Governance
Given any policy, template, override, or exception rule change occurs Then an immutable audit record is created with actor (user id or API client id), UTC timestamp, target scope (org/class/session/attendee), action, before/after values, and optional reason Given I view the audit log in the Admin Console When I apply filters for actor, time range, target, or action type Then matching entries are displayed and can be exported to CSV Given an attempt is made to modify or delete past audit records Then the system prevents the change and records the attempt as a separate audit entry with reason unauthorized_mutation Given I apply a template to multiple upcoming sessions Then a single parent audit entry is created with references to all affected session IDs and per-session child entries are accessible via the parent record
Secure Tokenization and Anti-Replay
"As a security-conscious admin, I want join links to be resistant to replay and tampering so that only the intended attendee can use them."
Description

Issue signed, time-scoped, single-attendee join tokens with unique jti nonces. Enforce one active token per attendee per session; bind token to device fingerprint on first use and rotate tokens on subsequent accesses. Implement server-side replay detection and immediate invalidation on bind reset or policy violations. Require TLS, HSTS, and strict SameSite cookie usage where applicable. Ensure deep link compatibility on mobile. Provide optional offline QR fallback for in-person check-in that still respects one-device rules when later synced. Minimize PII in tokens and centralize secrets management and key rotation.

Acceptance Criteria
First Use Device Binding
Given a signed, time-scoped join token containing only attendee_id, session_id, exp, jti, and kid And the token is delivered via an HTTPS link And jti has not been seen before for this session When the attendee opens the link on an unbound device Then the server verifies the signature using the key identified by kid from the active JWKS And validates exp is within the configured TTL And binds the attendee-session to the device fingerprint derived on first use And marks the token as bound to that fingerprint And returns a 200 OK join response And writes an audit log entry with timestamp, IP, device fingerprint hash, jti, and outcome
Token Rotation and Single Active Token Enforcement
Given an attendee-session is already bound to a device fingerprint And there is exactly one active token T1 with jti J1 When the attendee accesses the join link again from the same bound device over HTTPS Then the server issues a new signed token T2 with a new jti J2 and fresh exp within policy limits And immediately invalidates T1 so only T2 is active for that attendee-session And any request using T1 thereafter returns 401 with error code token_rotated And the system records rotation in the audit log with prior and new jti
Replay Detection and Immediate Invalidation
Given a valid token T with jti J bound to a device fingerprint F When the server receives a second request bearing the same token T (same jti J) Then the server classifies it as a replay and denies the second request with 401 and error code replay_detected And increments a replay counter on the attendee-session And does not change the existing device binding And, if policy requires, triggers a step-up verification challenge before allowing further access And logs the replay event with jti, fingerprint hash, IPs, and timestamps
Cross-Device Access Policy Enforcement (Step-Up or Block)
Given an attendee-session is bound to device fingerprint F1 When the join link is opened on a different device producing fingerprint F2 ≠ F1 Then if policy = step_up And the attendee completes the configured step-up challenge (e.g., OTP via SMS/email) within the allowed window Then the binding is transferred to F2, all prior tokens are invalidated, and a new token with a new jti is issued And the event is logged with prior and new fingerprint hashes Else if policy = block or the challenge fails Then access is denied with 403 and error code device_mismatch And the original binding to F1 remains unchanged And the event is logged
Transport, Cookie, and Key Management Security
Given any web join request When the request is received over HTTP Then it is redirected to HTTPS and Strict-Transport-Security is set with max-age ≥ 15552000 and includeSubDomains And all subsequent requests must use TLS 1.2+ with strong ciphers And any session cookies set by the join experience include Secure, HttpOnly, and SameSite=Strict And the join token is not persisted in localStorage or sessionStorage and is not sent in Referer due to Referrer-Policy: same-origin And JWTs include kid and are signed with keys managed in centralized KMS And key rotation supports at least one previous active signing key for validation during rollout, verified by tests that pass with both current and previous keys
Mobile Deep Link Compatibility
Given a join link is opened on a supported mobile device When the ClassTap app is installed and configured for universal/app links Then the link opens the native app without intermediate browsers, and the token is passed via the OS link handoff to the app And device binding and token rotation behavior mirror the web flow When the app is not installed Then the link opens the responsive web join page over HTTPS with the token preserved And the token is not exposed via clipboard or system share sheets during handoff And iOS (16+) and Android (11+) tests confirm successful join, rotation on re-open, and no token leakage in referrer logs
Offline QR Fallback with Sync and One-Device Enforcement
Given offline check-in is enabled for a session And an attendee presents an offline QR code containing a signed, time-bounded offline nonce tied to attendee_id and session_id When staff scans the QR while the device is offline Then the scan is stored locally with encrypted payload and timestamp When the device later syncs Then the server validates the QR signature and that the offline nonce has not been used before And creates or confirms a single active token for the attendee-session without violating one-device rules And subsequent sync attempts using the same offline nonce are rejected as replay with 409 and error code offline_nonce_replay And if a different device binding already exists, server follows the configured policy (step-up to rebind or block) before issuing any new token And all events are logged
Audit Trail, Alerts, and Reporting
"As an instructor, I want visibility into device lock events so that I can detect abuse and support attendees."
Description

Capture detailed events for first bind, challenge triggers, passes/fails, blocks, device switches, overrides, and token invalidations with timestamps, coarse IP/geo, and hashed device identifiers. Surface searchable logs and per-class dashboards with filters and CSV export. Provide optional real-time alerts to instructors (email/SMS/push) on repeated failed challenges or suspected link sharing, and optional courtesy notifications to attendees about successful device changes. Offer weekly/monthly summaries highlighting prevented misuse and challenge pass rates. Integrate with webhooks/SIEM. Respect data retention and privacy settings.

Acceptance Criteria
Event Logging: First Bind and Security Events
Given an attendee completes their first device bind for a class, When the bind succeeds, Then an event is written with fields: event_type=first_bind, timestamp=UTC ISO8601, class_id, attendee_id (UUID), join_link_id, hashed_device_id (SHA-256 salted), coarse_ip (IPv4 /24 or IPv6 /48), geo_country, user_agent_hash, policy_id Given a challenge is presented due to policy, When the challenge screen is shown, Then an event is written with event_type=challenge_trigger including reason and the same context fields Given the attendee submits the challenge, When validation completes, Then an event is written with event_type in {challenge_pass, challenge_fail} and result_reason Given access is blocked by policy, When the block decision is made, Then an event is written with event_type=access_block and block_reason Given a device switch is approved via step-up, When the bind is moved, Then an event is written with event_type=device_switch_approved including old_hashed_device_id and new_hashed_device_id Given an instructor/admin applies a manual override, When the override is saved, Then an event is written with event_type=override_applied including actor_id and reason_code Given a join token is invalidated or expires, When invalidation occurs, Then an event is written with event_type=token_invalidated and cause in {expired, revoked, rotated} Rule: Events are immutable, strictly time-ordered, and visible in the log UI and API within 3 seconds at p95
Searchable Logs and Per-Class Dashboard with CSV Export
Given an instructor views a class dashboard, When they filter by date range, event_type, outcome, attendee_id, hashed_device_id, coarse_ip, geo_country, and policy_id, Then the results reflect only matching events Rule: Filtered queries up to 10,000 events return within 2 seconds at p95 Rule: Instructors see only events for their classes; org admins can view all classes; attendees have no access Given a filtered result set is displayed, When Export CSV is requested, Then a CSV containing only the filtered events is generated with headers [timestamp, class_id, attendee_id, join_link_id, event_type, outcome, policy_id, hashed_device_id, coarse_ip, geo_country] and UTF-8 encoding Rule: CSV export begins within 5 seconds and completes for up to 100,000 rows within 60 seconds Rule: CSV and UI never include raw device identifiers or full IP addresses beyond the specified coarse granularity
Real-Time Instructor Alerts on Repeated Failures or Suspected Link Sharing
Given alerting is enabled for a class, When there are >= 3 challenge_fail events for the same join_link_id within 10 minutes, Then send an alert to the instructor via each enabled channel in {email, SMS, push} Given alerting is enabled, When suspected link sharing is detected (>= 2 distinct hashed_device_id or >= 3 coarse_ip for the same join_link_id within 15 minutes), Then send an alert as above Rule: Alerts include class name, event counts, time window, and a link to the filtered log view; they exclude PII and full IPs Rule: Throttle to at most 1 alert per class per 15 minutes per condition (failures, suspected_sharing) Rule: Record an event_type=alert_dispatched with channels, condition, and delivery_status for each channel Rule: If a channel provider returns a transient error, retry with exponential backoff for up to 1 hour
Courtesy Notifications to Attendees on Successful Device Change
Given attendee courtesy notifications are enabled, When an event_type=device_switch_approved is recorded, Then send a notification to the attendee via their preferred channel in {email, SMS} within 1 minute Rule: Notification includes class name, date/time (UTC and local), geo_country and coarse_ip, and a secure link to report "This wasn’t me" that opens a support flow Rule: If delivery fails permanently, record an event_type=notification_failed with reason; do not retry more than 3 times Rule: Attendees can opt out; when opted out, no notification is sent and an event_type=notification_opt_out is recorded
Weekly and Monthly Summaries of Device Lock Activity
Given summaries are enabled, When a reporting period ends (weekly and monthly), Then generate a summary containing counts of prevented misuse (access_block), challenge pass/fail rates, top classes by challenge volume, and trend charts Rule: Summaries are delivered within 24 hours of period end to instructors for their classes and to org admins for the org via email with a link to the dashboard Rule: Summaries include a downloadable CSV of period metrics; no raw PII or full IPs are included Rule: Users can opt out per frequency; opt-out is honored and logged with event_type=summary_opt_out
Webhooks and SIEM Integration for Audit Events
Given a webhook endpoint and secret are configured, When any audit event is written, Then POST a JSON payload to the endpoint within 5 seconds with HMAC-SHA256 signature in header X-CT-Signature and an idempotency key X-CT-Event-Id Rule: On non-2xx response, retry with exponential backoff for up to 24 hours; respect Retry-After for 429 Rule: Provide a replay mechanism to resend events for a date range scoped to the tenant and event types Rule: Support payload formats JSON (default) and CEF over syslog TCP/TLS for SIEMs; format is tenant-configurable per destination Rule: Record delivery outcomes with event_type=webhook_delivery_{success|failure} including attempt_count and last_status_code
Data Retention and Privacy Controls Applied to Audit Data
Given a tenant sets a retention period (30/90/365 days), When events exceed the retention window, Then they are purged and no longer available via UI, API, export, or webhook replay Rule: Only coarse IP/geo and hashed device identifiers are stored; raw device identifiers and full IPs are never persisted or exported Rule: Alerts, notifications, and summaries exclude PII beyond attendee first name and do not include full IPs or precise geo Given an attendee invokes data deletion, When the request is processed, Then their attendee_id is pseudonymized in historical events while aggregated metrics remain intact, and an event_type=privacy_erasure_processed is recorded Rule: All accesses to audit logs are themselves logged with actor_id, timestamp, and purpose for compliance
Accessibility, Localization, and Compliance
"As a global attendee, I want the verification flow to be accessible and localized so that I can complete it regardless of my abilities or location."
Description

Ensure all Device Lock screens and flows meet WCAG 2.2 AA: keyboard navigable, screen-reader semantic labels, sufficient contrast, and motion-reduced alternatives. Localize UI copy, SMS/email templates, and error messages for supported languages and right-to-left scripts. Provide non-SMS verification options for users without mobile numbers. Present transparent explanations of why verification is required and what data is used; obtain consent where required. Avoid prohibited or uniquely identifying fingerprinting techniques per platform policies (e.g., iOS/Android). Support data export/deletion requests and document retention durations.

Acceptance Criteria
Keyboard and Screen Reader Accessibility Compliance (WCAG 2.2 AA)
- Given a user navigating the Device Lock flow with keyboard only, when pressing Tab/Shift+Tab, then focus moves in logical order without traps and is visible with a focus indicator meeting WCAG 2.2 AA (≥3:1 contrast). - Given a modal/dialog opens, when it appears, then initial focus moves into the dialog, background content is inert, Esc closes the dialog, and focus returns to the trigger. - Given interactive controls (buttons, links, inputs, toggles), when activated with Enter or Space as appropriate, then they perform the same action as a pointer click. - Given form validation fails, when an error is shown, then the message is programmatically associated to the field (aria-describedby), announced by screen readers within 1 second, and placed inline in reading order. - Given common screen readers (VoiceOver, TalkBack, NVDA/JAWS), when navigating any control, then each has an accessible name, role, state/value that match the visible label and locale’s language attribute. - Given any time limit in the flow (e.g., code expiry), when the timer starts, then users are warned before expiry and can extend at least once by ≥60 seconds unless disallowed by tenant security policy (with rationale shown).
Visual Contrast and Reduced Motion Compliance
- Given any Device Lock screen, when rendered, then normal text meets ≥4.5:1 contrast and large text ≥3:1, UI components and focus indicators meet ≥3:1 against adjacent colors. - Given focus is visible, when moving focus sequentially, then the indicator remains within viewport and has sufficient size per WCAG 2.2 AA Focus Appearance guidance. - Given user prefers-reduced-motion, when the flow loads, then non-essential animations/transitions are disabled; no auto-scrolling/parallax; substituted transitions complete in ≤100ms. - Given any animated content, when displayed, then it does not flash more than 3 times per second and provides a pause/stop mechanism if it runs >5 seconds. - Given any motion-based interaction, when device motion is unavailable or disabled, then an equivalent non-motion control is available and discoverable.
Localization and Right-to-Left Support
- Given the user’s selected locale is supported, when starting Device Lock, then all UI strings, emails, SMS templates, dates, times, numbers, and plurals render correctly in that language. - Given an RTL locale (e.g., ar, he), when rendering, then layout and navigation are mirrored, icons with direction are flipped, caret and punctuation behave correctly, and mixed-direction text uses proper bidi controls. - Given an unsupported locale, when detected, then the default locale is used and a language switcher is available; no hardcoded untranslated strings appear. - Given pseudo-localization is enabled in QA, when rendering, then ≥95% of screens show no clipped/overlapped text with up to +30% string expansion at common breakpoints. - Given i18n linting runs, when building, then 0 hardcoded user-visible strings are reported and all strings are externalized with unique keys.
Non-SMS Verification Alternatives
- Given a user without a mobile number or with undeliverable SMS, when step-up verification is required, then at least two non-SMS options are offered: email one-time code/link and WebAuthn/passkey (or TOTP when WebAuthn unsupported). - Given the user chooses email, when the code/link is requested, then delivery occurs within 30 seconds; codes are 6–8 digits, expire within 10 minutes, allow ≤5 attempts, and resends are rate-limited to ≥30 seconds. - Given the user chooses WebAuthn/passkey, when registering/asserting, then flows complete per platform UX, with clear fallback to email/TOTP if unavailable or failed. - Given verification succeeds via any path, when returning to the booking/join flow, then the device is bound/revalidated equivalently to SMS and the user proceeds without re-prompt. - Given accessibility needs, when interacting with any alternative, then the controls are fully keyboard and screen-reader operable and localized.
Transparency, Explanation, and Consent
- Given Device Lock is initiated, when the first verification screen loads, then a just-in-time notice explains why verification is required, what data is used, retention duration, and who can access it, with links to policy. - Given consent is required by tenant policy or jurisdiction, when the notice is shown, then an unchecked consent checkbox is presented; proceeding is blocked until checked; consent is logged with user ID, timestamp, locale, and notice version. - Given consent is not required, when the notice is shown, then acknowledgement text and a link to learn more are presented; proceeding is allowed and the notice display is logged. - Given the user wants to review details, when clicking “What data is used?”, then a localized panel enumerates data categories (e.g., device model, OS version, storage state, IP region) and excludes unique hardware identifiers. - Given the user declines or cannot consent, when policy requires verification, then a safe alternative path is offered (e.g., contact host/manual check) and the session is not silently degraded.
Platform Policy Compliance: No Prohibited Fingerprinting
- Given iOS/Android platform policies, when collecting device signals, then prohibited identifiers (e.g., IMEI, MAC) and covert techniques (e.g., canvas/audio/battery fingerprinting) are not used. - Given device binding is needed, when permitted signals are insufficient, then the system falls back to user-mediated verification (e.g., passkey/email) rather than adding unapproved entropy sources. - Given mobile builds, when static/dynamic analysis runs, then 0 references to banned APIs are present and runtime logs show no access to restricted device info. - Given policy changes, when flagged, then a kill switch can disable specific signal collectors within 48 hours without requiring an app update. - Given release readiness, when passing QA, then a documented privacy review checklist is signed off and stored for audit.
Data Export, Deletion, and Retention Disclosure
- Given a data export request, when submitted via self-serve or admin console, then a machine-readable export (JSON/CSV) of device verification records and consents is generated within 5 minutes, including timestamps, IP region, device alias, and audit events. - Given a deletion request, when processed, then identifiable verification data is erased from active systems within 7 days and from backups within 30 days; tombstones prevent re-creation; outcomes are logged. - Given retention policies, when viewing settings, then retention duration for verification data is visible and configurable per tenant (0–365 days default 90), and shown in user-facing notices/templates. - Given operational events, when exports/deletions occur, then audit logs capture requester/actor, scope, time, and result; failures emit actionable error codes and notifications.

Silent Rotate

Auto-rotates the tokenized join link shortly before class and seamlessly updates the same SMS/email/calendar thread. Old links are invalidated, new links just work—reducing link-sharing and eliminating last-minute “wrong link” support pings.

Requirements

Time-based Token Rotation Engine
"As an instructor, I want the system to automatically rotate join links shortly before class so that only the latest link works and shared old links can’t be used."
Description

Implements a service that generates per-attendee, per-class join tokens and automatically rotates them a configurable number of minutes before class start. The engine invalidates previous tokens atomically, issues new tokens, and guarantees idempotent rotation even under concurrent triggers. Supports per-class rotation offsets, grace periods, and configurable token TTLs. Ensures tokens are cryptographically signed, non-guessable, and bound to booking and class session metadata. Exposes rotation status and webhooks/events for downstream systems. Designed to scale across peak rotation windows and to recover gracefully from failures with retry and backoff.

Acceptance Criteria
Per-Attendee Per-Class Token Generation and Binding
- Given a confirmed booking for attendee A in class session S, When a token is issued, Then the token is unique per (A,S), URL-safe, and contains at least 128 bits of entropy. - Given the token for (A,S), When the service verifies the signature, Then verification succeeds and the token is bound to booking_id, attendee_id, class_session_id, and rotation_version. - Given two different attendees A and B for the same session S, When tokens are issued, Then token(A,S) != token(B,S). - Given repeated token issue requests for the same (A,S) before any rotation, When a token is requested, Then the same current token is returned. - Given a token whose payload or signature is modified, When it is validated, Then validation fails and access is denied with 401 invalid_token.
Configurable Pre-Class Rotation Timing
- Given a class session S with rotation_offset=T minutes, When current_time reaches start_time(S) - T, Then a rotation for S is scheduled and executed within 60 seconds. - Given a class session S with a per-class rotation_offset override, When the rotation scheduler runs, Then the override value is used instead of the default. - Given no override on S, When the rotation scheduler runs, Then the system uses the global default rotation_offset. - Given rotation has been performed for S at version V, When the scheduler runs again before start_time(S), Then no additional rotation is performed (at-most-once per window).
Atomic Rotation with Grace Period Enforcement
- Given rotation occurs at time R for session S with grace_period=G minutes, When rotation executes, Then all pre-rotation tokens for S are marked expired at R and new tokens with rotation_version=V+1 are issued atomically. - Given a pre-rotation token for S, When it is presented at time t where R <= t < R+G, Then access is granted under grace policy and the request is recorded as grace_used=true. - Given the same pre-rotation token for S, When it is presented at time t >= R+G, Then access is denied with 401 invalid_token. - Given a post-rotation token for S (version V+1), When it is presented at time t >= R, Then access is granted.
Idempotent Rotation Under Concurrent Triggers
- Given N concurrent rotation triggers for the same session S and rotation window, When they execute, Then exactly one rotation_version is created and all triggers return the same rotation_version identifier. - Given concurrency as above, When inspecting tokens for any attendee in S, Then each attendee has exactly one valid post-rotation token. - Given a retry of the rotation request after success, When it executes, Then the system returns a successful idempotent result and does not create new tokens.
Token TTL and Validation Enforcement
- Given token_ttl policy is issuance_based=N minutes, When time >= token_issued_at + N, Then the token is rejected with 401 invalid_token. - Given token_ttl policy is class_end_based=D minutes, When time >= class_end_time + D, Then the token is rejected with 401 invalid_token. - Given a token for (A,S), When it is presented for a different class session or attendee, Then validation fails and access is denied. - Given a valid unexpired token for (A,S), When presented within TTL and grace policies, Then validation succeeds with p95 verification latency <= 50 ms.
Rotation Events and Webhooks Delivery
- Given a successful rotation for session S, When events are emitted, Then a RotationCompleted event is produced containing session_id, rotation_version, affected_attendee_count, and occurred_at. - Given webhook delivery to subscriber U fails with a non-2xx response, When retries are attempted, Then the system retries with exponential backoff (initial 1s, max 60s, jitter) up to 8 attempts and records attempt history. - Given duplicate deliveries occur, When the subscriber receives events with the same event_id, Then the payloads are identical and event_id is stable for de-duplication. - Given all retry attempts are exhausted, When delivery still fails, Then the event is moved to a dead-letter queue and is visible via the status API.
Rotation Status API and Observability
- Given a session S, When querying GET /rotation-status?session_id=S.id, Then the response includes current_rotation_version, scheduled_rotation_time, grace_period_minutes, last_rotation_at, and rotated_attendee_count. - Given a rotation is in progress, When querying status, Then progress percentage and in-flight metrics are returned and update at least every 5 seconds. - Given an attendee A in S, When querying status with attendee_id, Then the response shows current token version for A (without revealing the token) and whether grace is active. - Given an audit request for session S, When retrieving logs, Then rotation events, webhook attempts, and validation failures are available with timestamps and correlation IDs.
Threaded Comms Auto-Update (Email/SMS/Calendar)
"As a student, I want any last-minute link update to appear in the same email/SMS/calendar thread I already have so that I don’t miss it or wonder which message to trust."
Description

Automatically pushes the rotated link into the existing communication threads without creating new conversations. For email, sends an update using the original Message-ID threading headers and updates the calendar invite by reissuing the ICS with the same UID and incremented SEQUENCE. For SMS, sends a follow-up message from the same sender number within the same thread, referencing the class and including the new short link. Ensures consistent copy/templates, localized content, quiet hours compliance, and link preview behavior. Guarantees that recipients see the update in the same thread they already have, minimizing confusion and support contacts.

Acceptance Criteria
Email: Update Appears in Original Thread
Given an attendee received the class confirmation email with Message-ID=M and a known thread in their inbox When the join link is rotated within 15 minutes before class start Then an email update is sent using the same From/Reply-To as the original, with In-Reply-To=M and References including M, and unchanged Subject And the email renders the new join link in the body with consistent template copy And in Gmail, Outlook.com, and Apple Mail, the update appears in the same thread as the original And only one update email is sent per rotation per recipient, even on retry
Calendar: ICS Update Replaces Prior Invite
Given the original calendar invite was sent with UID=U and SEQUENCE=S When the join link is rotated Then a new ICS is issued with UID=U and SEQUENCE=S+1, METHOD=REQUEST, updated DESCRIPTION/LOCATION containing the new link And attendee list is unchanged, organizer is unchanged, and DTSTAMP is current And on Google Calendar, Outlook (desktop/web), and Apple Calendar, the existing event is updated in place without creating a duplicate And attendee RSVP statuses are preserved
SMS: Follow-up Message in Same Thread
Given an attendee received the original SMS confirmation from sender number X When the join link is rotated within 15 minutes of class start Then a follow-up SMS is sent from sender number X in the same thread, referencing the class name/date/time and including the new short link And messages exceeding 160 characters are concatenated with proper UDH and display as a single thread on iOS and Android And only one SMS update is sent per rotation per recipient, even on retry And opt-out compliance language/STOP handling is preserved
Quiet Hours: Channel-Specific Compliance
Given the recipient's quiet hours are configured as 10:00 PM–7:00 AM in their local time zone When the link rotation occurs during quiet hours Then SMS updates are not sent during the quiet hours window and are queued to send at the window end And email updates follow the configured quiet-hours policy (sent if allowed; otherwise queued), with decisions logged per recipient and channel And calendar ICS updates follow the same policy without creating duplicate calendar entries or extra notifications
Localization: Consistent Templates and Local Times
Given the recipient profile has locale=fr-FR and time zone=Europe/Paris When the update is sent Then the SMS and email templates render in French with dates/times formatted for fr-FR and Europe/Paris And currency, punctuation, and number formats match the locale And if a translation key is missing, the system falls back to en-US with a logged warning And template variables (class name, start time, instructor, short link) are populated consistently across channels
SMS Link Previews: Controlled Display
Given SMS link preview policy is set to Disabled for rotated join links When the SMS update with the new short link is sent to iMessage and Android Messages clients Then no rich link preview is displayed in either client And the link remains clickable and resolves correctly When the policy is set to Enabled Then a rich preview appears where supported without exposing the token to third-party crawlers
Idempotency: No Duplicate Updates or New Threads
Given the update send operation is retried with the same idempotency key due to a transient failure When email, SMS, and ICS updates are processed Then each recipient receives at most one update per channel and no additional threads or duplicate calendar entries are created And provider/MTA/message IDs are recorded once in audit logs with a single delivery status per channel And subsequent retries are acknowledged without re-sending
Per-Attendee Tokenization & Access Controls
"As a studio owner, I want each attendee to have a unique, controlled link so that shared links don’t grant unintended access and class capacity is protected."
Description

Issues tokens uniquely bound to the attendee’s booking, with optional device fingerprint and single-session concurrency limits. Enforces time-windowed access (e.g., open X minutes before start, expire after class end plus buffer) and geo/time anomaly checks. Detects potential link sharing via concurrent IP/device patterns and triggers soft challenges or blocks. Provides instructor/host tokens with elevated privileges and different lifecycles. Integrates with waitlist and last-minute seats so that when a learner is promoted, a valid token is immediately provisioned and others remain invalid.

Acceptance Criteria
Unique Token Issuance per Booking
Given a confirmed booking with booking_id and attendee_id When the booking is created or confirmed Then the system issues a cryptographically random token uniquely bound to that booking_id and attendee_id And the token is embedded in the attendee’s join URL And the token validates to exactly one active booking And attempting to use the token for any other booking_id responds 403 Unauthorized And the token is stored hashed at rest and includes a scoped TTL tied to the class lifecycle
Device Binding and Single-Session Concurrency
Given device_binding=true and concurrency_limit=1 for the class And an attendee has not yet joined this class When the attendee first joins with device_fingerprint=A Then the token is bound to fingerprint A for the active session And a second concurrent join using fingerprint=B is blocked with 409 Concurrency Limit Reached And if the first session ends or is inactive for grace_period=60s, the next join is allowed and the prior session is invalidated
Time-Windowed Access Control
Given class start_time=T, duration_minutes=D, open_window_minutes=15, expiry_buffer_minutes=10 When a tokenized join request occurs before T-15 minutes Then the request is denied with 403 Not Yet Open When a request occurs between T-15 minutes and T+D+10 minutes Then access is granted with 200 OK When a request occurs after T+D+10 minutes Then access is denied with 410 Expired (or 403 Expired)
Geo/Time Anomaly Detection and Soft Challenge
Given geo_anomaly_radius_km=500 and anomaly_interval_minutes=5 and challenge_mode=soft When two successive requests for the same token originate from IP geolocations >500 km apart within 5 minutes Then a soft challenge (one-time code via SMS/email) is required before access continues And on successful challenge within 5 minutes, access is granted And on failed/expired challenge, the session is blocked with 403 and the event is logged with reason=geo_anomaly
Concurrent IP/Device Link Sharing Enforcement
Given share_detection=true and concurrency_limit=1 When the same token is used concurrently from more than one distinct IP or device_fingerprint Then the newer session is soft-challenged and only one verified session remains active And if the challenge fails or 3 sharing anomalies occur within the class session, the token is blocked for the remainder of the class and an instructor dashboard alert is created
Instructor/Host Token Privileges and Lifecycle
Given a token issued with role=host When the host uses the join link Then access is allowed outside attendee windows (up to 60 minutes before and after class) and bypasses geo/time anomaly checks And host concurrency_limit is >=5 simultaneous sessions And the host can view and terminate attendee sessions for this class via the API And revoking a host token immediately terminates associated sessions
Waitlist Promotion Immediate Token Provisioning
Given a waitlisted learner is promoted to confirmed within 10 minutes of class start When the promotion event is processed Then a unique attendee token is generated and activated within 5 seconds And the attendee portal shows an active Join button using the new token within 5 seconds And non-promoted waitlisted learners have no active tokens; their join attempts return 403 And the promotion and token issuance are audit-logged with timestamps
Late Booking, Reschedule, and Cancellation Resilience
"As a late registrant, I want to receive a valid link right away and have it stay up-to-date if the class time changes so that I can join without confusion."
Description

Handles bookings or changes occurring after rotation time by immediately issuing fresh tokens and updating existing threads. On reschedule, revokes old tokens, schedules a new rotation for the new time, and sends updated ICS/email/SMS in-thread. On cancellation, invalidates all tokens and notifies attendees with a clear message. Consistently reconciles edge cases such as timezone shifts, multi-session series, and instructor swaps while preserving message threading and minimizing duplicate notifications.

Acceptance Criteria
Late Booking After Rotation Cutoff
Given a class instance has already rotated its join link and a learner books at or after the rotation time When the booking is confirmed Then a fresh, attendee-specific tokenized join link tied to the active rotation is issued within 10 seconds And an SMS and an email containing the valid link are sent within 30 seconds, using the existing thread if one exists, otherwise starting a new thread And a calendar invite is created/updated in place within 60 seconds using the same UID for the class instance And the new link is immediately usable (HTTP 200) and scoped to the correct class instance window And no more than one SMS and one email are sent for this booking event per attendee
Reschedule After Link Rotation
Given an upcoming class instance has already rotated its join link and the host reschedules the start time When the reschedule is saved Then all previously issued attendee and host tokens for that instance are revoked within 5 seconds (old links return HTTP 410) And a new rotation schedule is created for the new start time And attendees receive a single SMS and a single email in the existing threads with the new time and a fresh tokenized link within 2 minutes And the existing calendar event is updated in place using the same UID with an incremented SEQUENCE reflecting the new time And the new link is valid and the old link is invalid across all channels And an audit log records the reschedule and token revocations
Cancellation After Link Rotation
Given an upcoming class instance has already rotated its join link and the class is canceled When the cancellation is saved Then all attendee and host tokens for that instance are invalidated within 5 seconds (old links return HTTP 410) And attendees receive a single SMS and a single email in the existing threads with a clear cancellation message within 2 minutes and no join link And the calendar event is canceled in place by sending an ICS with the same UID and METHOD:CANCEL And attempts to access the old link display an informative cancellation message And no duplicate notifications are sent per channel for this cancellation event
Timezone Shift and DST Reconciliation
Given a class's timezone is changed or a DST transition affects the scheduled start before rotation fires When the change is saved Then the rotation trigger time is recalculated relative to the class's new local start time And at most one rotation occurs for the instance (no duplicate rotations) And outbound notifications show the correct start time in each attendee's local timezone And email/SMS updates reuse existing threads And the calendar event is updated in place (same UID, incremented SEQUENCE) without creating duplicate events
Multi-Session Series — Single Occurrence Reschedule
Given a learner is enrolled in a multi-session series and a single occurrence is rescheduled after its link rotation When the reschedule is saved Then only the affected occurrence's tokens are revoked and reissued; other occurrences' tokens remain valid And SMS/email updates reference only the affected occurrence and post in the same series thread And only the affected occurrence's calendar instance is updated in place (same UID/RECURRENCE-ID with incremented SEQUENCE) And attendees receive no more than one SMS and one email for this change
Instructor Swap After Rotation
Given the instructor for an upcoming class instance is changed after the join link rotation When the instructor swap is saved Then host tokens tied to the old instructor are revoked immediately (old host link returns HTTP 401/410) And the new instructor receives a fresh host tokenized link that is immediately usable And attendee links remain valid unless regeneration is required by the meeting backend; if regenerated, attendees receive updated links in existing threads within 2 minutes And SMS/email/ICS reflect the new instructor's name And an audit log records the instructor change and token actions
Duplicate Notification Suppression and Thread Preservation
Given any single state change event (late booking, reschedule, cancellation, timezone shift, or instructor swap) When outbound communications are dispatched Then each attendee receives at most one SMS and one email per event and one ICS update per instance And emails use consistent subject and threading headers (Message-ID/References or In-Reply-To) to remain in the same thread And SMS are sent from the same sender/number to remain in the same thread And retries due to transient failures do not create duplicates because messages are de-duplicated by event ID And all message-sending operations are idempotent on reprocessing
Admin Rotate & Reissue Console
"As support staff, I want a console to see and manage link rotations and resend or reissue tokens so that I can quickly resolve attendee access issues."
Description

Provides an internal dashboard for staff to view rotation status per class, see attendee token states, and take actions: force rotate, reissue a token, resend an update, copy links, or pause rotation for a session. Includes search and filters, role-based access controls, and audit trails of manual actions. Surfaces health indicators (delivery success rates, pending updates) and contextual tips for common support scenarios to speed resolution of “wrong link” tickets.

Acceptance Criteria
RBAC Access and Action Permissions
Given a signed-in user with role "Admin" or "Support Agent" When they navigate to the Admin Rotate & Reissue Console Then the console loads with HTTP 200 and permitted actions are enabled for their role And restricted actions are disabled with tooltips explaining required role Given a signed-in user with role "Instructor" or "Viewer" When they navigate to the console URL Then access is blocked with HTTP 403 and a friendly message is displayed and no data is leaked Given a "Support Agent" When they attempt Force Rotate Then the UI blocks the action with an authorization error And the event is logged in the audit trail as a denied action Given an "Admin" When they perform Force Rotate, Reissue, Resend, Copy Link, or Pause/Resume Then the action succeeds and an audit record is created with actor, role, timestamp, class ID, attendee ID (if applicable), action, and outcome
Class List: Search, Filters, and Health Summary
Given there are 1,000+ upcoming classes with varied instructors and rotation states When a user enters a keyword (e.g., "Yoga") and applies filters (date range, instructor, location, rotation state, health = At Risk) Then the results reflect the query accurately and return within 800 ms for 95th percentile Given a user clears filters When the page reloads Then the full, default list is shown sorted by start time ascending Given the list view displays health indicators per class When delivery success rate < 95% or pending updates > 5 Then the class row is flagged "At Risk" with an accessible label and tooltip showing metric breakdown Given a user clicks Export on the filtered list When the export completes Then a CSV with columns (Class ID, Start Time, Instructor, Rotation State, Health% Success, Pending Updates) downloads and matches on-screen results
Class Detail: Rotation Status and Attendee Token States
Given a user with access opens a specific class detail page When the page loads Then it shows rotation status (Scheduled/Rotated/Paused), last rotation timestamp, next scheduled rotation window, and countdown if < 2 hours Given attendees exist for the class When the attendee table renders Then each row shows attendee name, token state (Active/Pending Update/Invalidated/Reissued), last notification time per channel (SMS/Email/Calendar), and link domain Given a token state changes in the backend (e.g., reissued) When the page is open Then the UI reflects the change within 5 seconds without full page refresh Given the class is in Rotated state When an old link is tested Then it returns an invalid token response (HTTP 410 or app-equivalent) while the new link succeeds (HTTP 200)
Force Rotate Session Links
Given a class scheduled to start within the next 3 hours and auto-rotation not yet executed When an Admin clicks Force Rotate and confirms Then new tokens are generated for all attendees, old tokens invalidated, and update messages are queued within 5 seconds Given Force Rotate completes When attendees use the previously issued link Then access is denied with a clear "link expired/rotated" message and guidance to check latest message Given Force Rotate is triggered twice within 60 seconds When the second request arrives Then it is rejected as a duplicate with an idempotency notice and no additional tokens are created Given Force Rotate completes When viewing the class detail Then rotation status updates to Rotated, last rotation timestamp is set, health metrics refresh, and an audit record is stored including token hash prefixes (masked)
Reissue Token for a Single Attendee
Given an attendee with token state Active or Invalidated When an Admin selects Reissue and chooses delivery channels (SMS/Email/Calendar) Then a new token is generated only for that attendee, old token is invalidated, and selected channel messages are sent within 10 seconds Given an attendee lacks a verified channel (e.g., no SMS) When the Admin selects that channel Then the UI prevents selection with a validation message and suggests available channels Given Reissue completes When the attendee opens the newly issued link Then it joins the correct class room/session and the old link fails with a rotated/invalid message Given Reissue is performed When viewing audit logs Then an entry appears with actor, attendee ID, old/new token hash prefixes (masked), channels sent, and delivery outcomes
Resend Update and Copy Link
Given an attendee with an existing valid token When an Admin clicks Resend Update and selects SMS and Email Then the same token is resent on the selected channels and delivery outcomes are displayed in-line within 15 seconds Given an Admin clicks Copy Link on an attendee row When the action completes Then the attendee-specific URL with the current valid token is placed on the clipboard and a toast confirms success; the UI displays the token masked while the clipboard contains the full link Given Resend fails on a channel (e.g., SMS provider error) When the retry option is clicked Then the system retries with exponential backoff up to 3 attempts and logs outcomes per attempt in the audit trail
Pause and Resume Rotation for a Session
Given a class with auto-rotation scheduled When an Admin clicks Pause Rotation Then auto-rotation for that class is skipped for the current cycle and the class status shows Paused with a visible banner and reason Given a class is Paused When time passes into the usual rotation window Then no automatic token changes occur and no update messages are sent Given a class is Paused When an Admin clicks Resume Rotation Then the next scheduled rotation is recalculated and displayed; Force Rotate remains available during Pause Given Pause or Resume is executed When viewing audit logs Then an entry exists with actor, previous state, new state, timestamp, and optional reason
Delivery Reliability & Fallbacks
"As an operations manager, I want failed link updates to automatically retry or use a fallback channel so that students still receive the correct link before class begins."
Description

Monitors and improves deliverability of updates through retries, alternative channels, and short-link management. Tracks SMS delivery receipts and email engagement signals; if delivery is uncertain, triggers a fallback channel (e.g., email if SMS fails, SMS if email bounces). Manages branded short domains with per-token links, rate limiting, and anti-spam safeguards. Provides per-class delivery dashboards and alerting for high failure rates prior to class start.

Acceptance Criteria
SMS Retry and Email Fallback on Undelivered Rotate Update
Given a scheduled class with Silent Rotate enabled and a participant having a valid phone and verified email And the rotate update is initiated 15 minutes before class start When the system sends the SMS containing the participant’s per-token short link Then a "delivered" receipt must be received within 2 minutes or the SMS is marked uncertain And if the SMS is uncertain or failed, an email with the same tokenized link is sent within 1 minute And the system must not send more than 3 SMS attempts per participant, with exponential backoff (1m, 3m) And the participant’s delivery outcome is "Success" if either SMS delivered receipt is received or the link is clicked from any channel And all attempts, receipts, and timestamps are logged to the per-class delivery dashboard within 30 seconds
Email Bounce or No-Engagement Triggers SMS Fallback
Given a participant is opted into both email and SMS and not opted out of SMS When the rotate update email with a per-token short link is sent Then if a hard bounce is detected, an SMS fallback is sent within 1 minute And if no email open or link click is detected within 5 minutes and class start is in ≤ 15 minutes, an SMS fallback is sent And no more than 1 email and 1 SMS fallback are sent for this rotate event per participant And thread continuity is preserved by replying in the existing SMS/email thread where supported And the delivery outcome is "Success" if the SMS is delivered or the link is clicked; otherwise set to "Failed" 5 minutes before class if neither delivered nor clicked
Branded Short Domain Health Check and Auto-Fallback
Given an organization has configured a branded short domain When generating per-token links for a rotate event Then links use the branded domain over HTTPS with a valid certificate And if DNS resolution fails, TLS validation fails, or upstream returns 5xx for > 30 seconds, the system switches to the default ClassTap short domain within 30 seconds And redirect latency to the final join URL is ≤ 300 ms P95 and ≤ 600 ms P99 during the rotate window And domain health status and current domain (branded or fallback) are displayed on the per-class delivery dashboard
Per-Token Link Rotation, Invalidation, and Abuse Controls
Given a new rotate token is issued for a participant When the new token is activated Then all prior tokens for that participant and class are invalidated within 5 seconds And requests to invalidated tokens return HTTP 410 with a branded help message And valid tokens redirect with HTTP 302 to the current join URL and are scoped to a single participant And link access is rate-limited to 5 requests per minute per IP and 20 per hour per token; exceeding limits returns HTTP 429 and is logged And token identifiers provide ≥ 128 bits of entropy and expire 2 hours after class start
Outbound Message Rate Limiting and Anti-Spam Safeguards
Given a rotate event requires notifying multiple participants When dispatching SMS and email messages Then SMS send rate must not exceed the configured provider limit (default 30 msg/sec per sender) and must back off and retry on 429/420 with exponential backoff up to 3 times And no participant receives more than 1 message per channel within any 60-second window for the same rotate event And SMS content includes brand identification and opt-out language; noncompliant messages are blocked and surfaced on the dashboard And dispatch completes within 3 minutes for up to 2,000 recipients while honoring rate limits
Per-Class Delivery Dashboard and High-Failure Alerting
Given a class with pending or in-progress rotate updates When viewing the per-class delivery dashboard Then the dashboard displays counts and percentages by channel state (Queued, Sent, Delivered, Failed, Fallback Triggered, Clicked) updated at least every 60 seconds And failure rate is computed as (Failed + Uncertain>10m)/Sent and timestamped And if failure rate ≥ 10% with ≥ 20 recipients between T-30 and T-5 minutes before class, an alert is sent to the instructor via email and SMS within 1 minute containing a link to the dashboard And acknowledging the alert suppresses further alerts for 10 minutes unless failure rate increases by ≥ 5 percentage points And dashboard data exports to CSV and reconciles with message logs within ±1 count
Security, Audit Logging, and Compliance
"As a compliance lead, I want comprehensive logs and strong controls around link issuance and rotation so that we meet regulatory requirements and can investigate incidents with confidence."
Description

Applies encryption at rest and in transit for tokens and related metadata, uses HMAC/nonce protections, and stores minimal PII in links. Maintains immutable audit logs for token issuance, rotation, invalidation, and access attempts, with exportable reports for incident review. Implements configurable anomaly alerts, data retention policies, and compliance controls aligned to GDPR/CCPA and SOC 2, ensuring that security posture is upheld without degrading user experience.

Acceptance Criteria
Encrypt Tokens and Metadata at Rest and In Transit
Given the system stores join tokens and related metadata When data is written to persistent storage Then it is encrypted at rest using AES-256-GCM with keys managed by a KMS and rotated at least every 90 days Given an API or webhook transmits tokens or metadata When data is sent over the network Then TLS 1.2+ (prefer TLS 1.3) with PFS (ECDHE) is enforced and HSTS is enabled Given a service attempts to connect without strong TLS When a weak cipher, protocol below TLS 1.2, or invalid certificate is presented Then the connection is rejected and an audit event is recorded Given backups or exports contain token metadata When backups are created or restored Then backups are encrypted at rest and in transit using the same controls and keys are access-controlled via least privilege
Signed, Nonce-Protected, Minimal-PII Join Links
Given a join link is generated for a class session When the link is created Then the URL contains no direct PII (no name, email, phone) and only an opaque token and timestamp parameters Given a join link is generated When it is signed Then an HMAC-SHA256 signature is computed over token + timestamp + scope using a server-held key and appended as a signature parameter Given a client presents a join link When the server validates it Then the HMAC signature is verified, the nonce is checked for one-time use, the token scope matches the class/session, and the TTL has not expired Given a rotation occurs When a new token is issued Then the prior token is invalidated immediately and further requests using it fail with HTTP 401 and are logged as invalid attempts
Immutable Audit Logging for Token Lifecycle and Access
Given token lifecycle events (issuance, rotation, invalidation) and access attempts occur When these events happen Then an append-only audit record is written capturing event type, UTC timestamp, org_id, class_id, actor_id (or system), request_ip, user_agent, reason_code, and a hashed token reference (no raw token) Given audit records are stored When records are persisted Then they are protected via immutability controls (WORM or hash-chain with periodic anchor) and write/delete operations are denied except through retention policy enforcement Given auditors query logs When filtering by time range and fields Then results return within 2 seconds for up to 10k events and within 60 seconds for up to 100k events Given time synchronization is required When events are recorded Then system clocks are NTP-synced with max drift under 200ms and drift metrics are logged
Audit Log Export with Role-Based Access and MFA
Given an organization admin requests an audit export When the admin is authenticated and has the Audit.Export permission and active MFA Then the export UI/API allows selecting format (CSV/JSON), time range, event types, and fields without exposing raw tokens or PII Given an export job is submitted When the dataset is <= 100k events Then the export completes within 60 seconds and is available via a signed, time-limited download URL (<= 15 minutes) Given access to audit exports is sensitive When a user without the required role or without MFA attempts export Then the request is denied with HTTP 403 and an audit event is recorded Given an export is generated When it is downloaded Then the download is logged with requester identity, IP, timestamp, and checksum of the exported file
Configurable Anomaly Detection and Alerting
Given security anomalies must be detected When more than 5 invalid link attempts occur for the same class within 10 minutes from distinct IPs Then an anomaly alert is generated and delivered to configured channels (email, SMS, webhook) within 60 seconds Given rotation behavior may be abused When more than 3 rotations are triggered for a single session within 30 minutes Then an alert is generated with context (org_id, class_id, actor_id, counts) and recommended actions Given geo/IP changes can indicate compromise When access attempts for a single token originate from 3+ countries in 1 hour Then the token is auto-invalidated and an alert is sent Given organizations have different risk appetites When an admin updates thresholds or channels Then changes take effect within 5 minutes and are audited
Data Retention and GDPR/CCPA Deletion/Pseudonymization
Given a data retention policy is configured per organization When the retention window is set (30–730 days) Then audit records older than the window are purged automatically within 24 hours while preserving aggregate metrics Given a data subject access/deletion request is received When the subject is verified Then all direct PII related to the subject is deleted or pseudonymized in tokens/metadata within 72 hours, while retaining security logs with irreversible pseudonyms (salted hash) and no raw PII Given backups contain data subject information When a deletion request is processed Then backups are exempt from immediate purge but documented, and restoration procedures include re-applying deletions before making data accessible Given compliance reporting is required When exporting a deletion proof report Then the report includes request id, timestamps, scope of data affected, fields anonymized/deleted, and verification evidence
Security Controls Do Not Degrade User Experience
Given a join link is validated at click time When a participant opens the link Then server-side verification adds no more than 150ms p95 latency per request within the primary region and maintains 99.9% success rate for valid links Given a silent rotation occurs before class When the system updates SMS/email/calendar threads Then the new link appears in the same conversation/thread within 60 seconds and the old link shows a friendly expiration page with guidance, without exposing sensitive details Given an invalid or expired link is used When the participant clicks it Then they are redirected to a help screen that offers a one-tap path to the latest valid link (if authenticated) or prompts minimal, secure re-auth, and the event is logged Given accessibility requirements When the expiration/help page is rendered Then it meets WCAG 2.1 AA for contrast, focus order, and screen reader labels

Join Pass

Lets attendees save a dynamic Join Pass to Apple Wallet/Google Wallet with a time-aware Join button and QR fallback. Passes auto-refresh on rotation and work offline, making access frictionless even when users can’t find the original message.

Requirements

Wallet Pass Generation & Delivery
"As an attendee, I want to save my class pass to Apple Wallet or Google Wallet so that I can access class details and join with one tap without searching emails or texts."
Description

Generate per-booking Apple Wallet (PKPass) and Google Wallet passes that include class title, date/time, location or online meeting link, attendee name, instructor, and booking status. Provide Add to Apple Wallet and Save to Google Wallet actions on the confirmation screen and in SMS/email receipts, with deep-link fallbacks. Sign passes with platform-required certificates/keys, assign unique serials, and store minimal pass metadata for updates. Support single classes, series, and multi-spot bookings. Ensure delivery reliability with retry and link-expiry controls, and respect privacy by excluding sensitive data.

Acceptance Criteria
Add to Apple Wallet from Confirmation Screen (Single Booking)
Given a confirmed single-class booking on an iOS device When the user taps "Add to Apple Wallet" on the confirmation screen Then a PKPass is generated and signed with the platform Apple Wallet certificate And the pass includes class title, start date/time with timezone, venue address or "Online" with meeting link, attendee name, instructor name, and booking status And the pass has a unique serial number for this booking And the Wallet sheet presents and the pass installs successfully without duplicates (updates existing if previously installed) And no sensitive data (email, phone, payment details, internal notes) is included
Save to Google Wallet from Email/SMS with Deep-Link Fallback
Given a confirmed booking and a receipt delivered via email or SMS When the user taps "Save to Google Wallet" on an Android device with Google Wallet available Then a Google Wallet pass/object is created with class title, start date/time with timezone, venue address or "Online" with meeting link, attendee name, instructor name, and booking status And the pass is saved to the user's Google Wallet without errors When the same link is opened on an unsupported device/browser Then a responsive fallback page is shown with guidance and device-aware deep links (Apple pkpass on iOS, Google Wallet on Android)
PKPass Signing and Unique Serial Assignment
Given pass generation is requested for Apple Wallet When the pass payload is assembled Then it is signed using the configured Apple Wallet certificate/private key and validated before delivery And the pass is assigned a globally unique serial per booking (and per occurrence/spot where applicable) And if signing or validation fails, no pass is delivered, an error is logged with correlation ID, and the user sees a retry-safe message
Pass Content Accuracy and Privacy Exclusions
Given a booking exists When generating a wallet pass Then displayed fields exactly match the booking: class title, date/time with timezone, location (street address) for in-person or an https meeting/join link label for online/hybrid, attendee name, instructor name, and booking status (Confirmed/Cancelled/Waitlisted) And field formats meet Apple/Google schemas and localize to the booking locale And sensitive data (email, phone number, payment method/amount, health info, internal notes) is excluded from the pass payload
Series and Multi-Spot Booking Support
Given a booking for a series with N scheduled occurrences When generating passes Then one pass is generated per occurrence with occurrence-specific date/time and location And delivery surfaces provide per-occurrence save actions and an "Add all" option Given a booking with M spots for the same occurrence When generating passes Then M distinct passes are created (one per spot) with unique serials and attendee labeling (named attendees or "Spot X of M")
Delivery Reliability: Retry and Link Expiry Controls
Given pass generation or delivery to the client fails due to a transient error When the attempt fails Then the system retries up to 3 times with exponential backoff (1m, 5m, 15m) and records outcomes And if all retries fail, the user-facing link offers a manual regenerate option without creating duplicate serials Given a wallet pass link/token is issued When X days have elapsed since issuance (X=7) Then the link expires and returns HTTP 410 with a secure path to request a fresh link after re-authentication
Minimal Metadata Storage and Update Propagation
Given a pass has been issued When storing data for future updates Then only minimal metadata is persisted: booking_id, wallet_type (apple/google), serial/object_id, booking_status_hash, and last_synced_at (timestamps), encrypted at rest And no PII (email, phone, payment details) is stored in wallet metadata Given a booking status or schedule changes (e.g., cancel, reschedule) When the change is saved Then the system uses stored metadata to push an update to the existing pass within 60 seconds or flags for next sync if offline, without reissuing a new serial
Time-aware Action Button Logic
"As an attendee, I want the pass button to change to the right action at the right time so that I always know how to get into class or check in with minimal friction."
Description

Implement a server-driven rules engine that controls the primary action on the pass based on schedule context. Before the class, show Add to Calendar or View Location; within the configurable join window for hybrid/online classes, show Join Now to open the meeting URL; at the venue, show Show QR for check-in; after start, show Late Check-in if permitted; after end, show Class Ended. Enforce time zones, daylight saving changes, and instructor overrides. Ensure the action works with one tap from the pass and degrades gracefully if the device is offline.

Acceptance Criteria
Pre-Class Primary Action: View Location vs Add to Calendar
Given an in-person class and current time is before the server-configured join window When the pass is displayed Then the primary action label is "View Location" and a single tap opens the default maps app to the venue using the server-provided address or lat/long Given an online-only class and current time is before the join window When the pass is displayed Then the primary action label is "Add to Calendar" and a single tap opens the OS calendar add flow pre-filled with title, start/end, and event timezone Given a hybrid class and current time is before the join window When the pass is displayed Then the primary action label is "View Location" and the map deep link opens in one tap Rule: The primary action does not require more than one user interaction (no intermediary confirmations within the app)
Join Window Action: Join Now Opens Meeting URL
Given a hybrid or online-only class and current time is within the server-configured join window When the pass is displayed Then the primary action label is "Join Now" And a single tap opens the meeting URL in the native app if installed, otherwise in the browser And the meeting URL is the server-provided canonical URL Given the device is offline within the join window When the user taps "Join Now" Then the app surfaces the cached meeting URL with options to Copy and Open (deferred) without crashing And the action label remains "Join Now" and state is preserved until connectivity returns Rule: The action switches to "Join Now" no later than 60 seconds after entering the join window and reverts appropriately if the server updates the window
On-Site Check-in Action: Show QR
Given current time is within the server-defined check-in window and the server action is CHECK_IN When the pass is displayed Then the primary action label is "Show QR" And a single tap displays a scannable QR containing the server-issued booking token And the QR screen loads in under 500 ms on a mid-tier device Given the device is offline When the user taps "Show QR" Then the QR renders from cached token and can be successfully scanned by the ClassTap scanner to check in Rule: If the server updates the action away from CHECK_IN, the pass updates the label within 60 seconds
Post-Start Action: Late Check-in When Permitted
Given class has started and instructor allows late check-in within a server-configured grace period When the pass is displayed Then the primary action label is "Late Check-in" And a single tap triggers the late check-in flow: either opens the QR for late check-in or posts to the late check-in endpoint, as configured by the server Given instructor does not allow late check-in or grace period has elapsed When the pass is displayed Then the late check-in action is not shown and the next applicable action is rendered per rules Given the device is offline during permitted late check-in When the user taps "Late Check-in" Then the cached late check-in QR/token is shown and is accepted by the ClassTap scanner upon reconnection at the door
Post-End Action: Class Ended State
Given current time is after the server-reported class end time When the pass is displayed Then the primary action label is "Class Ended" And the button is visually disabled and non-tappable And no navigation occurs on tap Rule: If the server extends the end time, the pass removes the "Class Ended" state within 60 seconds and restores the correct action
Time Zone and Daylight Saving Enforcement
Given the event timezone is set by the organizer (e.g., America/Los_Angeles) When a user in any device timezone views the pass Then the join window, check-in window, and end time are computed using the event timezone and not the device timezone Given a class scheduled across a DST transition in the event timezone (e.g., starts at 01:30 during fall-back) When the join window opens Then the action transitions occur at the correct absolute instant accounting for the DST shift Rule: Server responses include timezone-aware timestamps (ISO 8601 with offset or Z) and the client honors them without applying additional device-local shifts
Instructor Overrides Precedence and Expiry
Given an instructor sets an override for the primary action with a target label and destination until a specified expiry timestamp When the pass is displayed within the override’s effective window Then the overridden action label and tap behavior are shown in place of the computed default And the change is reflected on the pass within 60 seconds of the override being published Given the override expires or is revoked by the instructor When the pass next refreshes Then the action reverts to the server-computed default within 60 seconds Rule: If the server rejects an override as invalid, the pass continues to display the last valid action and logs the error state (no broken or empty action shown)
Rotating QR Check-in Fallback
"As a front-desk staff member, I want to scan a secure QR on the attendee’s pass so that I can verify their booking even if links fail or connectivity is poor."
Description

Embed a unique QR code on the pass that encodes a short-lived, signed token tied to the booking, class, and device. The QR can be scanned by the ClassTap Host app or web scanner to confirm attendance, with replay protection and rate limiting. Rotate the token periodically when online and persist the last valid token for offline display. Prevent sharing by limiting scans per booking and invalidating tokens after successful check-in or after the time window. Provide human-readable backup code for manual lookups.

Acceptance Criteria
Signed, Short-Lived QR Token Encoding and Rotation
Given a confirmed booking has an active Join Pass and the device is online When the pass is opened Then the QR contains a signed, short-lived token including booking_id, class_id, device_id, iat, exp, nonce, contains no PII, and the signature verifies against the server public key Given the device remains online When 30 seconds elapse Then a new token is generated and displayed, replacing the prior token within 5 seconds Given the device is offline When the pass is opened Then the last unexpired token generated in the past 10 minutes is displayed Given a token is expired When it is scanned Then validation fails with reason "Expired" and attendance is not recorded
Online Scan Validation with Replay Protection
Given the Host app or web scanner is online When it scans a QR token Then it validates the signature, token freshness (current server time < exp), and that booking_id, class_id, and device_id match an existing, eligible booking Given a token has already been scanned successfully When the same token is scanned again Then the scan is rejected with reason "Token already used" Given the token payload or signature is tampered When scanned Then the scan is rejected with reason "Invalid signature" Given the booking is canceled, transferred, or otherwise ineligible When its token is scanned Then the scan is rejected with an appropriate ineligibility reason and attendance is not recorded
Post-Check-in Invalidation and Single-Use Enforcement
Given a booking has not yet been checked in and is within the configured check-in window When a valid token for that booking is scanned successfully Then the booking is marked "Checked in", all tokens for that booking are immediately invalidated, and the pass state updates to "Checked in" within 10 seconds Given any token for a booking that is already checked in When scanned Then the scan is rejected with reason "Already checked in" Given the configured check-in window has elapsed When any token for that booking is scanned Then the scan is rejected with reason "Outside check-in window"
Rate Limiting of Scan Attempts per Booking
Given any single booking When more than 10 scan attempts (valid or invalid) occur within 60 seconds across scanners Then subsequent attempts within that window are rejected with reason "Too many attempts" and the event is logged Given any single scanner device When more than 30 scans are attempted across bookings within 60 seconds Then subsequent attempts within that window are throttled with reason "Rate limited" Given repeated invalid scans for the same booking exceed 5 within 5 minutes When further scans are attempted Then scans are temporarily blocked for 2 minutes for that booking with reason "Temporarily locked"
Offline Display with Last Valid Token Persistence
Given the pass device is offline and has previously fetched a valid token When the pass is opened Then the last unexpired token is shown in the QR and its valid-until time is reflected on the pass Given the pass device returns online When the pass is reopened or receives a background update Then token rotation resumes within 5 seconds without user action Given the pass device is offline and no cached token exists When the pass is opened Then no QR token is shown and the backup code is prominently displayed for manual lookup
Manual Backup Code Lookup
Given a user cannot present a scannable QR When they present the backup code displayed on the pass Then the code format is 10-16 characters, grouped (e.g., AAAA-BBBB-CCCC), case-insensitive, excludes ambiguous characters (O, 0, I, 1), and maps to the booking Given a host enters a valid backup code in the Host app or web scanner When submitted Then the system returns the matching booking and allows check-in subject to the same eligibility and rate-limit rules as QR scans Given an invalid or expired backup code is entered When submitted Then the system responds "Code not found or expired" and does not record attendance
Real-time Pass Updates & Token Rotation
"As an attendee, I want my wallet pass to update automatically when class details change so that I always have the latest information without re-adding the pass."
Description

Push updates to Wallet passes when class details or booking status change, including start time, venue, instructor, waitlist promotion, cancellations, and transfer. Integrate with Apple Wallet push update APIs and Google Wallet real-time updates, manage certificates and issuer keys, and handle retries and throttling. Update pass fields, messages, and the time-aware action state, and rotate QR tokens on schedule or on-demand. Maintain versioning and audit logs to ensure traceability of every update.

Acceptance Criteria
Update Pass on Class Detail Change
Given a confirmed booking with a Join Pass saved in Apple Wallet or Google Wallet When the class start time, venue, or instructor is changed in ClassTap Then a push update is sent to both wallet platforms within 60 seconds And the pass fields (time, venue, instructor) reflect the new values And the time-aware action state is re-evaluated and updated accordingly And the pass version increments by 1 and no duplicate pass is created And an audit log entry records the update with before/after values and push response IDs
Waitlist Promotion Push Update
Given an attendee on a waitlist with a Join Pass showing status "Waitlisted" and a disabled Join action When the attendee is promoted to confirmed Then the pass status changes to "Confirmed" on both Apple and Google Wallet within 60 seconds And the Join action becomes enabled per the time window rules And the QR token rotates immediately and previous token is revoked And an audit log entry is created linking the waitlist promotion to the pass update And the pass version increments by 1
Cancellation and Transfer Notifications
Given a Join Pass for a booked attendee When the class is cancelled Then the pass shows status "Cancelled", the Join action is disabled, and the QR token is revoked And a cancellation message is displayed with refund/credit info when applicable And both Apple and Google Wallet passes receive the update within 60 seconds When the attendee is transferred to a different class Then the pass updates to the new class details and shows a "Transferred" banner And the previous class QR tokens are invalidated and not accepted by the scanner And audit logs capture cancellation/transfer events with correlating pass versions
Time-Aware Join Action State
Rule: Join action is Disabled when now < start_time - 15 minutes Rule: Join action is Enabled from start_time - 15 minutes to start_time + 30 minutes Rule: Join action changes to "Expired" after start_time + 30 minutes Rule: For hybrid/online classes with a join URL, the action label shows "Join Online" when Enabled; otherwise shows "Check In" Rule: Device timezone differences are accounted for using the class timezone to compute windows Rule: Changes in start_time trigger immediate re-evaluation of the action state on the pass
QR Token Rotation and Offline Fallback
Rule: QR token rotates on a fixed schedule every 5 minutes during the Enabled window and on-demand upon status changes (promotion, transfer, cancellation) Rule: Validation accepts only the current and immediately previous token for a maximum overlap of 10 minutes Rule: Pass displays the most recent token while offline; scanner validates tokens offline using signed, time-bounded payloads Rule: On cancellation or transfer, server marks all prior tokens as revoked; scanners reject them on next sync or immediately if online Rule: Token payload includes pass_id, class_id, attendee_id, expiry, and signature; no PII is embedded
Reliability, Rate Limits, and Credentials Management
Rule: Push updates use idempotency keys per pass update; duplicate deliveries do not create duplicate versions Rule: Transient failures are retried with exponential backoff up to 6 attempts; final failures go to a dead-letter queue with alerting Rule: Rate limiting respects Apple/Google quotas; backlog is queued and drained without exceeding provider thresholds Rule: Apple Wallet pass certificates are monitored and rotated at least 14 days before expiry with zero-downtime key rollover Rule: Google Wallet issuer keys are rotated without breaking existing passes; all API calls use the current active key Rule: Operational metrics (success rate, latency, retry count) and alerts are available via dashboard and emitted to logs
Versioning and Audit Logging
Rule: Each pass update increments a monotonically increasing version number stored on the pass and backend Rule: Audit log entry includes timestamp, actor (system/user), change type, before/after fields, target pass IDs, token version, and provider response IDs Rule: The system can reconstruct the pass state at any historical timestamp using versioned records Rule: Audit logs are immutable, tamper-evident, and retained for at least 13 months; access is RBAC-controlled Rule: A read-only endpoint exposes update history filtered by pass_id or booking_id
Offline Validation & Resilience
"As a studio staff member, I want to validate passes offline so that check-in continues smoothly during network outages or in low-connectivity locations."
Description

Enable check-in without internet by encoding a signed claim in the QR that includes booking id, class id, valid-from/until, and a nonce, verifiable offline with a public key embedded in the scanner. Queue check-ins for later sync, prevent double use with local caching, and handle clock drift with a small grace period. Ensure the pass itself fully renders offline with essential details and provides a confirm code for manual verification if scanning is unavailable.

Acceptance Criteria
Offline QR Signature Verification
Given the scanner device has no internet connectivity And a Join Pass QR encodes a signed claim containing booking_id, class_id, valid_from, valid_until, and nonce And the scanner has the correct public key embedded When the QR is scanned Then the signature is verified offline against the embedded public key And the payload fields are present and parse to valid formats And verification completes in <= 1 second on a mid-tier device And if signature verification fails, the scan is rejected with "Invalid pass" and no check-in is recorded
Local Double-Use Prevention (Offline)
Given the scanner is offline And a pass for booking_id X and class_id Y is successfully checked in When any pass for booking_id X and class_id Y is scanned again before sync Then the app blocks the check-in using a local cache And displays "Already checked in at [HH:MM]" And the block persists across app restarts until class end time or successful sync
Queued Offline Check-ins and Idempotent Sync
Given there are N offline check-ins queued on the device When connectivity is restored Then the device transmits the queued check-ins within 10 seconds And preserves the original local check-in timestamps And retries failed transmissions with exponential backoff until acknowledged And the server treats duplicate submissions idempotently (no duplicate attendance) And the local queue entries are only removed after acknowledgment is received
Clock Drift Grace Period Handling
Given the scanner device clock may drift by up to +/- 5 minutes from server time When a pass is scanned within [valid_from - 5 minutes, valid_until + 5 minutes] Then the check-in is accepted When a pass is scanned outside this extended window Then the check-in is rejected with "Not yet valid" or "Expired" accordingly
Offline Pass Rendering with Essentials
Given the user's device is offline When the Join Pass is opened in Apple Wallet or Google Wallet Then the pass renders without network calls using embedded assets and data And displays attendee name, class title, class date/time (with timezone), location, booking status, and a visible confirm code And the QR code is visible and scannable And the layout remains intact with offline image fallbacks
Manual Confirm Code Entry (Offline)
Given the scanner camera is unavailable or the QR is unreadable And the device is offline When staff selects "Enter Confirm Code" and inputs the code shown on the pass Then the app validates the code offline and maps it to booking_id and class_id And applies the same validity window and double-use rules as QR scans And upon success, records an offline check-in queued for sync And upon failure, displays "Invalid code" and records nothing
Rotated Token Acceptance (Offline)
Given the pass rotates its QR token periodically And the scanner is offline When any rotated token for a booking is scanned within its valid window Then the scan is accepted if that booking has not been checked in locally And the scan is rejected if that booking has already been checked in locally, regardless of token rotation And tokens with expired valid_until are rejected
Pass Lifecycle Management & Revocation
"As an attendee, I want my pass to reflect booking status changes instantly so that I can trust whether it’s valid to use for entry."
Description

Define lifecycle rules for issuance, activation, suspension, transfer, expiration, and revocation of passes tied to booking state. Auto-issue on confirmation; issue a holding pass for waitlisted users that auto-activates on promotion; mark as void on cancel/refund; transfer ownership on booking transfer; auto-expire after the class with configurable retention. Propagate lifecycle changes to Wallets via push, and surface clear status messaging on the pass.

Acceptance Criteria
Auto-Issue Active Pass on Booking Confirmation
Given a booking transitions from Pending to Confirmed after successful payment When the confirmation event is persisted to the system of record Then a Wallet pass is generated and provisioned to the attendee within 30 seconds And the pass displays status "Active" with class name, date/time, and venue And the QR payload encodes a valid, non-expired token bound to the booking and session And the pass renders with the last known state when the device is offline
Issue Holding Pass for Waitlisted Users and Auto-Activate on Promotion
Given a user is added to a class waitlist When the booking status is Waitlisted Then a Wallet pass is issued showing status "Waitlisted" with Join disabled and QR inactive And the pass includes messaging that seat is not yet confirmed When the user is promoted to Confirmed for the same class Then the existing pass auto-updates to status "Active" within 30 seconds via wallet push And, if push delivery fails, the pass updates on next open within 5 seconds
Void Pass on Cancel or Refund
Given a confirmed booking with an Active pass When the booking is cancelled by attendee or instructor, or a refund is executed Then the pass status updates to "Void" within 30 seconds and the QR token is revoked And the Join action is disabled and hidden from the pass And the revoked token fails validation at check-in with a clear "Voided" reason And the pass cannot be reactivated without a new confirmed booking
Transfer Ownership on Booking Transfer
Given a booking transfer from User A (current holder) to User B (recipient) is approved When the transfer is finalized in the system of record Then User A's pass updates to status "Transferred" (or "Void") and its QR token is revoked within 30 seconds And a new pass is issued to User B with status "Active" and a fresh QR token And both wallet updates are delivered via push with retries until success or 24 hours timeout And check-in only accepts User B's pass token after the transfer completes
Auto-Expire After Class with Configurable Retention
Given a class has an end time configured When current time reaches class end time Then associated passes switch to status "Expired" within 5 minutes and the Join action is disabled And expired pass QR tokens are rejected at check-in And the pass remains viewable for the configured retention period (default 30 days) with status "Expired" And after the retention period, the pass is removed from the active list or archived per platform capabilities
Propagate Lifecycle Changes to Wallets with Reliable Push
Given any pass lifecycle change (issue, activate, void, transfer, expire) When the change is committed to the system of record Then a wallet push update is enqueued and delivered with measured success rate ≥ 99.5% over a rolling 30 days And failed deliveries are retried with exponential backoff for up to 24 hours And, if the device push token is invalid, the pass state syncs on next open within 5 seconds And an audit log records the change, push attempts, and outcomes with timestamps
Token Rotation, Revocation, and Offline Behavior
Given an Active pass is displayed on a device When the pass is opened or the device orientation changes Then the QR token rotates at least every 60 seconds while remaining valid for scanning during its rotation window And if the pass is revoked (cancel/refund/transfer), subsequent tokens are immediately invalid and previously issued tokens fail validation And if the device is offline, the last issued unexpired token remains scannable for up to 10 minutes, after which it is rejected And scanners apply the latest downloaded revocation list so revoked tokens are blocked even when the attendee device is offline
Branded Pass Templates & Theming
"As a studio owner, I want the wallet pass to match my brand so that the experience feels professional and consistent with my other customer touchpoints."
Description

Provide white-label templates that inherit studio branding, including logo, brand colors, background image, and accent color, with dark mode-aware design. Allow per-organization configuration of displayed fields and ordering (e.g., show room number, instructor headshot). Ensure Apple Wallet and Google Wallet constraints are respected for text length and colors. Support localization of all labels and right-to-left languages.

Acceptance Criteria
Brand Colors and Logo Applied to Wallet Pass
Given an organization with uploaded logo asset and defined primary/accent brand colors When a Join Pass is issued for Apple Wallet and Google Wallet Then the logo renders within the designated logo area without stretching, pixelation, or background artifacts, and maintains aspect ratio on both platforms And the pass background and accent colors are applied, mapping to the nearest platform-supported values if out of gamut And the contrast ratio between foreground text and background is at least 4.5:1 in both light and dark modes And no ClassTap branding is visible anywhere on the pass
Dark Mode-Aware Theming
Given a device set to dark mode When the pass is viewed Then text, icons, and accent elements switch to a dark-mode palette ensuring minimum 4.5:1 contrast, and any provided dark-mode logo variant is used; otherwise an automatic safe inversion is applied And when the device is toggled back to light mode, the pass reverts to the light palette with no layout shift or truncation changes And these behaviors are consistent on both Apple Wallet and Google Wallet
Per-Organization Field Selection and Ordering
Given an admin selects a set of fields (e.g., room number, instructor headshot, class title, start time) and specifies their order in the Pass Template configuration When the configuration is saved Then the selection and order persist for the organization When a new pass is issued Then only the selected fields are displayed in the configured order And if a selected field is unsupported on a platform, it is moved to a supported section (e.g., back fields/supporting text) without validation errors
Text Length and Overflow Handling
Given field values exceeding platform-recommended lengths (e.g., long class titles or instructor names) When the pass is rendered Then values are truncated with an ellipsis according to the platform’s truncation rules, with no clipping, overflow, or wrapping outside the allowed area And the pass payload validates successfully on both platforms with zero errors related to text length
Localization and Right-to-Left Support
Given the user’s locale is set to Arabic When a pass is issued and viewed Then all static labels are displayed in Arabic using the organization’s translations (or default locale fallback), date/time and numbers follow the locale format, and the layout mirrors to right-to-left where applicable And if a translation for a label is missing, the system falls back to the organization’s default language without showing placeholder keys And the same behavior applies for other locales (e.g., en-US, fr-FR), with pluralization and capitalization rules respected
Background Image Support and Safe Cropping
Given an organization provides light and dark background images within documented size and format limits When the pass is rendered on Apple and Google Wallet Then the background image is applied and safely cropped within platform-defined safe areas without obscuring essential text or QR code And if the image exceeds size or aspect limits, a warning is presented at upload and a resized version is used automatically without degradation beyond platform guidelines And if no background image is provided, the pass falls back to solid brand colors without validation warnings
Platform Schema and Validator Compliance
Given a configured branded template When generating Apple Wallet (.pkpass) and Google Wallet (JWT/Save) payloads Then both payloads pass their respective official validators with zero errors and zero critical warnings And if an input (e.g., color or text length) violates a platform constraint Then a pre-issuance validation error prevents saving until corrected or an automatic compliant fallback is applied and logged

Abuse Insights

Provides a live audit trail of link opens with device fingerprint, IP city, and outcome (joined, blocked, step-up). Surfaces anomalies and can auto-enable stricter checks for that booking—helping admins spot sharing patterns and resolve issues in seconds.

Requirements

Open Event Tracking Pipeline
"As an admin, I want every access and outcome on a booking link recorded in real time so that I can see an accurate timeline and quickly identify suspicious behavior."
Description

Implements a low-latency, append-only pipeline to capture every booking-link interaction (open, join, blocked, step-up required, verification passed/failed) with timestamp, device identifier, IP-derived city/region, ASN, user agent, referrer, and optional UTM parameters. Provides SDK hooks for booking pages and server-side email redirect handlers to ensure events are recorded from both web and email entry points. Ensures idempotency and deduplication via event IDs, encrypts data at rest, and exposes a query API optimized for per-booking timelines and organization-wide aggregations. Targets sub-200 ms write latency and near-real-time availability so anomalies can be surfaced instantly in the admin console.

Acceptance Criteria
End-to-End Event Capture From Web and Email Entry Points
Given a booking page rendered with the SDK When a user opens a booking link Then an "open" event is recorded with event_id, booking_id, org_id, timestamp (UTC ISO-8601 with ms), device_id, ip_city, ip_region, asn, user_agent, referrer, and any present utm_* parameters Given an email booking link routed via the server-side redirect When the recipient clicks the link Then an "open" event is recorded with referrer="email" and preserved utm_* parameters Given a user successfully joins a session When the join completes Then a "join" event is recorded associated to the same booking_id and device_id Given access controls deny entry When the page blocks the user Then a "blocked" event is recorded for that booking_id and device_id Given step-up verification is required When the challenge is presented Then a "step_up_required" event is recorded; and When the user completes the challenge Then either a "verification_passed" or "verification_failed" event is recorded
Idempotent Write and Duplicate Suppression
Given the ingest API receives multiple writes with the same event_id within 24 hours When the requests are processed Then exactly one event is persisted and subsequent requests return a success with a dedup indicator Given two writes with different event_id but identical payload When processed Then two distinct events are persisted Given 100 concurrent retries of the same event_id due to client timeouts When processed Then at most one stored record exists for that event_id Given out-of-order arrival of the same event_id from different entry points When processed Then only one canonical record is stored
Low-Latency Ingest and Near-Real-Time Availability SLOs
Given steady load of 2,000 events/second and bursts up to 5,000 events/second When measuring end-to-end write latency Then p95 <= 200 ms and p99 <= 300 ms Given events are written to the pipeline When querying for those events via the query API Then 95% of events are visible within 2 seconds and 99% within 5 seconds of receipt Given ingestion pressure or transient retries When observed over a 10-minute window Then no more than 0.01% of events miss the p99 visibility target
Per-Booking Timeline Query Performance and Ordering
Given a booking_id with up to 500 events When requesting its timeline via the query API Then events are returned in ascending timestamp order with a stable secondary sort by event_id for identical timestamps Given deduplication at write time When the timeline is requested Then no duplicate event_id appears in the response Given pagination with limit=100 and a cursor When requesting pages Then each page returns within 150 ms p95 and 300 ms p99 and includes a next_cursor until all events are retrieved
Organization-Wide Aggregation Query Accuracy and Performance
Given an org_id and a 24-hour time window When requesting aggregations grouped by event_type, ip_city/ip_region, asn, referrer, and utm_source Then counts equal the totals obtained by scanning raw events with the same filters Given typical production load When calling the aggregation endpoint for up to 10 million events in range Then p95 response time <= 1,000 ms and p99 <= 2,000 ms Given filters for event_type, booking_page_id, referrer, and utm_* When applied to aggregations Then results reflect the filters and remain within the stated SLOs
Append-Only Storage Guarantees
Given a client attempts to update an existing event by event_id When calling the API Then the request is rejected (e.g., 405 Method Not Allowed) and no mutation occurs Given a client attempts to delete an event by event_id When calling the API Then the request is rejected and the original event remains retrievable Given repeated reads of the same event_id over time When the payloads are compared Then they are byte-identical, evidencing immutability
Encryption at Rest Compliance
Given events persisted to the primary data store When inspecting infrastructure configuration via provider APIs Then encryption at rest is enabled for all storage artifacts holding event data Given a key rotation operation is performed according to policy When new events are written Then writes succeed and new data is encrypted under the rotated key Given backups or snapshots are produced When examined via provider APIs Then they are also encrypted at rest
Device Fingerprinting & Session Correlation
"As a fraud analyst, I want opens correlated to stable device identifiers so that I can distinguish one person re-opening a link from sharing with multiple devices."
Description

Generates a privacy-preserving device identifier using limited-entropy signals (e.g., user agent, platform, timezone) plus a first-party persistence token to correlate repeated opens from the same device across a booking. Hashes and rotates identifiers on a configurable schedule, gracefully degrades when storage is blocked, and respects user consent settings. Associates events to devices and sessions, handles re-association after step-up verification, and exposes a consistent device_id for UI and rules engine consumption without storing sensitive raw attributes.

Acceptance Criteria
Initial Device ID Generation on First Open
Given a user opens a booking link for the first time on a device with storage available and consent granted When the booking page initializes the Abuse Insights client Then the system generates a device_id using only limited-entropy signals plus a random first‑party persistence token And the device_id is a 64‑character lowercase hex string And no raw device attributes are persisted at rest; only salted hashes or derived values are stored And the persistence token is stored in first‑party storage only and is readable by the same origin And subsequent opens on the same device within the rotation window produce the same device_id for that booking
Identifier Rotation Schedule Enforcement
Given rotation_period_days is configured to N days and a device_id was last generated at T0 When the user next opens the booking page at time T >= T0 + N days Then a new device_id is generated and persisted, and it differs from the previous device_id And the previous device_id is no longer emitted for new events And historical events remain queryable by their original device_id And the rotation timestamp is recorded for audit
Graceful Degradation When Storage Is Blocked
Given the browser blocks first‑party storage or the user has disabled cookies/local storage When the user opens the booking link Then the system issues an ephemeral, session‑scoped device_id computed from limited‑entropy signals and an in‑memory salt And no persistence token is written to storage And repeated requests within the same browser session reuse the same device_id, but a new browser session produces a different device_id And no errors are logged at error level; only an INFO event indicates degraded mode
Consent Respect and Revocation Handling
Given the consent state for fingerprinting is Not Granted When the user opens the booking link Then no persistence token is created and only a session‑scoped device_id is used And no raw device attributes are stored; only transient in‑memory computation occurs Given the consent state changes to Granted during the session When the next page view or significant interaction occurs Then a persistent device_id is generated and used for subsequent events Given consent is later Revoked When revocation is received Then any persistence token is deleted within 5 minutes and future events revert to session‑scoped device_ids And consent changes are captured in the audit log with timestamps
Event-to-Device and Session Association
Given a device_id is present (persistent or session‑scoped) When the user generates events (open, join, blocked, step‑up) Then each event is recorded with device_id and session_id And all events within a session share the same session_id And events across sessions share the same device_id until rotation occurs And querying a booking by device_id returns the ordered event timeline And the 95th percentile latency to fetch events by device_id for a single booking is <= 300 ms
Re-association After Step-up Verification
Given a user is prompted for step‑up verification due to risk When the user successfully completes step‑up Then subsequent events from the current browser are associated to the existing booking's canonical device_id And events produced during the verification flow are linked to that same device_id And if the device_id changed during verification due to storage reset, the system merges pre‑ and post‑verification events under a single canonical device_id within 60 seconds And an audit entry is recorded with reason = step_up_success and includes the prior and new device_id values
Consistent device_id Exposure to UI and Rules Engine
Given the UI or rules engine requests device information for a booking When events are returned or rules are evaluated Then each event includes a device_id matching the regex ^[a-f0-9]{64}$ And the device_id remains stable within its rotation window And APIs and exports expose only derived attributes (e.g., IP city) and never raw sensitive attributes (e.g., full IP, full user agent) And rules can reference device_id and derived risk signals without access to raw attribute values
Anomaly Detection & Scoring Rules
"As an operations manager, I want suspicious patterns automatically scored and explained so that I can act confidently without manually parsing raw logs."
Description

Evaluates incoming events in real time against configurable rules to compute an anomaly score and reason codes per booking. Detects patterns such as many distinct devices on one link, rapid IP-city changes, high open frequency, device churn after a failed step-up, and mismatches with historical behavior. Provides organization-level thresholds and safelists/blocklists, emits structured findings for the UI, and supports simulation mode for tuning. Designed for low false positives with explainable outputs that cite the exact rule triggers.

Acceptance Criteria
Distinct Devices per Link Detection
Given org rule many_devices with window=6h, threshold=3, weight=40 and a booking link A When the 3rd distinct device_fingerprint opens link A within the 6h window Then increment booking A anomaly_score by 40, add reason_code "many_devices" with evidence {count:3, window_hours:6, device_fingerprints:[...]}, and emit a structured finding within 500ms Given subsequent opens by additional distinct devices within the same window for link A When the rule is configured as one_shot=true (default) Then the rule does not trigger again for booking A during that window Given multiple opens from the same device_fingerprint When they occur within the window Then they do not increase the distinct count and do not trigger this rule Given the device_fingerprint differs only by minor browser/OS version When normalization is applied Then the opens are treated as the same device for counting
Rapid Geo‑IP City Switching
Given org rule rapid_geo_switch with window=15m, distinct_city_threshold=2, weight=30 When booking link A is opened from 2 or more distinct IP-derived cities within 15 minutes Then increment anomaly_score by 30, add reason_code "rapid_geo_switch" with evidence {cities:[...], ip_hashes:[...], window_minutes:15}, and emit a structured finding Given both opens resolve to the same metro area (geo_distance<=50km) or same ASN When evaluating distinct cities Then do not trigger this rule Given geo lookup confidence for an open is low (confidence<0.6) When computing distinct cities Then exclude that open from the distinct city count
Post-Step-Up Device Churn
Given org rule device_churn_after_stepup with window=30m, threshold=2, weight=35 and booking A with a failed step-up challenge at t0 When 2 or more distinct device_fingerprints open link A within 30 minutes after t0 Then increment anomaly_score by 35, add reason_code "device_churn_after_stepup" with evidence {since:t0, distinct_devices:2, window_minutes:30}, and emit a structured finding Given booking A has no failed step-up event When multiple devices open link A Then this rule does not evaluate or trigger
Historical Behavior Mismatch Scoring
Given user U has >=3 prior bookings in the last 12 months and baseline metrics are computed (devices_per_booking, cities_per_booking, open_rate) When current booking A deviates from any baseline metric with z_score>=2 under org rule history_mismatch with weight=25 Then increment anomaly_score by 25, add reason_code "history_mismatch" with evidence {metric:..., baseline:..., observed:..., z_score:...}, and emit a structured finding Given user U has <3 prior bookings When evaluating this rule Then do not trigger and record suppression reason "insufficient_history" in the finding
Safelist/Blocklist and Org Threshold Actions
Given a device_fingerprint, IP, email, or booking on the org safelist When evaluating rules for booking A Then exclude safelisted entities from negative scoring and do not trigger enforcement actions due to those entities Given an entity on the org blocklist interacts with booking A When any event is received Then set anomaly_score to org.max_score, add reason_code "blocklisted", emit action "block", and prevent joins Given org thresholds {caution:50, block:80} When booking A anomaly_score crosses 50 for the first time Then emit action "step_up" and mark booking A as requiring step-up on next join attempt Given booking A anomaly_score crosses 80 When evaluating thresholds Then emit action "block" and prevent further joins for booking A Given a safelisted entity conflicts with an active block action When evaluating precedence Then safelist overrides the block and clears the enforcement for that booking
Simulation Mode for Rule Tuning
Given organization O has simulation_mode=true When rules evaluate for booking A Then produce findings with simulated=true and simulated_anomaly_score, but do not change the live anomaly_score and do not emit enforcement actions Given simulation_mode=true When retrieving findings via UI or API Then both live and simulated findings are available and clearly labeled Given simulation_mode is toggled off When subsequent events arrive Then only live findings are produced and enforcement actions apply per thresholds
Explainable Findings Emission
Given any rule triggers for booking A When emitting a finding Then include fields {rule_id, reason_code, weight, delta_score, score_after, booking_id, org_id, rule_version, timestamp, evidence:{...}} and make it available to UI/API within 500ms of the triggering event Given multiple rules trigger on the same event When computing and emitting results Then emit one finding per rule and update the cumulative anomaly_score deterministically using configured rule order/weights Given a rule is suppressed by safelist When emitting findings Then emit a suppressed finding with suppressed=true and reason "safelist" without changing the live anomaly_score
Auto Step-up Verification & Booking-Level Enforcement
"As an admin, I want the system to require extra verification on risky bookings so that potential link sharing is stopped without blocking legitimate attendees."
Description

Automatically enables stricter checks for a specific booking when anomaly thresholds are exceeded, including SMS/email OTP, single-verified-device enforcement, temporary joins lock, and device allow/deny lists. Integrates with existing ClassTap SMS/email infrastructure, supports fallback from SMS to email, and provides time-bounded policies that auto-expire. Includes admin overrides with full audit logging and ensures enforcement does not disrupt payments or legitimate re-joins once verification succeeds.

Acceptance Criteria
Auto step-up policy applied on anomaly threshold breach (booking-scoped)
Given a booking with anomaly detection configured to trigger when ≥3 distinct device fingerprints or ≥2 distinct IP cities occur within 10 minutes When the anomaly threshold condition is met Then the system creates a booking-scoped enforcement policy with actions: require OTP, enable single-verified-device, and lock joins until verification completes And the policy applies only to the affected booking and not to other bookings or the customer account And an audit log entry is written with booking_id, trigger_reason, counts, and triggered_at timestamp And policy activation occurs within 2 seconds of the triggering event
OTP step-up with SMS primary and email fallback
Given a user with both phone and email on the booking and an active step-up policy When a join is attempted Then the system sends a 6-digit OTP via SMS using ClassTap messaging infrastructure and templates And if SMS delivery fails or no delivery receipt is recorded within 60 seconds, an email OTP is sent to the booking email using ClassTap email infrastructure And OTPs expire in 5 minutes and verification attempts are limited to 5 per hour per booking And successful OTP verification marks the current device fingerprint as verified for this booking and clears the temporary join lock And all message send and delivery events are logged to the booking audit trail
Single-verified-device enforcement for the booking window
Given a booking with step-up policy active and a device has successfully verified via OTP When subsequent join attempts occur within the policy window of 4 hours Then only the verified device fingerprint may join without re-verification And join attempts from other device fingerprints are blocked with reason single_device_enforced and offered the option to verify And the verified device may leave and rejoin unlimited times without further prompts within the window And IP or network changes on the verified device do not block joins
Temporary joins lock until first successful verification
Given a booking where step-up policy has just been activated When any attendee attempts to join before any device has verified Then the join is blocked with status verification_required and a prompt to complete OTP And upon the first successful OTP verification for the booking, the lock is lifted for the verified device immediately And if no verification occurs, the lock auto-expires after 15 minutes or upon policy expiry, whichever comes first And payment checkout, refunds, and waitlist actions remain fully available during the lock
Booking-level device allow/deny lists with instant effect and audit
Given an admin with permission manages a booking under step-up enforcement When the admin adds a device fingerprint to the booking allow list Then that device may join without OTP for the duration of the policy and the event is logged with admin_id, fingerprint, reason, and timestamp When the admin adds a device fingerprint to the booking deny list Then join attempts from that device are blocked even with a valid OTP and the event is logged with admin_id, fingerprint, reason, and timestamp And changes take effect within 1 second and are reversible with full audit history
Time-bounded policy auto-expiry and cleanup
Given a booking step-up policy with a configured TTL of 24 hours When the TTL elapses and no further anomalies are detected Then the policy automatically expires and all associated locks and single-device restrictions are removed And verified device entries for the booking are invalidated And an audit entry policy_expired is recorded with expired_at timestamp And subsequent joins proceed without step-up unless a new anomaly triggers a new policy
Admin override with full audit and no disruption to payments or legitimate re-joins
Given a booking with an active step-up policy When an admin overrides the policy to disabled via the console Then the change takes effect immediately and is logged with admin_id, previous_state, new_state, reason, and timestamp And users who have already verified can rejoin without interruption And users who are mid-checkout, payment confirmation, or refund flows are not blocked or errored by the override When the admin re-enables the policy Then subsequent joins again require step-up according to configuration and all actions are logged
Admin Audit Trail & Remediation Console
"As a support agent, I want a clear timeline and one-click remediation tools so that I can resolve access issues during class check-in without escalating."
Description

Delivers a live, filterable timeline per booking that shows link opens with device ID, IP city, and outcomes (joined, blocked, step-up), highlighting anomalies and their reason codes. Offers quick actions—block device, lift restrictions, regenerate link, mark as legitimate—to resolve issues in seconds. Supports streaming updates, role-based access, PII minimization (mask IPs, show city only), and export to CSV for dispute resolution. Optimized to load the most recent 500 events under one second and maintain real-time updates via SSE/WebSocket.

Acceptance Criteria
Load and Display Live Audit Trail (Last 500 Events)
Given a booking with 500 or more audit events When an authorized admin opens the Audit Trail & Remediation Console for that booking Then the most recent 500 events render in the timeline within 1.0 seconds at or below P95 on a cold load And each event row displays timestamp (UTC ISO-8601), device ID (hashed), IP city, and outcome (joined, blocked, step-up) And events flagged as anomalous are visually highlighted and include a machine-readable reason code And if auto-strict checks are active for the booking, a persistent banner shows the current enforcement state with a link to lift restrictions And older events are available via on-demand pagination without blocking initial render
Filter and Search Timeline
Given at least 200 events are loaded in the timeline When the admin applies any combination of filters (outcome, device ID, IP city, time range) or enters a device-ID substring search Then the timeline updates to reflect the filter within 300 ms at or below P95 without a full page reload And the active filters are shown as removable chips And clearing all filters restores the full 500-event view
Real-Time Streaming Updates
Given the console is open and connected via SSE/WebSocket When a new audit event for the booking is produced by the backend Then the event appears at the top of the timeline within 2 seconds end-to-end in at least 99% of cases And the stream auto-reconnects on network drop within 30 seconds with exponential backoff And duplicate events (by event ID) are not rendered more than once
Quick Actions: Block/Lift/Regenerate/Mark Legitimate
Given an authorized admin is viewing the console When the admin selects "Block device" on an event with a device ID Then subsequent link opens from that device for the booking are blocked and a new audit event "device_blocked" is appended with actor, timestamp, and reason And the UI reflects the block state within 2 seconds When the admin selects "Lift restrictions" for a previously blocked device Then future link opens from that device are allowed per normal policy and an audit event "restriction_lifted" is appended When the admin selects "Regenerate link" for the booking Then the previous access link is immediately invalidated, a new link token is generated, and an audit event "link_regenerated" is appended And the timeline records any attempts to use the invalidated link as "blocked (link_invalidated)" When the admin selects "Mark as legitimate" on an anomalous event Then the anomaly flag is cleared for that event and auto-strict checks are disabled for this booking going forward And an audit event "marked_legitimate" with actor and reason is appended
Role-Based Access and Permissions
Given a signed-in user with role "Owner" or "Admin" When they navigate to a booking's Audit Trail & Remediation Console Then they can view the timeline and perform all quick actions and exports Given a signed-in user with role "Instructor" or "Viewer" When they navigate to the console Then they can view the timeline but cannot perform quick actions or regenerate links, and export is disabled And action buttons are disabled or hidden and unauthorized attempts return HTTP 403 Given an unauthenticated user or a user outside the booking's organization When they attempt to access the console route Then access is denied with HTTP 401/403 and no timeline data is leaked
PII Minimization and Masking
Given any event displayed in the console Then the raw IP address is never rendered; only the geolocated city is shown And device identifiers are displayed as stable, non-reversible hashes And copying or export features do not expose raw IPs or other PII beyond city and masked IP (e.g., 203.0.113.xxx) Given an API consumer fetches data for the console Then responses exclude raw IP fields by default and include city and masked IP only
CSV Export for Dispute Resolution
Given an authorized Owner/Admin is viewing a filtered timeline When they click "Export CSV" Then a CSV is generated containing only the events that match the current filters, ordered by timestamp descending And the CSV includes columns: timestamp_utc, outcome, device_id_hashed, ip_city, anomaly_flag, reason_code, actor, action_type And the file is RFC 4180 compliant, UTF-8 encoded with header row, and downloads within 5 seconds for up to 10,000 events And no raw IP addresses are included; if present, IPs are masked And the export action is logged as an audit event with actor and timestamp
Privacy, Consent, and Data Retention Controls
"As a studio owner, I want privacy-friendly tracking with clear consent and retention controls so that I can use Abuse Insights confidently and compliantly."
Description

Provides organization-configurable consent messaging for tracking on booking pages, data minimization (city-level geolocation, hashed device IDs), encryption, and retention windows (e.g., 30–180 days) with automatic purge jobs. Honors Do Not Track where applicable, restricts access via roles and audit logs, and offers per-customer data export/deletion. Supplies policy text snippets and admin documentation to align with GDPR/CCPA requirements without blocking core Abuse Insights functionality.

Acceptance Criteria
Configurable Consent Messaging on Booking Pages
Given an org admin configures consent messaging and tracking categories in Privacy Settings When a first-time visitor loads any booking page Then a consent banner is displayed within 200ms of DOM ready with org-provided text and links to policy snippet Given the visitor has not consented When the page executes Then non-essential tracking for Abuse Insights is disabled and no client-side identifiers are stored except a strictly necessary session nonce Given the visitor provides consent for selected categories When they accept Then the selected categories are persisted with timestamp, consent version, language, and booking page ID Given consent is withdrawn via the banner link When the user opts out Then non-essential tracking is disabled immediately and client-side identifiers for those categories are deleted within 5 seconds Given consent state exists on the device When the same device visits another booking page of the same org Then the prior consent is respected and the banner is not shown unless the consent version changed Given consent is declined When Abuse Insights evaluates an event Then it continues functioning using minimized server-side data only and surfaces anomalies without blocking the booking flow Given an admin views Privacy Settings When they open Policy Snippet Preview Then localized GDPR/CCPA text renders with org name, contact, and retention window placeholders populated
Honor Browser Do Not Track (DNT)
Given a visitor's browser sends DNT:1 When any booking page loads Then non-essential tracking is disabled by default without prompting for consent Given DNT:1 When Abuse Insights records a security event Then only essential fields (timestamp, booking ID, city-level geolocation, hashed device ID) are stored and no client-side cookies are set Given DNT:1 When the visitor navigates across booking pages Then the consent interface displays “respects Do Not Track” copy and any override is off by default Given an admin exports audit events filtered to DNT traffic When records are reviewed Then each record includes DNT=true and contains no granular location or raw IP data Given DNT:1 When an anomaly is detected Then stricter checks may auto-enable for that booking without blocking the booking or exposing disallowed personal data
Data Minimization for Abuse Insights Signals
Given geolocation is derived from IP When the system stores location Then only city and country are persisted; no latitude/longitude or street-level data are stored Given device fingerprinting is enabled When a device identifier is computed Then the identifier is a salted, non-reversible hash that excludes names, emails, and exact IPs Given hashing salt rotation policy When the salt rotates Then prior hashes remain resolvable via versioned keys without revealing original identifiers Given event schemas for Abuse Insights When fields are persisted Then PII fields are limited to the minimum set required for outcomes and are listed in the data inventory Given schema governance is enforced When a developer attempts to add a new event field Then CI validation fails unless the field is tagged as essential with documented justification and approval
Retention Windows and Automatic Purge
Given an org sets a retention window between 30 and 180 days When the setting is saved Then the value is validated and stored per org with a default of 90 days if unset Given the nightly purge job runs at 02:00 UTC When records exceed the org's retention Then those records are hard-deleted or irreversibly anonymized and a purge audit entry is created with counts by type Given an org reduces retention from a higher value to a lower value When the setting is saved Then data older than the new threshold is queued for purge and removed within 24 hours Given purge completes When an admin views the Purge Report Then it shows timestamp, org, number purged, duration, and any failures with retry status Given retention boundaries exist When an admin attempts to set a value outside 30–180 days Then the system rejects the change with a validation error and retains the prior value
Role-Based Access Control and Audit Logging
Given org roles of Owner, Security Admin, Admin, Instructor, and Support When accessing Abuse Insights raw event data Then only Owner and Security Admin can view event details; others see aggregated counts without PII-reduced details Given a user with access views or exports Abuse Insights data When the action occurs Then an immutable audit log entry captures user ID, role, action, filters, timestamp, and IP city Given an unauthorized role attempts access When the request is made Then the system returns 403 and logs the attempt without exposing data Given audit logs exist When an Owner filters by user and date range up to 50k entries Then matching entries return within 2 seconds Given audit integrity requirements When logs are stored Then they are write-once or hash-chained and integrity verification passes during periodic checks
Per-Customer Data Export and Deletion (GDPR/CCPA)
Given an admin enters a customer's email or booking reference When requesting an export Then a downloadable package in machine-readable JSON/CSV is produced within 24 hours including Abuse Insights data for that subject Given an admin submits a deletion request with dual approval When confirmed Then all personal data for that subject is deleted or anonymized across primary stores within 7 days and from backups within 30 days, and a deletion certificate is generated Given the subject has active or past bookings When deletion is processed Then bookings are preserved for accounting but personal fields are replaced with pseudonymous placeholders and Abuse Insights links referencing the subject are removed Given an export or deletion completes When the action finishes Then a corresponding audit log entry is created and visible to Owner and Security Admin Given a subject is deleted When Abuse Insights processes new events for other users Then the system operates normally without errors or references to the deleted subject
Encryption In Transit and At Rest
Given data in transit When booking pages or APIs are accessed Then TLS 1.2+ with modern ciphers is enforced and HTTP is redirected to HTTPS Given sensitive fields (emails, device hashes, IP-derived city) When stored at rest Then they are encrypted using AES-256 or stronger with keys managed in a KMS and access restricted by role Given key rotation policy When a rotation occurs Then decryption continues without downtime and old keys are retired within 24 hours Given backups are created When backup files are written Then they are encrypted at rest and subject to the same retention and purge policies Given lower environments When plaintext access to sensitive fields is attempted Then automated checks block the change and alert security owners

Guest Convert

When a second device attempts access, show a friendly paywall to purchase a guest seat or request a transfer from the booking owner. Monetizes unauthorized sharing without derailing genuine attendees, keeping rosters accurate and revenue intact.

Requirements

Multi-Device Access Detection & Session Linking
"As a booked attendee, I want legitimate device switches to be recognized without blocking me so that I can join class if I swap devices while preventing unauthorized sharing."
Description

Detect and attribute simultaneous or sequential access attempts for the same booking from multiple devices, linking them to a single booking ID while distinguishing legitimate device switches from unauthorized sharing. Implement lightweight device fingerprinting, token-based session tracking, and time/IP heuristics that are privacy-compliant and configurable. Provide an enforcement API that flags second-device events and returns a decision (allow, warn, interstitial) with rationale. Ensure resilience across web and mobile clients, handle intermittent connectivity, and support instructor and kiosk exemptions. Integrate with existing authentication and class attendance systems without adding noticeable latency.

Acceptance Criteria
Graceful Device Switch (Same User, Short Interval)
Given a booking owner with bookingId={id} is active on DeviceA with sessionToken=tokenX and fingerprint=f1 And gracePeriodMinutes=10, overlapSeconds=60, idleTimeoutMinutes=2 are configured defaults When the same authenticated user opens the same booking on DeviceB within gracePeriodMinutes And DeviceB shares the same userId and either the same IP ASN or geofence distance <= 10km from DeviceA Then the Enforcement API returns decision="allow" and reasonCode="graceful_switch" with linkedSessionId referencing DeviceA's session And no interstitial is shown on DeviceB and DeviceA is soft-logged-out after overlapSeconds if both remain active And the attendance system maintains a single attendee record for the booking (no duplicates)
Second Device Detected: Interstitial Paywall Trigger
Given DeviceA is actively accessing bookingId={id} with publicIP=IP1 and fingerprint=f1 at time=t0 When DeviceB with fingerprint=f2 and publicIP=IP2 (IP2 != IP1 ASN or geofence > 10km) attempts access within the same session window Then the Enforcement API returns decision="interstitial" and reasonCode="second_device_detected" with payload including {bookingId, deviceIdB, ttlSeconds >= 120, correlationId} And DeviceA remains allowed with no throttling or disruption And the client renders the Guest Convert paywall within 300ms of receiving the decision And upon successful guest seat purchase or approved transfer callback, the next check for DeviceB within 3 seconds returns decision="allow" And the attendance system records DeviceB as a guest/transfer attendee linked to the original booking (no duplicate owner)
Cross-Platform Session Linking & Roster Consistency
Given the same authenticated user accesses bookingId={id} on Web (DeviceA) and later on Mobile (DeviceB) When DeviceB obtains a new sessionToken via refresh and presents device signals (UA, timezone, screen size) Then DeviceA and DeviceB are linked to a single booking session chain with linkConfidence >= 0.8 And test harness over >= 10,000 simulated events shows session linking accuracy >= 99.5% and duplicate attendance entries <= 0.1% And the audit trail includes {priorDeviceId, newDeviceId, linkConfidence, timestamp} for each link action And the link persists across app restarts and token rotations for up to 24 hours (configurable)
Enforcement API Performance & Latency Budget
Given the Enforcement API is invoked from web and mobile clients at 200 RPS (70% allow, 25% interstitial, 5% warn) under cold cache When requests are made with standard payload size <= 2KB over TLS 1.2+ Then p95 response time <= 150ms and p99 <= 300ms with error rate < 0.1% And end-to-end class join flow adds <= 50ms p95 compared to baseline (measured by client timing) And each response includes non-empty fields: {decision in [allow,warn,interstitial], reasonCode, rationale, correlationId} And API is available 99.9% over a rolling 30-day window, with retries idempotent via checkId
Instructor & Kiosk Exemptions
Given a user with role=instructor for classId={id} or a device marked kiosk=true for the venue When multiple devices attempt to access the same booking concurrently Then the Enforcement API returns decision="allow" with reasonCode in ["instructor_exempt","kiosk_exempt"] And no interstitial or warn prompts are shown on exempt devices And events are logged with exempt=true and excluded from abuse-rate metrics and automated enforcement And allowlists (userIds, deviceIds, CIDR ranges) are configurable and changes propagate to enforcement nodes within 60 seconds
Intermittent Connectivity: Warn and Escalate
Given DeviceA (owner) loses connectivity for <= 3 minutes and reconnects with a rotated token and clock skew within ±60s And DeviceB attempts access to the same booking during DeviceA's outage from a different IP and fingerprint When the sharing confidence score < 0.8 (configurable threshold) Then the Enforcement API returns decision="warn" with reasonCode="uncertain_second_device" and a verification challenge is triggered And if the challenge is not completed within 120 seconds, subsequent checks escalate to decision="interstitial" And no duplicate roster entries are created during the warn window, and all checks remain idempotent via correlationId/checkId
Privacy Compliance & Configurable Heuristics
Given lightweight fingerprinting is enabled (UA, timezone, screen metrics) and PII collection is disabled When a second-device check is executed for bookingId={id} Then captured data contains no PII, is retained <= 30 days by default, and honors tenant-level opt-out toggles And IP/time heuristics thresholds (gracePeriod, distanceKm, ASN match) are configurable per tenant with audited change logs And responses and logs redact IPs to /24 for IPv4 and /48 for IPv6, and user identifiers are tokenized And automated tests verify absence of PII fields and correct application of tenant overrides
Paywall Interstitial with Dual Options
"As a guest attempting access, I want a clear choice to purchase a seat or request the booking owner’s transfer so that I can join without disrupting the class."
Description

Present a branded, accessible interstitial when a second device is flagged, offering two clear paths: purchase a guest seat or request a transfer from the booking owner. Display class details, seat availability, pricing, policies, and countdowns for time-sensitive actions. Support localization, white-label theming, and A/B-tested copy. Maintain user context (class link, device, and state) during navigation, handle deep links from emails/SMS, and provide a graceful fallback if the user signs in as the booking owner. Track impressions, selections, and drop-off for analytics while meeting performance and accessibility standards.

Acceptance Criteria
Second Device Detection Shows Interstitial With Dual Options
Given an active booking where device A has accessed the class link And device B is flagged as a second device for the same booking When device B opens the class link Then a paywall interstitial is shown blocking access And the interstitial displays class title, date/time, delivery mode (in-person or virtual), remaining seats, price (in local currency), and cancellation/refund policy And primary actions "Purchase Guest Seat" and "Request Transfer" are visible and enabled according to seat availability And a countdown is displayed for time-sensitive actions, visible and updating in real time
Guest Seat Purchase Flow and Seat Hold
Given seats are available for the class And device B is on the paywall interstitial When the user selects "Purchase Guest Seat" Then a seat is reserved for the configured hold duration and a countdown begins And the user is routed to the hosted checkout with class_id, device_hash, ab_variant, locale, and return_url preserved When payment succeeds through the configured provider Then access is granted on device B and the interstitial is dismissed And the class roster is updated to include the guest without exceeding capacity or creating duplicates And analytics events are emitted: interstitial_impression, select_purchase, checkout_start, purchase_success with timestamps and metadata (class_id, booking_id, device_hash, ab_variant, locale, seat_availability) When the countdown expires before payment completion Then the seat hold is released, analytics logs purchase_timeout, and the CTA reflects updated availability or offers waitlist if full
Transfer Request Flow and Owner Approval Deep Link
Given device B is on the paywall interstitial When the user selects "Request Transfer" Then a transfer request is sent to the booking owner via email and SMS when available, including a secure deep link to approve or decline And the requester sees an on-screen confirmation and status badge "Transfer pending" When the owner opens the deep link and approves within the configured window Then device B gains access, device A loses access, and both parties receive confirmation And audit log records transfer_approved with timestamps and actor ids When the owner declines or the window expires Then device B remains blocked and the interstitial presents the purchase option (or waitlist if full), and analytics logs transfer_declined or transfer_expired
Graceful Fallback When Booking Owner Signs In
Given the paywall interstitial is displayed on device B When the user authenticates and is verified as the booking owner of the booking Then the interstitial is dismissed and class access is granted without requiring purchase or transfer And analytics logs owner_sign_in_bypass And no additional roster entry is created and device B is marked as an authorized device for the session
Localization, Theming, and A/B Copy Consistency
Given a user locale, currency, and studio white-label theme are determined When the interstitial renders Then all text, numbers, currency, dates/times, and policy links are localized to the user locale with fallback to the studio default when a translation is missing And prices are shown in the correct currency with appropriate symbol and formatting And the studio's logo, colors, and fonts are applied without obscuring text or failing contrast requirements And an A/B copy variant is assigned deterministically per user+class and remains consistent across interstitial, checkout, and return flow And analytics events include locale, currency, and ab_variant for segmentation
Deep Link Handling and State Preservation
Given a user arrives via an email or SMS deep link containing class_id, booking_id, device_hash, and return_url When the interstitial loads and the user navigates to purchase or transfer and returns Then class context (class_id, booking_id, device_hash), ab_variant, locale, and countdown remaining time persist across navigation and browser refresh within the session And the return_url lands the user in the correct post-action state (access granted on success; interstitial with updated status on failure) And UTM and referrer parameters are preserved for analytics attribution
Performance and Accessibility Standards
When the interstitial is requested on a mid-tier mobile device over simulated 4G Then time-to-interactive is <= 1.5 seconds, total additional transfer size is <= 250 KB gzipped (excluding fonts), and Cumulative Layout Shift is <= 0.1 And all actionable elements are reachable and operable via keyboard and assistive technologies, have visible focus states, ARIA roles/labels, and meet 44x44 px minimum target size And screen readers announce headings, class details, button labels, errors, and countdown updates appropriately And color contrast meets WCAG 2.1 AA and there is no information conveyed by color alone
Instant Guest Seat Checkout
"As a last-minute guest, I want to complete payment in a few taps so that I can secure a seat and join immediately."
Description

Enable a frictionless checkout for guest seats initiated from the interstitial, including real-time seat availability checks, price/tax calculation, discount rules, and fee transparency. Support Apple Pay, Google Pay, and saved cards via existing payment gateways with PCI-compliant tokenization. Ensure transactional integrity to prevent overselling and automatically assign the purchased seat to the accessing device/user upon success. Provide immediate confirmation, receipt issuance, and roster updates, with robust error handling, retries, and refund/cancellation policy adherence for failures or reversals.

Acceptance Criteria
Interstitial-Initiated Guest Seat Purchase with Real-Time Availability
Given an accessing second device is intercepted by the Guest Convert interstitial, When the user selects "Buy Guest Seat", Then the system checks live inventory for the target class before rendering checkout. Given the inventory check occurs, When a seat is available, Then the checkout reserves 1 guest seat for the configured hold duration tied to the session. Given no seat is available at check, When the user attempts to proceed, Then the system blocks checkout and offers join-waitlist or request-transfer options with clear messaging. Given inventory becomes unavailable during checkout, When the user authorizes payment, Then the transaction is declined with a clear "Sold out" message and no charge is captured or authorized.
Accurate Pricing, Taxes, Discounts, and Fee Transparency
Given a class with base price, applicable taxes by location, and configurable fees, When checkout loads, Then the price breakdown displays itemized base, discounts, taxes, fees, and a total that matches the gateway charge amount. Given a valid discount (promo code, membership, or bundle rule) is applicable, When applied, Then the discount is validated in real time and reflected in the total; ineligible codes return a specific reason. Given tax rules depend on attendance mode and jurisdiction, When the user's taxable address and class location are known, Then tax is calculated per rule and rounded per currency rules from the gateway. Given fee transparency requirements, When totals are shown, Then there are no hidden fees and line items sum exactly to the total payable.
Express Payments via Apple Pay, Google Pay, and Saved Cards
Given device/browser support and verified merchant domains, When checkout renders, Then Apple Pay or Google Pay buttons display and initiate payment sheets successfully. Given the user has a saved card token, When selected, Then the charge uses tokenized details without exposing PAN to ClassTap. Given the user completes authorization via Apple Pay or Google Pay, When the payment is confirmed, Then the charge is captured or authorized per merchant setting without additional form input. Given an unsupported device or unverified domain, When checkout renders, Then express pay options are hidden and a standard card form is available.
Transactional Integrity and Oversell Prevention Under Concurrency
Given multiple second devices attempt to purchase the last available guest seat, When authorizing payments concurrently, Then at most one transaction succeeds and all others fail gracefully with no capture. Given a payment intent is created, When the same idempotency key is reused within 24 hours due to retries or duplicate submits, Then duplicate charges are prevented and a consistent success response is returned. Given a client timeout or retry occurs, When the server receives duplicate commit requests, Then the booking state machine enforces exactly-once seat assignment and releases any redundant holds. Given the gateway reports a late failure after prior authorization, When reconciliation runs, Then the seat hold is released and the roster remains accurate with no ghost assignments.
Seat Assignment, Confirmation, Receipt, and Roster Update on Success
Given payment is successfully captured/authorized, When the gateway returns success, Then the guest seat is assigned to the accessing device/user and linked to the correct class occurrence. Given successful assignment, When completion occurs, Then an on-screen confirmation displays within 5 seconds, an email/SMS receipt is issued, and the class roster updates for staff and the booking owner immediately. Given the booking owner views their booking, When the guest seat is purchased, Then booking details reflect the added guest with editable attendee info and transfer options per policy.
Error Handling, Retries, and Policy-Compliant Reversals
Given a recoverable error (network failure or 5xx) occurs during payment confirmation, When retry policy triggers, Then the server retries up to the configured limit with exponential backoff without double-charging. Given payment is authorized but capture fails or is reversed within the grace window, When reversal completes, Then the guest seat hold is released, the roster is reverted, and the user is notified with refund timeline per policy. Given a hard decline or 3DS authentication failure, When checkout fails, Then the user sees a clear error, no seat is assigned, and alternative payment options remain available. Given a dependent service outage is detected, When health checks fail, Then checkout is disabled with a status message and no payment attempts are initiated.
PCI Tokenization and Data Security Compliance
Given card entry is required, When the user inputs card details, Then all entry and transmission occur via PCI-compliant gateway SDKs with tokenization and no PAN, CVV, or sensitive data traverse or persist on ClassTap servers. Given logs and analytics events are emitted, When recording payment events, Then no sensitive card data or prohibited PII is logged and data is masked per policy. Given scheduled compliance reviews, When audits run, Then evidence of appropriate PCI scope (e.g., SAQ A) and signed agreements is available and up to date.
Owner Transfer Request & Approval Flow
"As a booking owner, I want to quickly approve transferring my seat to someone else so that the right person can attend without friction."
Description

Implement a secure, time-bound workflow allowing the accessing device to request a seat transfer from the booking owner. Send actionable notifications via SMS, email, or push with deep links to a one-tap approve/deny screen showing class details and policy implications. Enforce studio-configured limits (e.g., transfer cutoffs, number of transfers, fee requirements) and maintain an auditable trail of requests and outcomes. On approval, reassign the seat, update roster and access tokens in real time, and notify all parties; on denial, direct the requester to purchase a guest seat. Guard against spam with rate limits and verification.

Acceptance Criteria
Transfer Request from Secondary Device Paywall
Given a non-owner device is blocked by the Guest Convert paywall for a valid booking And the studio configuration permits transfer requests When the device selects "Request Transfer" Then the system collects the requester's contact identifier (email or phone) and verifies format And creates a TransferRequest record with status "Pending" linked to the booking, requester device fingerprint, and timestamp And sends an actionable notification to the booking owner via all enabled channels (SMS/email/push) containing a signed, single-use deep link And displays to the requester a confirmation with the owner's masked contact and expected response window
Owner Approves/Deny via Time-Bound Deep Link
Given the booking owner opens the deep link from an allowed channel When the link is opened within its validity period and prior to the studio's transfer cutoff Then an approve/deny screen is shown with class name, date/time, location, attendee names (current and requester), and policy/fee summary And "Approve" and "Deny" actions are available as single-tap actions And if the link is expired, cutoff passed, or request already resolved, the screen shows a non-actionable final state message with next steps
Policy Limits and Transfer Fee Enforcement
Given the owner taps "Approve" When studio rules require limits and/or fees Then the system validates the transfer count against the studio-configured maximum for the booking And validates the time cutoff relative to class start And calculates the applicable transfer fee (including taxes/discounts) and presents a concise confirmation And requires successful payment authorization before proceeding And if any validation fails or payment is declined, the approval is not executed, the request is marked "Denied" with a specific reason code, and both parties are notified
Real-Time Seat Reassignment and Access Token Update
Given an approval is executed When the system completes reassignment Then the booking seat is reassigned to the requester and the previous owner's seat access is revoked And new access tokens/QR codes are generated for the requester and propagated to edge services And the class roster in the instructor/studio dashboard reflects the new attendee within 5 seconds And both owner and requester receive confirmation notifications including updated access artifacts And the TransferRequest record is marked "Approved" with payment receipt metadata (if any)
Denial Path Directs Requester to Guest Seat Purchase
Given the owner taps "Deny" or an approval cannot proceed due to policy or expiry When the requester next attempts access via the paywall Then the paywall presents the Guest Seat purchase option prefilled with the class/session details and price And the transfer request status is shown as "Denied" with a human-readable reason And a CTA to "Buy Guest Seat" initiates the standard checkout flow in 2 taps or fewer
Spam Protection: Rate Limits and Requester Verification
Given repeated transfer requests for the same booking or from the same device/IP When thresholds are exceeded within a rolling time window Then additional requests are blocked with a friendly error and a communicated cooldown duration And first-time requesters must verify ownership of their contact (OTP via SMS or email) before a request is sent And per-booking daily cap, per-device hourly cap, and per-owner total pending cap are enforced as configurable limits
Audit Trail, Idempotency, and Concurrency Handling
Given any transfer request lifecycle event occurs When the system processes actions (request created, link opened, approved, denied, expired, payment) Then an immutable audit log entry is recorded with event type, actor, booking ID, request ID, channel, device fingerprint, IP (hashed per policy), and timestamps And processing is idempotent: repeated deep-link opens or duplicate webhook callbacks do not create multiple state transitions or payments And concurrent events resolve deterministically with priority order: cutoff/expiry > explicit deny > successful approve with payment And the requester and owner UIs reflect the terminal state within 5 seconds
Studio Policy Configuration & Branding Controls
"As a studio owner, I want to set pricing and rules for guest access so that monetization aligns with my policies."
Description

Provide admin controls for studios to enable/disable Guest Convert, set guest seat pricing models (class price, fixed surcharge, dynamic), define maximum guest seats per booking, set transfer allowances and cutoff windows, and choose default action precedence. Allow customization of interstitial copy, tone, and brand theming, with localization and preview. Support role-based access to settings, environment-specific defaults, and safe deployment via feature flags. Expose configuration via API for white-label partners and ensure changes propagate in near real time without downtime.

Acceptance Criteria
Guest Convert Toggle with Environment Overrides and Feature Flag
Given an Owner or Manager is editing Studio A settings in Production, When they toggle Guest Convert to Off and click Save, Then the API responds 200, an audit log entry is created, and the Production value is Off while Staging remains unchanged. Given the global GuestConvert feature flag is disabled, When an admin views the toggle, Then the control is disabled with an explanatory tooltip and Save is blocked. Given Guest Convert is enabled for the studio, When a second device triggers access for a booking, Then the Guest Convert paywall is shown; When disabled, Then the paywall is not shown and normal access rules apply.
Guest Seat Pricing Models Configuration
Given Pricing Model is set to Class Price, When a guest seat is offered, Then the guest price equals the current published class price at checkout time using the class currency and tax rules. Given Pricing Model is set to Fixed Surcharge and surcharge is a non-negative amount in the class currency, When a guest seat is offered, Then guest price = class price + surcharge, rounded to the currency minor unit, and the preview displays the computed price. Given Pricing Model is set to Dynamic with a configured rule (e.g., multiplier range 0.8–1.5 based on capacity and time-to-start), When preview inputs are provided (capacity=50%, TTS=2h), Then the preview computes and displays the resulting price; When the rule evaluates to an out-of-bounds value, Then the system clamps to min/max and logs the event. Given checkout occurs under Dynamic pricing, When the user confirms purchase, Then the evaluated guest price is snapshot, persisted on the order, and shown on the receipt. Given any pricing configuration is invalid (e.g., negative surcharge), When saving, Then the API returns 422 with field-level errors and no change is persisted.
Maximum Guest Seats per Booking Enforcement
Given Max Guest Seats Per Booking is set to N=2, When a booking owner attempts to add a 3rd guest seat across any devices/sessions, Then the API returns 409 GUEST_SEAT_LIMIT_EXCEEDED and the UI displays a clear message. Given Max Guest Seats Per Booking is set to 0, When a second device triggers access, Then the paywall does not offer guest purchase and only shows transfer options if allowed. Given guest seats are purchased in rapid succession by multiple devices, When the second purchase would exceed N, Then the system prevents oversell via atomic reservation and only up to N are confirmed. Given the class is full and waitlist is enabled, When a guest purchase is attempted, Then the user is offered to join the waitlist subject to the same N cap; When waitlist is disabled, Then the purchase path is blocked with an explanatory message.
Transfer Allowances and Cutoff Windows
Given Allow Transfer = On and Cutoff = 2 hours before start, When a second device triggers access 3 hours before start, Then the paywall shows Request Transfer and submitting creates a transfer request. Given the current time is within the cutoff (≤2 hours before start) or Allow Transfer = Off, When the paywall is shown, Then Request Transfer is not offered and a localized reason is displayed. Given Owner Approval = Required, When a transfer is requested, Then the booking owner receives a notification per studio channels and the request remains pending until approved or declined; When Owner Approval = Auto-Approve, Then the transfer is immediately applied and reflected on the roster.
Default Action Precedence Resolution
Given both Guest Purchase and Transfer are allowed, When Default Action Precedence is set to Guest Purchase, Then the paywall renders Guest Purchase as the primary CTA and Transfer as secondary across web and deep links; When set to Transfer, Then Transfer is primary and Guest Purchase is secondary. Given only one action is allowed by policy, When the paywall renders, Then only the allowed action is shown regardless of precedence setting. Given a change to precedence is saved, When a new paywall session starts, Then the new ordering is applied consistently in all locales and themes.
Branding, Tone, Theming, Localization, and Preview
Given an admin edits interstitial copy fields (headline, body, legal, CTA labels) within defined character limits, When saving, Then tokens like {class_name} and {start_time} validate and render correctly in preview. Given Tone is set to Friendly/Neutral/Firm, When applied, Then the corresponding template copy populates and can be further customized before save. Given a logo (PNG/SVG) and brand colors (hex) are set, When previewing, Then the paywall reflects the assets and passes WCAG AA contrast checks; invalid assets are rejected with 422. Given Locale is set to a supported language, When previewing, Then all copy, dates, times, and currency format according to locale; When locale is unsupported, Then the system falls back to the studio default and indicates the fallback in preview. Given an RTL locale is selected, When previewing, Then layout mirrors appropriately and CTAs render correctly. Given copy or theme changes are made, When previewing, Then updates appear within 1 second and match production rendering for the same configuration.
RBAC, API Exposure, Feature Flags, and Near Real-Time Propagation
Given role-based access is configured, When an Owner/Manager accesses settings, Then they have read/write; When Staff, Then read-only; When Instructor or unauthorized, Then access is denied with 403 and no data leakage. Given any configuration change is saved, When audited, Then an immutable log records actor, timestamp, before/after values, environment, and source (UI/API). Given a white-label partner with valid credentials calls GET /v1/studios/{id}/guest-convert-config, When the request is authorized, Then 200 returns the current configuration; When updating via PUT with valid payload, Then 200 returns the persisted configuration; invalid payloads return 422 with error codes. Given configuration is changed, When measuring propagation, Then new paywall sessions reflect the change within 5 seconds p95 without causing downtime or 5xx rate > 0.1%; active sessions keep previous config until refresh. Given feature flags are used for rollout, When a flag is toggled per environment or studio, Then behavior switches atomically for new sessions and safely falls back to defaults if the flag is Off.
Roster Synchronization, Notifications, and Analytics
"As an operator, I want rosters and reports to update automatically when guests convert so that attendance and revenue stay accurate."
Description

Automatically synchronize roster and attendance records upon guest purchase or transfer approval, preventing double-bookings and preserving capacity constraints. Send confirmations and updated calendar invites to guests, booking owners, and instructors, with receipts and check-in QR codes where applicable. Capture analytics including paywall impressions, conversion rates, transfer approval rates, incremental revenue, and drop-off points; provide exports and dashboards with cohort and class-level filtering. Support A/B testing of interstitial copy and pricing messages, and ensure GDPR/CCPA-compliant consent and data retention.

Acceptance Criteria
Roster Sync on Guest Seat Purchase
Given a class with remaining capacity and a booking owner’s invite link triggers a guest seat purchase When payment is confirmed as succeeded via webhook or checkout callback Then the guest is added to the class roster exactly once within 5 seconds (p95) and class capacity is decremented by 1 And the booking owner’s original seat remains unchanged And duplicate processing of the same payment event does not create additional roster entries (idempotent) And the system prevents double-booking by rejecting the purchase if the guest or owner already holds a seat in any overlapping timeslot for this class And a check-in QR code is generated for the guest if the venue uses QR check-in
Roster Update on Transfer Approval
Given a booking owner approves a seat transfer to a guest via the paywall flow When the transfer is approved and recorded Then the original seat is reassigned from the owner to the guest within 5 seconds (p95) without changing overall class capacity And any previously issued QR code for the owner’s seat is invalidated and a new QR code is issued to the guest And calendar invite ownership is updated to the guest and the owner’s invite is canceled And repeated approval callbacks do not create duplicate seats or notifications (idempotent)
Capacity Constraints and Concurrency Controls
Given there is only one seat remaining and multiple purchase or transfer attempts occur concurrently When the system processes these attempts Then exactly one attempt succeeds and all others receive a capacity-exhausted response without creating overbookings And the final roster count never exceeds the configured class capacity And all operations are atomic and retried safely using idempotency keys, ensuring no partial states remain
Notifications, Receipts, QR Codes, and Calendar Invites
Given a successful guest purchase or approved transfer When the roster update is committed Then confirmation notifications are sent to guest, booking owner, and instructor via configured channels (email/SMS) within 60 seconds (p95) And guests receive a receipt for paid purchases and a check-in QR code where applicable And .ics calendar invites (or updates/cancellations) are sent with correct timezone, location, and join instructions for hybrid classes And hard bounces are logged and retried per policy; undeliverable statuses are visible in the dashboard
Analytics Capture and Funnel Reporting with Filters
Given users encounter the guest paywall and proceed through purchase or transfer flows When events occur (impression, CTA click, payment initiated, payment succeeded, transfer requested, transfer approved/declined, exit step) Then events are captured with timestamp, class ID, instructor/studio IDs, device/session identifiers (pseudonymous), variant ID (if A/B), and consent state And dashboard metrics show impressions, conversion rate, transfer approval rate, incremental revenue, and step-level drop-off within 15 minutes of event time And exports (CSV and JSON) can be generated for a date range with cohort filters (e.g., new vs returning, device type) and class-level filtering, completing within 2 minutes for up to 100k rows
A/B Testing of Interstitial Copy and Pricing Messages
Given A/B testing is enabled for eligible paywall traffic When a visitor first views the paywall Then the visitor is deterministically bucketed into a variant per class-session using a stable key for session/device within that session And variant exposure, conversion, revenue, and transfer outcomes are attributed to the assigned variant And traffic allocation is configurable (e.g., 50/50 default) and changes only affect new sessions And the dashboard and export include per-variant metrics and confidence intervals or raw counts sufficient for statistical analysis
GDPR/CCPA Consent, Privacy Controls, and Data Retention
Given a visitor interacts with the paywall and analytics When consent is not granted for tracking Then no non-essential analytics events or identifiers are stored; necessary transactional records (payments, rosters) are processed under legitimate interest And users can access, export, and delete their personal data (DSAR) within 30 days of request; deletion propagates to analytics and exports And a Do Not Sell/Share preference is honored for CCPA, excluding data from advertising/third-party sharing And analytics event retention is configurable with a default max of 13 months; expired data is automatically purged

Family Cart

Book multiple children in a single, streamlined checkout. Each child gets their own eligibility check, pricing, and add-ons (e.g., mat rental, class packs), while you pay once and receive one organized confirmation. Seat holds apply per child to prevent double-bookings, family discounts are auto-applied, and everyone lands on the roster correctly—cutting time and errors for busy caregivers.

Requirements

Per-Child Eligibility & Profile Selection
"As a caregiver, I want to add multiple children to one booking and have each child checked for eligibility so that I don’t accidentally enroll an ineligible child."
Description

Support selecting multiple child profiles within a single cart. For each child, run age/grade/skill prerequisites and custom eligibility rules (e.g., date-of-birth cutoffs, membership status) against class configurations at search, add-to-cart, and checkout. Block ineligible enrollments with clear reasons and suggest eligible alternatives. Integrate with ClassTap’s profiles, guardian links, waivers, and class variant rules. Provide inline UI to add/manage child profiles and auto-fill required fields. Ensures compliance, reduces errors, and eliminates post-booking corrections.

Acceptance Criteria
Select Multiple Child Profiles in Family Cart
Given a logged-in guardian with two or more linked child profiles When the guardian opens a class detail page and initiates booking Then the cart displays a selectable list of only the guardian’s linked child profiles with name and DOB And the guardian can select one or more children to proceed And selecting an existing child auto-fills that child’s required fields (name, DOB, grade, skill level, membership status) from the profile And deselecting a child removes that child’s pending enrollment from the cart without affecting others And the selected children persist through navigation from class details to cart and checkout
Per-Child Eligibility Evaluation at Search Results
Given a guardian has selected one or more child profiles in the search filter When the class search results are displayed Then each class card shows per-child eligibility badges (Eligible or Ineligible) for the selected children And for each Ineligible child, a hover or tap reveals specific reason codes (e.g., Age below minimum, Grade mismatch, Membership required) And a "View eligible alternatives" action is available that filters to classes passing eligibility for that child And no class is marked Eligible for a child unless all configured prerequisites and custom rules pass for that child
Eligibility Enforcement on Add-to-Cart per Child
Given a guardian has selected multiple children for a specific class When the guardian clicks Add to Cart Then the system evaluates age, grade, skill, date-of-birth cutoff, membership status, and any custom rules for each selected child against the class (and variant, if applicable) And only the eligible children are added to the cart as separate line items And ineligible children are blocked from being added with a clear, per-child list of failure reasons And the UI provides up to three eligible alternative classes per ineligible child (if available), showing class name and next available start time And the action completes in under 2 seconds for up to five selected children under normal load
Per-Child Eligibility Revalidation at Checkout
Given a cart containing enrollments for multiple children When the guardian proceeds to checkout Then the system revalidates eligibility for each child against the latest class configuration and the child’s current profile data And if any child becomes ineligible (e.g., rule changes or profile updates), checkout is blocked for that child with explicit reasons while leaving eligible children intact And the guardian can remove or replace only the ineligible child’s enrollment and continue with eligible ones And the system offers eligible alternatives per ineligible child at this step And the Place Order button is disabled until all remaining children in the cart pass eligibility and required fields are complete
Inline Child Profile Add/Manage with Auto-Fill and Guardian Link
Given a guardian without a needed child profile during booking When the guardian selects "Add new child" inline from class details, cart, or checkout Then a modal or inline form appears capturing required fields (first/last name, DOB, grade, skill level, membership status) with client-side validation and field-level errors And upon save, the child profile is linked to the guardian account, persists to the profile store, and becomes immediately selectable in the cart And selecting the newly created child auto-fills their data wherever required in the flow And the guardian can edit an existing child’s non-immutable fields inline, with changes reflected immediately in eligibility checks
Variant Rules, Waivers, and Custom Rule Compliance per Child
Given a class configured with variants (e.g., age band or skill level), required waivers, and custom eligibility rules When a guardian attempts to enroll multiple children Then each child is automatically matched to the correct class variant based on profile data; if no variant matches, that child is blocked with a specific reason And required waivers are presented per child; a child’s enrollment cannot proceed until their waiver is accepted And each eligibility failure message references the rule that failed (e.g., DOB must be on or before 2016-09-01) and the child-specific value that failed And all pass/fail outcomes are evaluated independently per child without cross-child side effects
Per-Child Seat Hold & Inventory Lock
"As a caregiver, I want seats held for each child during checkout so that I don’t lose spots while entering details."
Description

When a child is added to a class/slot, decrement availability and create an independent seat hold per child with a visible countdown. Holds must persist through page refresh, be tied to session/account, and release on expiry or item removal. Prevent a single child from holding multiple seats for overlapping time slots. Admin-configurable hold duration and concurrency-safe inventory updates across devices. Emit events for hold-created/released to keep waitlists accurate and maintain audit logs.

Acceptance Criteria
Per-Child Hold Creation & Visible Countdown
Given a class with available seats and a caregiver adds Child A from Family Cart When Child A is added to the class Then availability decrements by 1 immediately and is not offered to other shoppers And a unique seat hold is created for Child A with a hold_id tied to the current session/account And a visible per-child countdown appears (mm:ss) and updates every second on the cart and class detail views And a hold_created event is emitted containing hold_id, child_id, class_id, account_id, start_at, end_at, timestamp And checkout for Child A consumes this hold if completed before expiry
Hold Persistence Across Refresh & Session/Account Binding
Given a seat hold exists for Child A with remaining time When the user refreshes the page, navigates away and returns within the hold duration, or opens a new tab in the same session Then the hold persists and the remaining countdown is restored with a drift of no more than 2 seconds And if the user signs out and signs into a different account during the hold, the hold is not accessible for checkout by the new account And if the original account returns within the hold duration, the hold remains usable and the countdown continues from the server-tracked time
Multi-Child Holds and Inventory Decrement
Given a class with at least 2 seats available and a Family Cart containing Child A and Child B When the caregiver adds Child A and then Child B to the same class Then two independent seat holds are created (one per child) and availability decrements by 2 in total And each child shows their own visible countdown timer And if only 1 seat is available when adding the second child, the second add attempt is blocked with a clear message and availability does not go below zero
Prevent Overlapping Holds for the Same Child
Given Child A holds a seat for Class X from 5:00 PM to 6:00 PM When the caregiver attempts to add Child A to any class whose time overlaps with 5:00–6:00 PM Then the system blocks the hold with an error stating the child already holds an overlapping seat and no availability is decremented And adding Child A to a class that starts exactly at 6:00 PM (no overlap) is allowed
Hold Release on Expiry or Removal
Given a seat hold exists for a child When the countdown reaches zero or the caregiver removes the child from the cart before checkout Then the hold is released within 2 seconds, the countdown disappears, and class availability increments by 1 And a hold_released event is emitted with hold_id, child_id, class_id, account_id, reason (expiry|removal), and timestamp And the waitlist is reevaluated and, if applicable, the next waitlisted user is notified or moved to pending within 3 seconds And an audit log entry is recorded for the release action
Concurrency-Safe Inventory Locking Across Devices
Given a class with exactly 1 seat remaining and two different accounts attempt to add a child within 100 ms of each other from different devices When both add-to-cart requests are processed Then exactly one seat hold is created and the other request receives a "Seat no longer available" message with no hold created And availability never becomes negative and no duplicate holds are recorded for the same seat/time And inventory state propagates to all affected clients within 2 seconds (e.g., availability and hold indicators update)
Admin-Configurable Hold Duration
Given an admin sets the seat hold duration to N minutes within the allowed range of 1–30 minutes When new holds are created after saving the setting Then each new hold’s initial countdown equals N minutes And existing active holds retain their original durations (no retroactive change) And attempts to set values outside 1–30 minutes are rejected with a validation message and no change is saved
Auto Family Discounts & Pricing Caps
"As a caregiver, I want family discounts applied automatically when booking multiple children so that I pay the correct reduced total without codes."
Description

Implement a pricing engine that detects household relationships and applies configurable family rules: sibling discounts, tiered pricing, per-order caps, exclusions, and eligibility windows. Calculate per-child line items and show an itemized savings breakdown in cart and confirmation. Stack correctly with promos, memberships, and class packs while honoring tax rules. Provide an admin UI to define rules, simulation tools for testing, and change audit history. Increases conversion and reduces manual adjustments.

Acceptance Criteria
Household Detection & Eligibility Windows
Given household matching is configured by the admin When a caregiver adds multiple children whose profiles share the configured household key Then the system marks those children as family-eligible for this order Given a child falls outside a rule’s eligibility dates or age/class constraints When pricing is calculated Then no family discount is applied to that child and the ineligibility reason code is stored and displayed in cart and confirmation Given children from different households are in the same cart When pricing is calculated Then family rules are applied only within each detected household group and not across households
Sibling Discount Tiering & Exclusions
Given a sibling tier rule (Child 1: 0%, Child 2: 10%, Child 3+: 15%) and tier ordering = highest-price-first When three eligible children with prices $30, $25, and $20 are in the cart Then family discounts applied are $0, $3.00, and $3.00 respectively and are attributed to the correct child line items Given a class or add-on is marked “Exclude from family discount” When pricing is calculated Then no family discount is applied to that line item and an exclusion reason is recorded and surfaced in the UI Given a discountable add-on is attached to a child When pricing is calculated Then the add-on receives the same tier percentage as that child unless overridden by a specific add-on rule
Per-Order Discount Cap Enforcement
Given a per-order family discount cap of $25 When calculated family discounts across all eligible line items total $32 Then the engine limits the applied family discount to $25 and retains the undiscounted remainder Given a distribution priority of “by child position in cart” When the cap is reached Then the engine reduces discounts starting from the last child backward until the cap is met and records a cap_reached indicator Given taxes are calculated after discounts When the cap reduces discounts Then taxable amounts reflect the capped discounts and tax totals remain arithmetically consistent with the cart total Given currency rounding to 2 decimals When cap apportionment occurs Then the sum of line-item family discounts equals exactly the cap after rounding and the order total equals the sum of line totals
Stacking with Promos, Memberships, and Class Packs; Tax Compliance
Given stacking precedence is configured as Membership → Family → Promo When all three benefits are present Then discounts are applied in that order and each subsequent discount is computed on the remaining price Given a class pack covers $15 of a $30 class and family discount is 10% When pricing is calculated Then the family discount applies only to the unpaid $15 portion, yielding a $1.50 discount Given a promo excludes stacking with family discounts for classes tagged “Private” When such a class is in the cart Then only the allowed discount(s) apply and the excluded one shows an excluded_by_rule reason Given a tax rule “tax after discounts” and the family discount is marked tax-reducing When discounts are applied Then tax is computed on the net price after applicable discounts Given a tax rule “tax before discounts” When discounts are applied Then tax is computed on the pre-discount price
Itemized Savings Breakdown in Cart and Confirmation
Given a multi-child cart with discounts When viewing the cart Then each child shows base price, each discount line itemized with label and amount, per-child subtotal, tax, and total, and an order-level “You saved $X” summary Given a completed order When the confirmation page and email are rendered Then the same itemized breakdown appears with identical amounts and labels as the cart Given an ineligible child or excluded line When displayed Then a “No family discount” label with the recorded reason is shown next to that line Given the org currency is GBP and locale is en-GB When amounts are shown Then all money values are formatted as £ with 2 decimals and thousands separators, and arithmetic totals match line items exactly
Admin Rule Management UI
Given an admin with Pricing Manager permission When creating or editing a Family Pricing rule set Then they can configure household matching method, sibling tiers, per-order caps, exclusions (by class, tag, add-on), eligibility window dates, stack precedence, rounding rules, and tax interaction flags Given required fields are incomplete or conflicting (e.g., cap < 0, overlapping tier ranges) When saving Then the form blocks submission with field-level errors explaining what to fix Given a rule set is scheduled with start and end dates When the current time enters or exits the window Then the rule set auto-activates or deactivates without manual intervention and is reflected in pricing Given environment scoping is set to Staging only When in Production checkout Then the rule set does not affect pricing and is hidden from Production rule lists
Rule Simulation & Audit History
Given a selected rule set and a mock household and cart input When running a simulation Then the engine returns the full pricing breakdown, applied rule list, reason codes for ineligible lines, and a success or fail indicator in under 3 seconds Given an order priced in checkout When viewing the order details in Admin Then the exact rule set version and evaluation trace used are displayed and downloadable as JSON and CSV Given any create, update, or delete to a family pricing rule set When saved Then an audit log entry records actor, timestamp, change diff, and previous and new values Given an admin selects a prior rule version When performing a rollback Then the prior version becomes active, a new audit entry is created, and future pricing uses the restored configuration
Per-Child Add-ons & Inventory
"As a caregiver, I want to choose different add-ons for each child so that each gets exactly what they need."
Description

Allow choosing add-ons per child (e.g., mat rental, t-shirt size, class pack) with independent pricing, eligibility, and inventory tracking. Support required vs optional add-ons, quantity limits, variants, and per-session vs term scope. Reflect add-ons on rosters and fulfillment lists and include them in taxation and reporting. Provide a child-grouped UI with clear subtotals and validation. Integrates with existing add-on catalog and stock systems to ensure accurate fulfillment.

Acceptance Criteria
Per-Child Add-On Selection UI & Subtotals
Given a family cart with two children, When the caregiver opens the add-ons section, Then the UI displays a distinct panel per child labeled with the child's name. Given a child panel, When add-ons are available, Then each add-on displays name, variant selector (if applicable), quantity control within min/max limits, unit price, and availability status. Given selected add-ons for a child, When quantities or variants change, Then the per-child add-on subtotal updates within 200 ms and the grand total updates accordingly. Given accessibility standards, When navigating via keyboard or screen reader, Then all add-on controls per child are reachable, labeled, and announce price and subtotal changes.
Required Add-Ons Enforcement per Child
Given a child with a required add-on, When attempting to proceed to payment without selecting the required add-on, Then the flow is blocked, the child panel is highlighted, and an inline error message specifies the missing add-on. Given a child without required add-ons, When proceeding to payment, Then no error is shown for that child. Given required add-ons with quantity constraints, When the caregiver selects a quantity outside the allowed range, Then validation prevents it and shows the allowed range.
Add-On Inventory Reservation & Decrement per Variant
Given an add-on variant with limited stock, When a caregiver selects quantity q for a child, Then q units are soft-reserved for the configured hold window and reflected in available stock to other shoppers. Given checkout success, When payment is captured, Then reserved units are decremented from inventory; Given checkout fails or the cart expires, When the hold window ends, Then reserved units are released. Given multiple caregivers concurrently selecting the last units of a variant, When holds are applied, Then only one checkout can capture remaining stock and the others receive an out-of-stock message prompting quantity adjustment. Given variant-level inventory, When the caregiver changes variant, Then reservations switch atomically from the old variant to the new variant, releasing old units and reserving new units if available.
Eligibility Rules Filter & Validate Add-Ons per Child
Given eligibility rules (e.g., age, level, membership), When viewing add-ons for each child, Then only eligible add-ons are selectable and ineligible add-ons are hidden or disabled with a reason. Given an API request attempts to add an ineligible add-on for a child, When the request is processed, Then the server returns a 4xx with a specific error code and the cart totals do not change. Given eligibility depends on selected class sessions, When the caregiver changes sessions, Then the eligible add-on set is re-evaluated per child and invalid selections are removed with an explanatory notice while valid selections are preserved.
Per-Session vs Term Scope Assignment
Given a term booking that spans N sessions, When the caregiver selects a per-session add-on with quantity q for a child, Then q units apply to each of the N sessions and appear on each session’s roster and fulfillment list. Given a term booking, When the caregiver selects a term-scoped add-on, Then it is recorded once per term for that child and does not duplicate across sessions. Given inventory-managed per-session add-ons, When applied across sessions, Then inventory is reserved per session accordingly; If any session lacks sufficient inventory, Then the selection cannot be saved and a message identifies the conflicting session dates. Given a single-session booking, When the add-on scope is per-session, Then behavior matches term scope with N=1.
Taxation, Pricing, Discounts, and Order Summary for Add-Ons
Given jurisdictional tax settings, When add-ons are added for a child, Then tax is calculated per line item using the add-on’s tax category and included in per-child subtotal and order total. Given family or promo discounts, When discount rules include or exclude add-ons, Then discounts are applied accordingly per line item without double-discounting and are visible in the per-child breakdown. Given rounding rules, When totals are computed, Then per-line and order totals round according to system settings with no more than ±$0.01 discrepancy between displayed and charged totals. Given order confirmation and receipt, When the order completes, Then line items show child name, add-on SKU/variant, quantity, unit price, tax, discounts, and extended amount. Given reporting exports, When daily sales are exported, Then add-on lines include SKU, variant, quantity, net, tax, discount, GL code, child ID, and class/session ID, with scope (per-session or term).
Roster & Fulfillment Outputs Reflect Per-Child Add-Ons
Given a class roster view, When the booking includes add-ons, Then each child’s row lists their add-ons with variant and quantity and required add-ons are flagged. Given a fulfillment list for a specific session, When generated, Then it aggregates add-on quantities by SKU/variant across children and is filtered to that session when scope is per-session. Given post-checkout edits (refunds or quantity changes), When changes occur, Then rosters and fulfillment lists update within 1 minute and maintain an audit entry with timestamp and actor. Given staff CSV export, When downloaded, Then columns include child name, add-on name, variant (e.g., T-shirt size), quantity, scope, and pickup status.
Unified Payment & Itemized Confirmation
"As a caregiver, I want to pay once and receive one organized confirmation that shows details per child so that I can keep records easily."
Description

Process a single transaction for the entire family cart while generating itemized ledger entries per child for tuition, discounts, taxes, and add-ons. Support saved payment methods, split tender, installments, and SCA/3DS. Generate one consolidated receipt and one organized confirmation (email/SMS) with per-child summaries, calendar files, waivers, and studio policies. After payment, create separate enrollments and sync each child to the correct roster. Expose per-order and per-child webhook events for integrations and accounting exports.

Acceptance Criteria
Single Transaction With Per-Child Ledger Itemization
Given a family cart with multiple children each having tuition, discounts, taxes, and add-ons When the caregiver completes checkout Then exactly one payment authorization and capture is created for the full order total And a separate ledger is created per child with distinct line items for tuition, discounts (negative lines), taxes, and add-ons And the sum of all per-child ledger lines equals the captured order total within currency precision rules And each ledger line includes child_id, class_id, SKU/GL code, quantity, unit price, tax rate, discount code (if applicable), and currency And capture is blocked if per-child ledger validation fails or totals do not reconcile
Saved Payment Methods and SCA/3DS Flow
Given a returning caregiver selects a saved payment method that may require SCA/3DS When the issuer requests a challenge Then a 3DS v2 challenge is initiated inline without losing cart state And on successful challenge the payment is captured and the order is marked paid And on challenge failure or timeout the user can retry or choose another method without creating duplicate authorizations And saved methods are vaulted as network/payment tokens; no raw PAN is stored on platform servers And SCA outcome details (version, transStatus, liability shift) are recorded in audit logs
Split Tender and Installments
Given the family cart total meets eligibility for split tender and/or installments When the caregiver allocates payment across up to two methods and/or selects an installment plan Then each tender leg is authorized for the specified amount and linked to a single order And if any leg authorization fails no captures occur and the user is prompted to reallocate and retry And for installments the first installment is captured at checkout and future installments are scheduled with explicit dates, amounts, and payment method And failed scheduled installments trigger dunning with at least 3 retry attempts and notifications without exceeding the authorized amount
Consolidated Receipt and Confirmation Delivery
Given an order is successfully paid When post-payment communications are generated Then exactly one consolidated receipt email is sent to the payer and one SMS confirmation is sent when a verified phone exists And the receipt includes order total, tender summary (type and last4 only), taxes, discounts, and per-child itemization with class names, dates/times, locations, and add-ons And for each child an ICS calendar file is attached or linked and waiver/policy links are included And messages are sent within 2 minutes of payment and duplicate messages are not sent on page refresh or retries
Enrollments and Roster Sync Per Child
Given payment capture succeeds and seat holds exist per child When enrollments are created Then a separate enrollment record is created per child per class with status enrolled or waitlisted based on availability at capture time And held seats are converted to enrollments atomically to prevent double-bookings under concurrency And rosters reflect new enrollments within 10 seconds and include child identifiers and family contact info for instructor view And if any child enrollment fails the failure is surfaced to staff and either the entire transaction is rolled back or the affected child’s charges are automatically refunded
Per-Order and Per-Child Webhooks
Given an order is paid and enrollments have been processed When webhook delivery occurs Then an order.paid event is emitted once per order and child.enrolled or child.waitlisted events are emitted per child And payloads include stable IDs, timestamps, HMAC signatures, idempotency keys, and references to per-child ledgers and rosters And webhooks are retried with exponential backoff for up to 24 hours on non-2xx responses and deliveries are idempotent And event ordering is preserved per order_id and delivery logs are queryable by event id and status
Accounting Export Alignment
Given itemized per-child ledgers exist for paid family orders When an accounting export is generated for a date range Then each child’s ledger lines appear as separate rows with mapped GL codes, tax codes, discount references, and currency compatible with QuickBooks/Xero CSV templates And exported totals reconcile to internal revenue reports within currency rounding rules And voided/failed orders are excluded; refunds appear as negative lines linked to original ledger IDs And exports include a unique batch ID, generation timestamp, and are deterministic when regenerated for the same filters
Split Outcomes & Per-Child Waitlist
"As a caregiver, I want children who can enroll to be confirmed and others placed on the waitlist in the same flow so that I don’t have to start separate bookings."
Description

Support mixed outcomes within one checkout: confirm enrollments for available children while placing others on waitlists with clear status indicators. Collect waitlist preferences and payment rules (auto-charge on seat open, manual confirm) per studio policy. Notify guardians per child and update charges and confirmations to reflect actual outcomes. Maintain an audit trail for conversions from waitlist to enrollment and adjust inventory, rosters, and discounts accordingly.

Acceptance Criteria
Mixed Checkout: Enroll Some, Waitlist Others
Given a family cart with multiple children and partial class seat availability, When the guardian checks out, Then seats are allocated and held per child for available classes and remaining children are marked as waitlisted for their specific classes. And Then the order summary itemizes each child with a status badge of "Enrolled" or "Waitlisted" and displays prices only for enrolled items; waitlisted items show $0 due now and the selected waitlist preference. And Then payment authorization/capture is performed only for the sum of enrolled items after applicable family discounts; no charges are created for waitlisted items at checkout. And Then class rosters are updated only for enrolled children; waitlisted children appear on the waitlist with their position; class inventory decrements only for enrolled children. And Then the confirmation page and notifications reflect per-child statuses matching the order summary.
Capture Waitlist Preferences per Child Respecting Studio Policy
Given the studio policy allows both Auto-Charge and Manual Confirm for waitlists, When a child is added as waitlisted, Then the UI requires the guardian to select one preference per child and stores it on the child line item. Given the studio policy restricts to Auto-Charge only, When a child is added as waitlisted, Then Auto-Charge is preselected, non-editable, and persisted on the child line item. Given the studio policy restricts to Manual Confirm only, When a child is added as waitlisted, Then Manual Confirm is preselected, non-editable, and persisted on the child line item. And Then the stored preference is displayed in the cart, order summary, and confirmations and is used to drive subsequent conversion behavior.
Auto-Charge Conversion Flow from Waitlist
Given a child is waitlisted with preference Auto-Charge and a vaulted payment method with guardian consent exists, When a seat becomes available, Then the system recalculates family discounts across siblings for that order and attempts to capture the correct amount for that child. Then on successful capture, the child is atomically moved from waitlist to enrolled, inventory decremented, roster updated, an audit record is created, and a confirmation notification is sent within 1 minute. Then on failed capture, the child remains on waitlist, inventory/roster are unchanged, a failure audit entry is logged, and a failure notification is sent within 1 minute. And Then no other waitlisted child is charged for that same seat until the current attempt completes and the waitlist order is respected.
Manual Confirm Conversion Flow with Timed Hold
Given a child is waitlisted with preference Manual Confirm, When a seat becomes available, Then a notification with a confirm link is sent to the guardian and a per-child seat hold is created for the studio-configured hold window. When the guardian confirms within the hold window, Then payment is captured, the child is enrolled, inventory decremented, roster updated, family discounts recalculated, a confirmation notification is sent, and an audit record is created. When the guardian does not confirm within the hold window, Then the hold expires, the seat is released to the next waitlisted person, no charge occurs, an expiration audit entry is logged, and an optional expiration notification is sent. And Then the hold timer and current hold status are visible to admins in real time.
Discount Recalculation on Split Outcomes and Conversions
Given family discounts are tiered by number of enrolled siblings, When only a subset of children enroll at checkout, Then discounts apply based on enrolled count only and the invoice reflects that calculation. When a waitlisted sibling later converts to enrolled (via auto-charge or manual confirm), Then the system recalculates the family discount across all siblings on that order, charges or refunds the net difference, updates receipts, and sends an updated notification. And Then reconciliation ensures no double-charging; prior payments are adjusted rather than duplicated, and line-item histories show before/after amounts.
Audit Trail and Reporting for Waitlist Conversions
Given any waitlist-to-enrollment conversion attempt, When it occurs, Then an immutable audit record is created including child, class, schedule, order ID, original waitlist position, selected policy (auto/manual), trigger source (system/admin/guardian), timestamps, payment transaction IDs and outcomes, pre/post discount prices, inventory change, roster actor, and notification IDs. And Then admins can view these records by order, child, and class, filter by date and outcome, and export them to CSV. And Then audit records are linked from roster and order detail pages and cannot be edited after creation.
Per-Child Seat Holds and Double-Booking Prevention During Checkout
Given multiple guardians are viewing the same class, When a child is added to cart, Then a per-child seat hold is created for the studio-configured duration and available inventory is reduced accordingly for other sessions. When checkout completes within the hold, Then the hold converts to enrollment; when checkout is abandoned or times out, Then the hold is released and inventory is restored. If the last seat is taken by another session during payment, Then the affected child is switched to waitlisted before payment capture, the order total is recalculated to exclude that seat, and an inline message explains the change. And Then hold creation, expiration, conversion, and release events are logged and visible to admins.
Per-Child Cancellations, Transfers & Refunds
"As a caregiver, I want to change or cancel for one child without impacting the others so that I can manage each child’s schedule independently."
Description

Enable post-purchase management at the child level: cancel specific sessions, transfer to another class/slot, or refund selected line items without affecting siblings. Enforce studio policies for cutoff windows, change fees, and non-refundable components. Recompute family discounts when items change and apply adjustments or credits fairly. Update rosters, capacities, inventory, and waitlists in real time. Produce itemized refund receipts and exportable accounting reports.

Acceptance Criteria
Enforce Per-Child Cutoff Windows and Options
Given a family booking where Child A and Child B are enrolled in upcoming sessions and the studio policy is configured with cancellation cutoff = 24 hours and transfer cutoff = 12 hours When the caregiver opens Manage Booking for Child A 36 hours before the session start Then the UI enables Cancel and Transfer for eligible sessions and disables them for ineligible sessions, displaying the exact cutoff timestamp and policy message for each session And when the caregiver opens Manage Booking for Child B 6 hours before the session start Then Cancel and Transfer are disabled with an explanatory policy message and no backend mutation occurs upon click And sibling eligibility and options remain independent per child
Cancel Single Session for One Child Within Cutoff
Given a purchase where Child A and Child B are both enrolled in Session S1 and the studio policy sets cancellation cutoff = 24 hours and change fee = $5 And Child A’s refundable tuition for S1 is $30 and there are no non-refundable components on A’s S1 line And a family discount was applied across the order When the caregiver cancels Child A’s S1 at least 24 hours before start Then the system calculates a preview showing refundable tuition ($30) minus change fee ($5) plus/minus any family discount adjustment attributable to A’s canceled line according to the configured rule and displays the net refund before confirmation And upon confirmation, Child A is removed from S1 roster, Child B remains enrolled, class capacity increments by 1, and the next eligible waitlisted learner (if any) is promoted atomically And a confirmation and itemized refund receipt are sent to the caregiver and studio, and the audit log records actor, timestamp, IP, and applied policy
Transfer One Child to Different Class With Change Fee
Given Child A is enrolled in Class X, Session S1 (price $25) and the caregiver selects a transfer to Class Y, Session S2 (price $35) And the studio policy sets transfer cutoff = 12 hours and change fee = $3 and seat hold duration = 10 minutes When the caregiver initiates the transfer more than 12 hours before S1 starts Then the system places a hold on one seat in Y:S2 for Child A for 10 minutes and shows a countdown, while keeping A’s seat in X:S1 until confirmation And when the caregiver confirms within the hold window, the system atomically removes A from X:S1, adds A to Y:S2, adjusts capacities, triggers any waitlist promotions, and charges the price difference ($10) plus the change fee ($3) minus any applicable credits And if the caregiver abandons or the hold expires, the hold is released, A remains in X:S1, and no charges are applied
Refund Selected Line Items Per Child With Non-Refundable Logic and Discount Recompute
Given Child A’s order contains Tuition $40 (refundable), Mat Rental $5 (non-refundable), and applicable tax And the studio policy allows refunds on tuition with a 10% policy fee and flags Mat Rental as non-refundable And a family discount rule has been applied across children on tuition When the studio processes a refund for Child A’s items Then the system prevents refunding the non-refundable Mat Rental line, allows refunding up to the refundable tuition minus the 10% policy fee, and recalculates the family discount so the remaining items across the family reflect the configured rule And any discount reversal or adjustment is applied as part of the refund or issued as account credit without introducing new charges beyond policy And the refund preview and final receipt itemize components (tuition, add-ons, taxes, policy fees, discount adjustments) and show the net returned amount and method
Real-Time Updates to Rosters, Capacity, Inventory, and Waitlists
Given concurrent users are booking and managing the same class When a per-child cancellation or transfer is confirmed Then the affected class rosters display the change within 2 seconds to instructors and caregivers, capacity counters update immediately, and associated inventory (e.g., mat rentals) is incremented/decremented accordingly And the next waitlisted learner is promoted without race conditions, and no double-booking occurs even if another user attempts to take the last seat concurrently And any conflicting simultaneous action returns a clear error and leaves data in a consistent state
Generate Itemized Refund Receipt and Accounting Export
Given a per-child cancellation, transfer, or refund results in a monetary adjustment When the action is confirmed Then a caregiver-facing and studio-facing receipt is generated within 1 minute containing: child name, class/session identifiers, per-line amounts, non-refundable flags, policy fees, taxes, discount adjustments, net amount, currency, payment method (type and last 4), transaction/authorization IDs, actor, timestamp, and policy references And the accounting export is updated with a row per financial event including: transaction ID, parent transaction link, GL codes per line, revenue reversal, fee income, tax, discounts, tips, processor fees, liability adjustments, and payout batch reference And CSV export and webhook payload totals reconcile to the payment processor amounts within ±$0.01

AgeSmart Filter

See only classes each child is eligible for based on age, grade, or prerequisites. Toggle between kids to instantly filter schedules, get clear reasons when a class isn’t a fit, and see smart suggestions for the nearest eligible option. Fewer misbookings, less back-and-forth, and more confidence at checkout.

Requirements

Multi‑Child Profiles
"As a parent with multiple children, I want to save and switch between each child’s profile so that I can see the right classes for the right child without re-entering details each time."
Description

Enable account holders to create, edit, and select among multiple child profiles, each storing first/last name, date of birth, grade (optional), known prerequisites/skills, and relevant notes. Persist profiles securely across web and mobile booking surfaces, expose them in the booking flow for quick switching, and make them the single source of truth for eligibility checks. Support import from prior bookings, validation on entry (e.g., DOB formats, plausible ages), and localization for grade systems. Ensure data is available via internal APIs to drive filtering, reminders, and waitlists without duplicating entry.

Acceptance Criteria
Create New Child Profile (Web)
Given an authenticated account holder on web When the user selects Add Child, enters First Name, Last Name, Date of Birth in the locale format, optionally Grade, Skills, and Notes, and clicks Save Then client-side validation blocks submission until required fields are present and DOB format is valid And server-side validation rejects future dates and implausible ages outside 0–21 years And on success the API responds 201 with profileId and normalized fields (dob as YYYY-MM-DD) And the new profile appears in the profile list within 2 seconds without page reload And PII fields are stored encrypted at rest and are not logged in plaintext
Edit Existing Child Profile (Web and Mobile)
Given an authenticated account holder with at least one child profile When the user edits any field of a selected child and saves Then the same validation rules as create are enforced And the API responds 200 with updated profile and updatedAt greater than previous value And changes are visible on both web and mobile within 5 seconds of save And future eligibility checks use the updated data, while past bookings remain unchanged
Quick Child Switching During Booking
Given an account holder with multiple child profiles When the user initiates a booking and switches the selected child in the booking flow Then the currently selected child is clearly indicated on the schedule and checkout steps And the class list immediately updates to reflect eligibility for the selected child And ineligible classes show a clear reason badge (Age, Grade, or Prerequisite) without blocking page interaction And the selected child context persists through payment confirmation
Import Child From Prior Bookings
Given an account with historical bookings that include child name and DOB but no saved profiles When the user chooses Import from prior bookings Then the system proposes exact matches based on First Name, Last Name, and DOB And on confirm a new profile is created and linked to the historical bookings And if a profile with the same First Name, Last Name, and DOB already exists the user is prompted to merge instead of creating a duplicate And import is available on both web and mobile surfaces
Validation and Localization for Grade and DOB
Given the tenant locale configuration is set When the user enters Grade and DOB for a child Then Grade options reflect the locale’s grading system and are stored as a normalized code with a localized label And leaving Grade blank is allowed without error And DOB input accepts the locale display format but is stored and returned as ISO 8601 date (YYYY-MM-DD) And invalid or implausible DOB values show localized inline error messages and block save
Internal API Availability for Eligibility, Reminders, and Waitlists
Given internal services require child data for filtering, reminders, and waitlists When a service calls GET /internal/children/{id} or GET /internal/children?accountId={accountId} Then the response includes id, accountId, firstName, lastName, dob (YYYY-MM-DD), gradeCode, gradeLabel, skills[], notes, createdAt, updatedAt And services can reference childId when creating bookings, waitlists, and reminders without re-entering child attributes And requests require valid service authentication and unauthorized requests receive 401/403 And read endpoints meet P95 latency under 300 ms within the production environment
Cross-Platform Persistence and Consistency
Given a child profile is created or edited on one platform (web or mobile) When the same account is opened on the other platform and the profile list is refreshed Then the updated profile data is visible within 5 seconds with identical field values and ordering And caching is invalidated on mutation so stale data is not shown And eligibility checks across all touchpoints resolve child data from the saved profile as the single source of truth
Eligibility Rules Engine
"As a studio owner, I want to define clear eligibility rules for each class so that only appropriate students can book and staff don’t need to manually screen registrations."
Description

Provide a configurable, high‑performance rules service that determines class eligibility based on age, grade, and prerequisites. Support min/max age windows assessed at class date or term start, grade‑based rules with configurable cutoff dates (e.g., school year boundaries), prerequisite completion requirements, and optional buffers (e.g., within 30 days of turning required age). Return deterministic outcomes with machine‑readable reason codes and human‑readable messages. Include an admin UI to define rule templates per program, set timezones, and preview outcomes for sample profiles. Integrate with search, class detail pages, checkout, waitlists, and APIs, with caching and fallbacks to maintain fast responses under load.

Acceptance Criteria
Age eligibility at class date with optional buffer
Given a single-session class with min_age=8 and buffer_days=30, and a child aged 7y 11m 10d on the class date, When buffer is enabled, Then the child is Eligible with reason_code=AGE_WITHIN_BUFFER and the message includes the days-until-birthday. Given the same inputs and buffer disabled, When evaluating, Then the child is Ineligible with reason_code=AGE_BELOW_MIN. Given a class with max_age=10 and a child is 10y 0m 1d on the class date, When evaluating, Then the child is Ineligible with reason_code=AGE_OVER_MAX. Given a multi-session term configured with age_date_mode=TERM_START, When evaluating, Then age is computed at term_start_date; When switched to age_date_mode=EACH_CLASS, Then age is computed at each session date and outcomes reflect the strictest session.
Grade eligibility with configurable cutoff and timezone
Given program_timezone=America/New_York and grade_cutoff=2025-09-01 00:00 local, When evaluating a class requiring min_grade=1, Then a child marked Grade=1 as of cutoff is Eligible. When the cutoff is changed to 2025-08-15 and published, Then eligibility outcomes update accordingly and are reflected in Preview within 1s of save. Given a child below min_grade, When evaluating, Then outcome is Ineligible with reason_code=GRADE_BELOW_MIN and the message includes the required grade. Given a child above max_grade, When evaluating, Then outcome is Ineligible with reason_code=GRADE_ABOVE_MAX. Given identical inputs evaluated in different program timezones at midnight boundaries, When evaluating, Then grade determination shifts only according to the program_timezone, not the requester timezone.
Prerequisite completion with equivalence and expiry
Given a class requires any_of=[Intro Swim A, Intro Swim B] and validity_days=365, When the child completed Intro Swim B within 365 days, Then the child is Eligible. Given an equivalence_map where Level 1 ≡ Intro Swim A, When the child completed Level 1, Then the prerequisite is satisfied. Given all_of=[Level 1, Level 2], When the child has only Level 1, Then outcome is Ineligible with reason_code=PREREQ_MISSING and the message lists unmet prerequisites ["Level 2"]. Given a completion older than validity_days, When evaluating, Then outcome is Ineligible with reason_code=PREREQ_EXPIRED. Prerequisite outcomes are unaffected by seat availability or schedule; only profile data and rule definitions may change the result.
Reason codes and localization
For every evaluation, Then the engine returns {eligibility in [Eligible, Ineligible], reason_code (non-empty), message (localized)} with fallback to program default locale. When multiple ineligibility reasons apply, Then primary_reason is selected by precedence [PREREQ, AGE, GRADE] and secondary_reasons contains all additional reason_codes. Reason codes are stable and drawn from a versioned catalog; deprecated codes are not emitted in new evaluations. Message templates include interpolated params {min_age, max_age, cutoff_date, unmet_prereqs} and are URL-safe and free of PII.
Admin UI rule templates and preview outcomes
Given an admin user, When creating/editing a rule template, Then the form supports fields: min_age, max_age, age_date_mode, buffer_days, min_grade, max_grade, grade_cutoff, prereq_sets (any_of/all_of), validity_days, equivalence_map, program_timezone. When invalid combinations are entered (min_age > max_age, buffer_days < 0, min_grade > max_grade, invalid timezone), Then Save is disabled with inline errors. When clicking Preview with a sample child profile (DOB, grade, completions) and class metadata, Then the preview returns outcome with reason_code/message and P95 latency <= 300ms. Templates are versioned; when publishing a version, Then only the published version is used by the rules engine; drafts do not affect live evaluations.
Integration across search, class detail, checkout, and waitlist
Given a selected child on the schedule search, When filtering, Then ineligible classes are suppressed by default and a "Show ineligible" toggle reveals them with reason badges. On class detail, When the child is ineligible, Then the page displays reason_code/message and a "Nearest eligible option" link to the next class instance within the same program that meets rules (within 30 days if available). At checkout, When the child is ineligible, Then booking is blocked with the same reason_code/message; waitlist join is allowed and shows reason; when buffer is the only blocker, Then the message includes the expected eligible date. Across search, detail, checkout, and APIs, Then the same child/class yields identical reason_code/message and includes rule_version and evaluated_at in the payload.
Performance, caching, and resilience under load
Under 1000 RPS, Then Rules API meets P95 <= 50ms, P99 <= 120ms latency and error rate <= 0.1% with cache warm. Cache hit ratio is >= 80% for repeat evaluations per child/class with TTL=10 minutes; cache invalidates within 5 seconds of profile or rule changes. On rules service or cache failure, Then the system serves last-known-good decisions for up to 15 minutes with decision_freshness=STALE and logs a WARN; no auto-eligibility is granted if the last-known decision was Ineligible. All evaluations are audit-logged with normalized inputs, applied program_timezone, rule_version, evaluated_at, and outcome; logs are retrievable by admins via the UI within two clicks.
Instant Child Toggle & Filter
"As a caregiver, I want to toggle between my children and see only the classes each is eligible for so that I can plan quickly without confusion."
Description

Add a responsive UI control to select a child profile and instantly filter the schedule to show only eligible classes, with options to view All, Eligible, or Not Eligible. Apply filters without page reloads, preserve state in URLs for sharable links, and default to the last‑used child for returning sessions. Ensure accessibility (keyboard navigation, ARIA labels), mobile readiness, and performance by reusing precomputed eligibility results. Integrate seamlessly with existing search facets (location, date, modality) and instructor pages.

Acceptance Criteria
Child Toggle Filters Schedule Instantly
Given a loaded schedule list and a selected child, When the user selects a different child from the child selector, Then the class results update in-place within 300 ms without a full page reload. Given Eligible view is active, When the user toggles to a different child, Then only classes eligible for the newly selected child are displayed and the visible result count matches the filtered total. Given network activity is monitored, When toggling children, Then no server-side eligibility recomputation request is made and precomputed eligibility data is used. Given the page has a vertical scroll position, When toggling children, Then the scroll position is preserved if at least one result remains visible.
View Mode Toggle: All, Eligible, Not Eligible
Given a selected child, When the user taps All, Then all classes in the current search scope are shown with an eligibility badge per class for that child. Given a selected child, When the user taps Eligible, Then only classes the child can attend are shown and the result count equals the number of eligible classes. Given a selected child, When the user taps Not Eligible, Then only classes the child cannot attend are shown with a concise reason label per class (e.g., age, grade, prerequisite). Given any view mode, When the view mode changes, Then the active state is visibly indicated and result counts update within 300 ms.
URL State Preservation and Shareable Links
Given a selected child and a view mode with search facets applied, When the user copies the page URL, Then the URL includes childId and view query parameters and active facet parameters. Given a share URL containing childId and view, When a user opens it in a new session, Then the same child is selected, the same view is active, and the same filtered results are rendered. Given browser navigation, When the user uses Back/Forward, Then the child selection and view mode restore to the historical state without a full page reload.
Default to Last-Used Child
Given a returning user with multiple children and no childId in URL, When the schedule loads, Then the last-used child from the prior session is preselected. Given a first-time user or no prior child selection stored and no childId in URL, When the schedule loads, Then the first child in profile order is selected by default. Given a stored last-used child no longer exists, When the schedule loads, Then the system selects the first available child and clears the invalid preference.
Accessibility: Keyboard and Screen Reader Support
Given a keyboard-only user, When focusing the child selector and view toggle, Then all interactive controls are reachable via Tab/Shift+Tab and operable via Enter/Space. Given assistive technologies, When navigating the child selector and view toggle, Then controls expose descriptive ARIA labels including the selected child's name and active view. Given focus state, When changing child or view, Then focus remains on the control and a polite ARIA live region announces the updated result count. Given contrast requirements, When rendering the active and inactive controls, Then text and control contrast ratios meet WCAG 2.1 AA.
Mobile Readiness
Given a viewport width ≤ 375px, When the child selector and view toggles render, Then touch targets are at least 44x44 px and controls do not overflow. Given mobile devices, When toggling child or view, Then updates complete within 500 ms and the content does not jump unexpectedly (scroll position preserved). Given a simulated Slow 3G network, When interacting with toggles, Then no full page reload occurs and input delay remains < 150 ms. Given the schedule list on mobile, When scrolling, Then the child selector and view toggle remain accessible via a sticky header or equivalent affordance.
Integration with Search Facets and Instructor Pages
Given location, date, and modality filters are applied, When the user changes child or view, Then the results respect both eligibility and the active facets. Given an instructor profile page, When selecting a child or toggling view, Then filtering behavior matches the global schedule page, including URL state persistence. Given any facet is cleared or adjusted, When eligibility filters are active, Then the resulting list updates correctly without conflicts and within the defined performance thresholds.
Ineligibility Reason Transparency
"As a parent, I want to understand exactly why a class isn’t a fit so that I can make the right choice or take steps to become eligible."
Description

For classes where a selected child is not eligible, display clear, concise explanations and next steps (e.g., “Requires age 7 by Sep 1; your child turns 7 on Oct 2” or “Missing prerequisite: Level 1”). Provide consistent reason codes, localized message templates, and UI patterns (tooltips, inline badges, detail modals) that avoid exposing sensitive information to other users. Include computed deltas (days until eligible) and contextual CTAs (see alternatives, join waitlist) to reduce support inquiries and misbookings.

Acceptance Criteria
Age cutoff ineligibility displays transparent reason, delta, and alternatives
Given a class rule age >= 7 by 2025-09-01 and a selected child with DOB 2018-10-02 When the parent views the class card or detail page Then an inline badge shows "Not eligible" and a tooltip states "Requires age 7 by Sep 1; your child turns 7 on Oct 2 (31 days after cutoff)" And reasonCode = AGE_CUTOFF_NOT_MET is available to the UI and analytics events And the primary CTA "See eligible classes" navigates to a prefiltered schedule for the selected child showing the nearest eligible option by date And the enroll/checkout action is disabled with aria-disabled=true and an accessible description referencing the ineligibility reason
Missing prerequisite explanation with actionable CTAs
Given a class requires prerequisite "Level 1" and the selected child has no recorded completion of Level 1 When the parent views the class card or detail page Then the message reads "Missing prerequisite: Level 1" And reasonCode = PREREQ_MISSING is present And a CTA "View Level 1 classes" opens a filtered list of Level 1 options in the same program/location And if placement assessments are enabled, a "Request assessment" CTA is shown; otherwise it is not rendered
Grade requirement mismatch with waitlist option
Given a class requires Grade 3–5 and the selected child is Grade 2 When the parent views the class detail page Then the UI shows "Requires Grade 3–5; your child is Grade 2" And reasonCode = GRADE_NOT_MET is present And if the class waitlist is enabled, a "Join waitlist" CTA is visible and functional; if disabled, no waitlist CTA is shown And clicking "Join waitlist" adds the child to the waitlist and returns a confirmation notification
Localized ineligibility messages via templates
Given the user's locale is es-MX and the selected child is ineligible due to age rule age >= 7 by 2025-09-01 When the class card is rendered Then the ineligibility text is displayed using the es-MX template with localized date formats and numerals And if the es-MX translation is missing, the message falls back to en-US while reasonCode remains AGE_CUTOFF_NOT_MET And all displayed dates/times reflect the organization's timezone setting
Consistent reason presentation across UI surfaces
Given a class has multiple ineligibility reasons (PREREQ_MISSING and AGE_CUTOFF_NOT_MET) When reasons are shown as an inline badge, tooltip, and detail modal Then each surface displays the same primary reason text using the priority order PREREQ_MISSING > AGE_CUTOFF_NOT_MET > GRADE_NOT_MET And the same reasonCode for the primary reason is present across surfaces And secondary reasons are accessible via a "More details" view within the modal
No sensitive child data exposed to unauthorized viewers
Given a public booking page or a viewer who does not own the selected child context When ineligibility reasons are rendered Then messages contain no child-identifying data (no name, no full birthdate, no grade level) and use generic phrasing And API/network responses for unauthorized contexts exclude child DOB, grade, or computed deltas; only reasonCode and templateId are returned And URLs, storage, and markup contain no child-specific PII or computed values
Smart Suggestions & Nearest Fit
"As a busy caregiver, I want helpful alternatives when a class isn’t eligible so that I can still book something that works without starting my search over."
Description

When a class isn’t a match, surface the nearest eligible alternatives ranked by proximity in age/grade requirement, time, location, and availability. Include options that become eligible soon (e.g., next session after birthday), related levels, sibling‑friendly time blocks, and waitlist options when appropriate. Respect existing capacity rules and instructor schedules, deduplicate across programs, and track impressions and click‑throughs for optimization. Provide API hooks and components to embed suggestions on class cards and detail pages.

Acceptance Criteria
Nearest Eligible Alternatives Ranking
Given a user views a class that is not a match for a selected child When the system generates alternative suggestions Then return between 3 and 8 suggestions sorted by a composite score: eligibility proximity 40%, time similarity 25%, location proximity 20%, availability 15% And then the first suggestion must be Eligible Now; if none are Eligible Now, the first must be the soonest Eligible Soon option And then each suggestion displays up to two reasons (e.g., "Age within 2 months; 1.2 miles away") And then results are deterministic for the same inputs within the same calendar day And then exclude any class that violates capacity rules or conflicts an instructor’s existing schedule And then p95 backend generation latency is ≤ 300 ms with ≤ 500 candidate classes
Becomes Eligible Soon Window
Given a child fails eligibility due to age/grade today When a class becomes eligible for the child within 0–45 days or by the next session start date, whichever is sooner Then include the class under an Eligible Soon group with a label "Eligible on <DATE>" And then exclude classes whose eligibility date is >45 days after the session start unless an admin override flag is true And then if eligibility starts mid-session, indicate the first eligible attendance date And then Eligible Soon items never rank above Eligible Now items when any Eligible Now exist And then boundary tests pass for birthday today, +1 day, +45 days, and leap-day cases
Related Levels and Prerequisite Compliance
Given a class is rejected for unmet level/prerequisites When generating alternatives Then include up to 3 related level options the child qualifies for (e.g., nearest lower level) and optionally the next level only if catalog marks it as instructor-approvable And then do not suggest any class with unmet prerequisites unless the prerequisite class is also suggested with a direct link And then each related suggestion carries a badge explaining the relation (e.g., "Prereq: Level 1 required" or "Try Level 1 first") And then clicking the prerequisite link opens the qualifying class detail preserving child context and filters And then analytics records a prereq_link_click event with classId, childId, and viewContext
Sibling-Friendly Time Blocks
Given a household selects two or more children When generating alternatives Then surface sibling-friendly pairings whose start/end times overlap within a 15-minute tolerance and are at the same venue or within 2 miles And then display at least 2 pairings when available, grouped under a Sibling-Friendly section And then exclude pairings that conflict with existing bookings for any selected child And then rank sibling-friendly groups above solo suggestions when each selected child has at least one Eligible Now option And then an Add Both to Cart action creates two cart line items with correct child assignment and pricing And then end-to-end test verifies cart contents and no time conflicts
Waitlist Option Surfacing
Given a target class is full When the provider has waitlist enabled and waitlist capacity not exceeded Then include a suggestion with CTA "Join Waitlist" and availability status Waitlist And then show estimated waitlist position or range when the provider exposes it; otherwise omit the estimate And then exclude waitlist options when waitlist is disabled or max waitlist size reached And then the CTA routes to the waitlist flow with correct classId and childId context And then track waitlist_impression and waitlist_cta_click events with impressionId, classId, childId, viewContext
Deduplication, Capacity, and Instructor Conflicts
Given classes may be cross-listed across programs/sessions When compiling suggestions Then deduplicate by canonicalClassId so each unique class appears once And then if duplicates differ only by marketing name or program, retain the instance with earliest start and nearest location to the user And then exclude any class instance with remainingCapacity ≤ 0 And then exclude classes that overlap with the assigned instructor’s other scheduled classes And then unit tests assert dedup across providers, zero-capacity exclusion, and instructor double-booking exclusion
Embed API and Tracking Hooks
Given integrators embed suggestions on class cards and detail pages When using the Suggestions API and UI component Then expose GET /v1/suggestions?childId={id}&classId={id} returning: suggestionId, classId, title, eligibilityStatus (EligibleNow|EligibleSoon|Related|Waitlist), reason, startDateTime, venueId, distance, availability, tags, ctaType, impressionId And then provide a <classtap-suggestions> component with attributes child-id, class-id, view-context (card|detail) and events suggestionImpression, suggestionClick And then fire at most one impression per impressionId per view-context per 24 hours, only when visible And then emit click payload {impressionId, suggestionId, classId, childId, viewContext, timestamp} and record server-side within p95 ≤ 200 ms with at-least-once semantics and de-dupe by (impressionId, suggestionId) And then the embedded component adds ≤ 20 KB gzipped and ≤ 1 additional network request on first paint
Prerequisite Tracking & Verification
"As an instructor, I want prerequisites to be automatically verified so that only prepared students enroll and I spend less time checking records."
Description

Maintain a record of prerequisite completions and skill achievements per child, mapped to class requirements. Sync with attendance/completion events from ClassTap and allow admins to map classes to skills (e.g., Level 1 certifies Skill A). Auto‑validate prerequisites at booking, support manual admin overrides with audit logging, and accept external proof via uploads or API integrations. Show prerequisite status in the booking flow and update eligibility in real time as new completions are recorded.

Acceptance Criteria
Auto‑Validate Prerequisites at Booking
Given a class configured with prerequisite skills and/or certifications And a child profile with a recorded skill history When a parent attempts to add the class to the cart or checkout Then the system performs a server‑side validation of all required prerequisites for that child And if any prerequisite is missing, the add/checkout is blocked and a message lists each missing prerequisite by name And if all prerequisites are met, the class may be added/checked out without blocking And the validation decision (pass/fail), evaluated rules, childId, classId, and timestamp are recorded in an immutable audit entry
Show Prerequisite Status in Booking Flow
Given a user views a class detail page or the checkout step with a selected child When the page renders Then the UI displays one of: Eligible, Not Eligible (with explicit missing items), Under Review (pending verification), or Overridden (admin override) for that child And each status includes a short explanation and a link or CTA to view details or resolve (e.g., upload proof) And the status reflects the most recent backend state within 5 seconds of any change
Admin Maps Class‑to‑Skill Certifications
Given an admin opens Class Settings for a class When the admin maps one or more certifying skills (e.g., completing this class awards Skill A) Then the mapping is saved versioned with editor, timestamp, and affected skills And when a completion event for that class is recorded for a child with outcome=Completed Then the child’s skill ledger is updated to include each mapped skill with source=ClassCompletion and the completion date And updating mappings later does not retroactively alter previously awarded skills
Sync Skills from Attendance/Completion Events
Given ClassTap emits an attendance/completion event for a session When the event outcome indicates completion that qualifies for mapped skills Then the child’s prerequisite record is updated within 5 seconds of event receipt And downstream eligibility for all impacted classes is recalculated for that child And the event processing is idempotent (duplicate events do not create duplicate skill entries)
Manual Admin Override with Full Audit Trail
Given an admin with permission creates a prerequisite override for a child and class (or skill) When the override is saved with reason and optional expiry date Then bookings for that class by that child bypass prerequisite checks until expiry or revocation And an audit log entry records adminId, childId, classId/skillId, reason, createdAt, expiresAt, and action=Create/Update/Revoke And revoking an override immediately restores normal validation behavior
External Proof via Upload/API Integration
Given a parent uploads proof of a skill or an external system posts verified completion via API When the submission meets validation rules (file types: PDF/JPG/PNG, max 10MB; API signature valid; includes childId, skillId, issuedOn) Then the system creates a pending verification record and sets status=Under Review for that skill And when an admin approves, the skill is added to the child’s ledger with source=External and verifierId recorded; on rejection, a visible reason is returned to the submitter And the booking flow immediately reflects the new status/eligibility within 5 seconds of approval
Real‑Time Eligibility Update During Browsing
Given a parent is viewing the schedule filtered for a selected child When a new completion or approved external proof updates the child’s skills Then classes that become eligible move to the Eligible list and enable the Book CTA without page refresh within 5 seconds And classes that remain ineligible continue to show explicit missing prerequisites And the UI shows a non‑blocking notice that eligibility has been updated
Privacy & Consent Compliance for Minor Data
"As a privacy‑conscious parent, I want my child’s data handled securely and with consent so that I can trust the platform when booking classes."
Description

Implement parental consent capture and management for storing children’s personal data (DOB, grade), with clear purpose statements and granular controls. Encrypt sensitive fields at rest and in transit, restrict access via role‑based permissions, and minimize exposure in notifications. Support data subject requests (export/delete), region‑specific rules (e.g., COPPA/GDPR‑K), and retention policies. Perform age/eligibility calculations server‑side and log consent events for auditability.

Acceptance Criteria
Consent Gating Before Storing Minor Data
Given a parent adds a child profile When they enter DOB and/or grade and click Save Then a consent modal displays purpose statements and granular toggles for: store DOB, store grade, use for eligibility filtering, use for notifications Given required consents are not granted When Save is attempted Then the child profile is not created/updated and a blocking error is shown Given consent is granted When Save succeeds Then a consent record is persisted with parent ID, child ID, purposes granted/denied, policy version, locale/region, timestamp, and evidence of assent
Consent Management & Audit Logging
Given a parent views Consent Settings When they update or revoke any purpose Then the change is applied and a new immutable consent event is appended with actor, timestamp (UTC), IP, user-agent, purpose delta, and policy version hash Given an admin exports consent logs for a child When export is requested Then a JSON or CSV file is generated within 60 seconds containing the event history and checksums; no sensitive child fields beyond IDs are included Given consent is revoked for "use for eligibility filtering" When schedules are viewed Then AgeSmart filtering is disabled for that child until consent is re-granted
Encryption and Retention for Minor Fields
Given data in transit When the parent submits child data Then the connection uses TLS 1.2+ with HSTS enabled Given data at rest When DOB/grade are stored Then the database stores them encrypted using a KMS-managed key; plaintext values are not readable at the storage layer or in backups/replicas Given log/analytics pipelines When events are emitted Then DOB/grade are never included in plaintext and are redacted or tokenized Given retention policy When a child profile has had no activity for 24 months Then DOB/grade and linked consent records are purged within 30 days, with purge events logged
Role-Based Access and Data Minimization
Given an authenticated user When accessing child details Then only Parent account owners can view full DOB/grade; Studio Admins and Instructors see age band (e.g., 8–10) and eligibility status only Given API requests When an unauthorized role requests DOB/grade fields Then the API returns 403 and omits those fields from responses Given UI displays When rendering class rosters Then DOB is never shown; only child's first name initial and age band are displayed
Notification Content Minimization
Given an SMS/email reminder is generated When templates are compiled Then placeholders for DOB and grade are disallowed and messages contain no DOB, grade, or exact age; only child initials and class name/time are included Given a user attempts to add a sensitive token to a template When they save Then validation blocks the change with a clear error and logs the attempt Given system-sent notifications When delivered Then content scanning confirms no sensitive fields are present; failed checks prevent send and raise an alert
Region-Specific Compliance Enforcement
Given the organization's region is United States and the child is under 13 When a parent submits data Then verifiable parental consent is required and consent records store the verification method type and outcome before data is saved Given the organization's region is in the EU/UK When a child is under the country-specific GDPR-K threshold Then parental consent is required and purpose statements reflect GDPR-K rights Given the region is changed When settings are saved Then the system re-evaluates existing child records and flags those needing updated consent, blocking processing until resolved
Server-Side Eligibility Calculation and Non-Disclosure
Given a child with DOB/grade stored When the schedule is requested Then age/grade eligibility is computed server-side and only eligibility flags and non-sensitive reason codes are returned to the client; DOB/grade are never returned Given time zone differences When eligibility spans a birthday at midnight local time Then the server uses the organization's time zone to compute age consistently Given a class is not a fit When reasons are shown Then the reason text does not reveal DOB/grade (e.g., "Requires age 8–10" rather than "Child is 7.9 years")

Guardian eConsent

One-time, reusable e-sign waivers and consents per child (liability, photo release, medical, policies) that auto-attach to future bookings. Supports dual-guardian signatures when required, expiry and renewal reminders, and step-up prompts inside reminder messages—so rosters stay compliant without last-minute paperwork.

Requirements

Child-Guardian Profile Linking
"As a guardian, I want to link my children to my account and save their consents so that I don’t have to re-enter information for every class booking."
Description

Implement a unified profile model that links each child to one or more guardians, enabling reusable consents per child across all future bookings. Support multiple children per guardian, primary/secondary guardian roles, verified contact methods (email/SMS), and deduplication by unique identifiers. Integrate with the booking flow to pre-fill known data, with APIs/imports to onboard existing rosters. Ensure multi-tenant isolation for studios, PII minimization, and consent-scoped visibility so that instructors see only what’s necessary for class operations.

Acceptance Criteria
Link Multiple Guardians to a Single Child
Given a child profile exists within a tenant And two distinct guardian profiles exist within the same tenant When both guardians are linked to the child Then the child record shows a guardians_count of 2 And each guardian record lists the child in linked_children And the linkage persists after page refresh and API read And an audit log entry is recorded with actor_id, child_id, guardian_ids, timestamp
Primary and Secondary Guardian Role Assignment
Given a child with at least two linked guardians When one guardian is set as Primary Then exactly one guardian for that child has role = "Primary" And all other linked guardians have role = "Secondary" And updating the Primary reassigns the previous Primary to "Secondary" atomically And the Primary guardian is used as the default contact in booking and reminder flows And role changes are captured in audit logs
Verify Guardian Contact Methods (Email and SMS)
Given a guardian has provided an email and mobile number When a verification email link is clicked Then the guardian.email_verified flag becomes true and verified_at is set And the system prevents reuse of the same token after first success When an SMS verification code is submitted correctly Then the guardian.sms_verified flag becomes true and verified_at is set And unverified contact methods are not used for consent requests or booking confirmations
Deduplication on Create and Import by Unique Identifiers
Given a guardian payload contains a tenant-scoped external_guardian_id or a previously verified email/phone When a create or import request is processed Then if a matching guardian exists in the same tenant, the record is updated (not duplicated) and duplicate_count remains 0 And if no match exists, a new guardian is created with a generated guardian_uid And no deduplication occurs across different tenants And the API/Importer response includes created, updated, and skipped counts with row_ids Given a child payload contains external_child_id When a matching child exists in the same tenant Then the system updates and links without creating a duplicate child profile
Booking Prefill and Consent Auto-Attach
Given a guardian starts a booking and selects an existing child When the booking form loads Then child profile fields and linked guardian contact details are prefilled and read-only where policy requires And all active consents for that child are auto-attached to the booking record And if any required consent is missing or expired, a step-up prompt blocks checkout until completion And upon successful checkout, the booking references child_id, guardian_id(s), and consent_ids
Roster Import/API Linking Existing Profiles
Given a CSV or API payload with children, guardians, roles, and linkage keys When the import job runs Then existing profiles are matched by external IDs or verified contacts and linked; new profiles are created otherwise And roles are applied per row (Primary/Secondary) with validation errors for invalid roles And the job returns a summary: created, updated, linked, errors with line numbers and reasons And re-importing the same file is idempotent (no duplicate records created)
Multi-Tenant Isolation and Consent-Scoped Visibility
Given two studios exist in separate tenants When an instructor from Studio A searches or views rosters Then no profiles, bookings, or consents from Studio B are returned by UI or API And instructors see only consent statuses and class-relevant PII (child first name + last initial, allergy flag, emergency contact name/phone) And full guardian contact details and non-class consents are hidden from instructor role And all access is logged with tenant_id, actor_id, and resource_id
Compliant eSignature & Audit Trail
"As a studio owner, I want compliant e-signatures with an audit trail so that I can confidently demonstrate consent validity if issues arise."
Description

Provide legally compliant e-signature capture for liability, photo release, medical, and policy forms, with support for typed signature, drawn signature, and checkbox attestations. Record a tamper-evident audit trail including timestamp, IP/device metadata, signer identity, consent version, and hash. Generate non-editable PDFs or immutable records stored with encryption at rest and in transit, with role-based access controls and downloadable proof for studios. Align with ESIGN/UETA principles and support per-tenant branding and language localization.

Acceptance Criteria
Multi-Modal Signature Capture (Typed, Drawn, Checkbox)
Given a guardian is signing a liability, photo release, medical, or policies consent When the guardian selects a signature mode (typed, drawn, or checkbox attestation) Then the corresponding input control is displayed and all non-applicable controls are hidden And submission is disabled until the required input is present (typed name not empty; drawn signature has minimum stroke count > 0; checkbox is checked) And the consent text for the selected document type and version is visible on the screen When the guardian submits the consent Then the system persists a signature payload containing signature_type, signature_value/reference, guardian_full_name, guardian_account_id, child_id, consent_version_id And the UI shows a success confirmation and prevents duplicate signing of the same consent_version_id for the same guardian/child
ESIGN/UETA eDisclosure and Affirmative Consent
Given a guardian initiates an electronic signing session When the ESIGN/UETA eDisclosure is presented Then signature inputs remain disabled until the guardian affirmatively consents via a required checkbox And the acceptance timestamp, IP address, user agent, and eDisclosure text/version are recorded When the consent is fully executed Then a copy is immediately available for download and a secure link is sent to the guardian via their chosen channel (email/SMS) And the system records that an electronic copy was provided to meet retention requirements
Tamper-Evident Audit Trail with Timestamp, IP/Device, Identity, Version, Hash
Given a consent has been executed When the record is persisted Then the audit trail includes ISO-8601 UTC timestamp, public IP, user agent string, device/app identifiers (if available), guardian_account_id, child_id, and consent_version_id And the system computes a SHA-256 (or stronger) hash over the canonicalized consent content and stores it alongside the record Then any update attempt creates a new immutable version; the original hash and audit entries remain unchanged And an integrity verification process can recompute the hash and returns Pass when the record is unaltered
Immutable PDF Generation and Non-Editable Record
Given a consent is completed When generating the proof artifact Then a PDF is produced that includes tenant brand header, document title, consent text/version, signer name(s), signature representation, UTC and local timestamp, IP, device metadata, unique document ID, and content hash And the PDF is flattened to be non-editable and saved with no-edit permissions Then the canonical consent record is stored immutably; any later change results in a new record linked by version lineage
Encryption In Transit and At Rest
Given any consent or audit data is transmitted When a client connects to the service Then TLS 1.2+ is enforced and weaker protocols/ciphers are rejected and logged And downloadable proof links are time-limited, single-use, and require authorization When data is stored at rest Then all consent artifacts and audit data are encrypted with AES-256 (or equivalent) and keys are managed and rotated by a KMS
Role-Based Access Control and Downloadable Proof for Studios
Given a studio user requests access to a consent record When the user has role Owner or Compliance Then they can view the record details, full audit trail, and download the PDF proof When the user has role Instructor Then they can see consent status (valid/expired/version) only, without access to PII or the PDF download Then all access attempts are authorized and logged with user ID and timestamp, and unauthorized roles receive 403 without data leakage
Per-Tenant Branding and Language Localization on Consent
Given a tenant has configured brand assets and a primary language When a guardian views and signs a consent Then the UI and generated PDF display the tenant logo/name/colors and the consent text in the selected language And the signed record is bound to a language-specific consent_version_id; if a translation is unavailable, the defined default language is used with a visible notice Then UI dates/times/numerals follow the viewer locale while the audit trail stores canonical UTC values
Consent Template Management & Versioning
"As an administrator, I want to version and assign consent templates to specific classes so that the right forms are always collected without manual setup each time."
Description

Enable admins to create, brand, and manage consent templates (liability, photo, medical, policies) with dynamic fields, localization, and per-class/category assignment. Support versioning so new signings always use the latest template while preserving historical versions for previously signed records. Allow configuration of required/optional status, dual-guardian requirement, and default expiry duration per template. Provide preview, publish, and rollback controls with change logs.

Acceptance Criteria
Admin creates and publishes a branded liability template with dynamic fields
Given an admin is on the Consent Templates page When they create a new Liability template and configure branding (logo, colors, header/footer) Then they can insert only supported dynamic fields {ChildName}, {GuardianName}, {ClassName}, {Date}, {StudioName} And When the template body contains an unknown placeholder Then Publish is disabled and an inline error lists the invalid tokens And When Title, Body, and at least one locale are completed Then Preview renders with brand styles and placeholder sample values And When Publish is clicked Then the template is saved as Version 1 with status Active and a Template ID is generated
Template editing creates new version and preserves historical signings
Given Template T (v1 Active) has 10 signed consents linked to v1 When an admin edits T and publishes Then a new version v2 is created and set Active; v1 moves to Archived And New signing sessions reference v2 automatically And Existing signed records remain bound to v1 content and metadata (read-only) And The admin can view and export the full v1 content from the audit log
Localization and language fallback on templates
Given a template has locales en-US (default) and es-ES When a guardian with language preference es-ES opens the consent Then the es-ES content is displayed And When a guardian with language preference fr-FR (unsupported) opens the consent Then en-US is displayed with a language selector listing en-US and es-ES And Publish is blocked if the default locale is missing required fields (title/body)
Per-class/category assignment and required vs optional enforcement
Given Template T1 (Required) is assigned to Category "Youth Soccer" and Template T2 (Optional) is assigned to Class "Clinic A" When a booking is initiated for "Clinic A" Then the consent flow includes both T1 and T2; T1 blocks completion until signed; T2 can be skipped And When the assignment of T1 to the Category is removed Then new bookings for classes under "Youth Soccer" no longer include T1 And Changes to assignments do not alter or invalidate previously signed consents
Dual-guardian signature requirement workflow
Given Template T3 is configured with Dual-Guardian = Required When Guardian A completes their signature Then the consent status becomes "Pending Co-signature" and a co-sign link can be sent via email/SMS to Guardian B And When Guardian B signs via the link Then the consent status becomes "Completed" and two distinct signature records (A and B) with timestamps are stored And When Dual-Guardian is Disabled on a template Then a single valid signature marks the consent as "Completed"
Default expiry duration application
Given Template T4 has a default expiry of 12 months When a consent is completed on 2025-01-10 Then its expiry date is set to 2026-01-10 23:59:59 in the template's timezone And When the default expiry is changed to 18 months on 2025-03-01 Then consents completed before that date retain 12-month expiry; consents completed after use 18-month expiry And Expired consents are flagged as "Expired" and excluded when filtering for Valid consents
Rollback and change logs for template versions
Given Template T has versions v1, v2, v3 (v3 Active) When an admin selects "Rollback to v2" and confirms Then v2 becomes Active and v3 moves to Archived; new signings use v2 And The change log records actor, timestamp, action = Rollback, from-version = v3, to-version = v2, and a diff summary of content/settings And Change log entries are immutable and exportable to CSV with filters by template, version, actor, and date range
Dual-Guardian Signature Flow
"As a guardian, I want an easy way to invite the second guardian to sign electronically so that we can complete required consents without paperwork or schedule coordination."
Description

Support workflows that require two guardian signatures for a child, including sequential or parallel invites via email/SMS, unique secure links, and real-time completion status. Handle partial completion, reminder cadence, and escalation rules. Validate identity per signer, capture separate audit trails, and mark a consent complete only when both signatures are recorded. Provide fallback administrator overrides with attestation and audit logging when policy allows.

Acceptance Criteria
Configurable Parallel or Sequential Invitations
Given a consent that requires two guardian signatures and both guardians’ contact details exist, When Parallel mode is selected and the request is initiated, Then unique invite links are sent to both guardians via all enabled channels (email and/or SMS) within 60 seconds and each guardian’s status shows as Pending. Given a consent that requires two guardian signatures and Sequential mode with Guardian A first is selected, When the request is initiated, Then only Guardian A receives the invite, and upon Guardian A’s completion the system sends Guardian B’s invite within 60 seconds. Given Sequential mode is enabled, When Guardian A has not completed within the configured wait window, Then Guardian B is not invited until Guardian A completes or an admin executes an allowed override, and the system continues reminders per cadence. Given any mode is selected, When one guardian completes, Then overall progress shows 1/2 and the consent remains Pending until both guardians complete.
Secure Unique Signer Links and Expiry
Given a dual-signature consent, When invites are generated, Then each guardian receives a unique, cryptographically random, single-use link bound to the child, consent type, and guardian that expires after the configured TTL (default 7 days). Given a valid signer link, When the guardian completes their signature, Then subsequent visits to that link display a Completed state and do not permit an additional signature. Given an expired or revoked link, When it is accessed, Then signing is blocked and the user is offered a Request New Link action that issues a fresh link and records an audit event. Given two guardians, When Guardian B attempts to use Guardian A’s link, Then the system prevents signing for Guardian B and instructs them to use their own link without exposing PII.
Real-Time Completion Status Visibility
Given a roster or booking page listing children requiring dual consent, When a guardian completes a signature, Then the child’s consent status indicator updates from 0/2 to 1/2 or from 1/2 to 2/2 within 5 seconds without page refresh and the activity feed records the event. Given staff view a child’s consent status, When they open the status details, Then they can see which guardian signed (displaying non-sensitive identifier), timestamp, channel used, and which guardian is outstanding. Given export or reporting is run after a signature event, When the export is generated, Then the reported per-child consent counts and timestamps match the UI within the same business day.
Completion Gate After Two Signatures
Given a booking that requires a dual-guardian consent, When only one guardian has signed (1/2), Then the consent remains in Pending state and is not auto-attached as Complete to the booking or child profile. Given both guardians have signed (2/2), When the second signature is recorded, Then the consent status changes to Complete, auto-attaches to the child’s profile and all future bookings for that consent type until expiry, and an audit/event entry is created. Given a child has 1/2 signatures, When staff attempt to manually mark the consent as complete outside the override flow, Then the action is blocked with a message directing them to obtain the second signature or use the admin override pathway if policy allows.
Partial Completion Reminders and Escalation
Given one guardian has signed and the other has not, When 24 hours elapse after the first signature, Then the system sends a reminder to the outstanding guardian via all enabled channels including their secure link. Given the consent remains at 1/2, When the configured cadence triggers (e.g., Day 3 and Day 7), Then reminders are sent on schedule, deduplicated across channels, and cease immediately upon completion. Given class/session reminder messages are scheduled while consent is incomplete, When the reminder is sent, Then a step-up prompt with the outstanding guardian’s secure link is included without exposing the other guardian’s personal details. Given an escalation policy is configured, When the final reminder window passes without completion, Then an escalation notification is sent to administrators/staff with a summary of attempts and next-step options (resend, contact, override) and the event is logged.
Per-Signer Identity Verification
Given a guardian opens their invite link via SMS, When they begin the signing process, Then the system requires a one-time passcode sent to that phone number and only permits signing upon correct entry within 5 minutes and no more than 5 attempts. Given a guardian opens their invite link via email, When they begin the signing process, Then the system requires email link authentication or a verification code sent to that email before allowing signing. Given the signer-provided name does not match the guardian name on file, When they attempt to submit the signature, Then the submission is blocked with a clear error, a correction prompt is shown, and an audit record of the mismatch attempt is created. Given identity is verified, When the signature is submitted, Then the audit trail records the verification method, timestamp, IP, and user agent, and the signature is associated to the correct guardian record.
Administrator Override with Attestation and Audit Trail
Given policy allows overrides for a consent type, When an administrator marks a dual-guardian consent as complete without both signatures, Then the system requires selection of an allowed reason, a free-text attestation of at least 20 characters, an optional file attachment, and a confirmation checkbox before proceeding. Given an override is recorded, When viewing the consent record, Then an Overridden badge is displayed, the activity log shows the admin user, timestamp, reason, and any attachment, and the child is treated as compliant for that consent type with source=Override. Given overrides are disabled by policy for a consent type, When a user attempts to override via UI or API, Then the action is blocked with a policy error and an audit entry is created. Given an override exists, When a pending guardian later provides a valid signature, Then the consent transitions to Completed (2/2), the override is preserved in the audit trail as historical, and the current source reflects Signatures.
Auto-Attach & Step-Up Prompts
"As a parent booking a class, I want to be prompted to sign only what’s missing so that I can finish quickly and avoid surprises at check-in."
Description

Automatically detect missing or expired consents during booking and attach existing valid consents to the reservation. If consents are missing, trigger step-up prompts embedded in confirmation and reminder emails/SMS with deep links to a mobile-friendly signing flow. Support inline prompts on the booking page and QR codes at check-in, updating roster compliance in real time without staff intervention.

Acceptance Criteria
Auto-Attach Valid Consents During Booking
Given a guardian is booking a class for a child who has existing, unexpired consents on file for all required consent types When the guardian completes the booking flow Then the system auto-attaches the valid consents to the reservation prior to rendering the booking confirmation And no consent prompts are displayed in the booking UI And the booking confirmation shows the child as “Compliant” for consents And an audit entry records which consents were attached, by ID and timestamp, without creating duplicates
Detect Missing/Expired Consents with Inline Booking Prompts
Given a guardian is booking a class for a child who is missing one or more required consent types or has expired consents When the guardian reaches the review/confirmation step of the booking Then the UI displays an inline prompt listing the specific missing/expired consent types per child And the booking can be completed with the roster status set to “Pending Consents” for the affected child(ren) And the system schedules step-up prompts for each missing/expired consent type And no prompts are shown for consent types already valid
Step-Up Prompts in Confirmation and Reminder Messages (Deep Links)
Given a booking contains a child with missing/expired consents and the guardian has valid email and/or SMS contact info When the booking confirmation and subsequent reminders are sent Then the messages include a clear call-to-action with a unique, signed deep link per child and consent type And the deep link opens a mobile-friendly signing flow pre-filled with the child and class context And the link is single-use, time-bound (expires after first completion or after policy-defined TTL), and prevents replay And upon successful signing, the consent is attached to the booking and future bookings, the roster updates to “Compliant” in real time, and further prompts for that consent are suppressed
QR Code Consent Capture at Check-In
Given a child arrives to check-in with missing/expired consents When staff displays the class check-in screen Then a QR code is available that opens the guardian’s mobile signing flow for the specific child and required consent types And upon successful signing, the roster updates to reflect compliance within 15 seconds without staff intervention And the check-in screen refreshes automatically to show the updated compliant status And duplicate or already-valid consents are not re-requested
Dual-Guardian Signature Workflow
Given a class requires dual-guardian signatures for specified consent types When the first guardian completes their signature via inline prompt, deep link, or QR flow Then the system requests the second guardian’s signature via a unique deep link or inline prompt addressed to the second guardian And the booking remains “Pending Dual Signatures” until both signatures are captured And once both signatures are completed, the consent is marked “Compliant,” prompts stop, and the roster updates in real time And all partial and final signature events are logged with guardian identity, timestamps, and consent IDs
Multi-Child Family Booking Handling
Given a guardian books for multiple children where at least one child has valid consents and at least one child has missing/expired consents When the booking is created Then valid consents auto-attach only to the applicable child(ren) And prompts are generated only for the child(ren) with missing/expired consents And confirmation/reminder messages include separate deep links scoped to each child and required consent types And the roster shows per-child compliance status, allowing a mix of “Compliant” and “Pending Consents” within the same booking
Expiry, Renewal & Reminder Engine
"As an operations manager, I want automatic consent renewal reminders so that our rosters remain compliant without last-minute chasing."
Description

Allow per-template expiry rules (e.g., 12 months) and automatically schedule renewal reminders for guardians with configurable lead times and channels (email/SMS). On expiry, prevent check-in or booking completion per studio policy, with clear guidance and one-tap renewal links. Preserve historical records and display consent validity windows on profiles and rosters to keep compliance current.

Acceptance Criteria
Template Expiry Configuration and Calculation
Given a consent template has an expiry rule (e.g., 12 months from last signature) When a guardian completes the consent for a child Then the system computes and stores the expiry date based on the rule in the studio’s timezone Given the same template is renewed When the final required signature is captured Then the expiry date is recalculated from that timestamp Given a template without an expiry rule When a consent is signed Then the consent is marked No Expiry and never schedules renewal reminders Given timezone and daylight saving changes When displaying expiry information Then dates are normalized to the studio timezone and formatted per studio locale
Configurable Renewal Reminder Scheduling (Email/SMS)
Given a template has renewal lead times configured (e.g., 30 and 7 days before expiry) and channels selected (email/SMS) When a consent’s expiry approaches Then reminders are scheduled at those offsets via the selected channels Given a guardian renews before a scheduled reminder When the reminder job executes Then the reminder is skipped and logged as canceled due to renewal Given both email and SMS are enabled and contact details exist When sending reminders Then both channels are used; if one channel is unavailable, the other is used Given studio and guardian opt-out preferences When scheduling or sending reminders Then opt-outs are honored and the suppression reason is recorded Given a send attempt fails transiently When retry policy applies Then the system retries per policy and records final delivery status
Step-up Prompts in Class Reminder Messages
Given a child has a required consent that will expire before the date/time of an upcoming booked class When the automated class reminder is generated Then the message includes a step-up prompt with a one-tap renewal link for that consent Given multiple required consents exist and only some are expiring When generating the reminder Then only the expiring consents are listed in the prompt Given the consent is renewed before the reminder send time When the reminder is sent Then the step-up prompt for that consent is omitted
Policy-Based Blocking on Booking and Check-in
Given the studio policy is set to block booking completion on expired/missing required consents When a guardian attempts to complete checkout Then checkout is blocked with a clear message naming the missing/expired consents and providing a one-tap renewal link Given the studio policy is set to block check-in but allow booking When staff attempts to check-in a child with expired/missing consents Then check-in is prevented with guidance and a quick-send renewal link to the guardian Given the studio policy is set to warn only When booking or check-in occurs with an expiring (but not expired) consent Then the action proceeds and a warning is displayed and logged
One-Tap Renewal Deep Link Flow
Given a renewal reminder or step-up prompt is sent When the guardian taps the renewal link Then a secure, tokenized page opens showing the child and required consent template(s) ready to sign without login Given the consent template has an updated version When the guardian opens the link Then the latest version is presented and captured as a new record upon signing Given the guardian completes the renewal When the final signature is recorded Then the system confirms renewal, updates the expiry, and redirects back to the originating context (e.g., booking/check-in) when applicable Given the token is expired or invalid When the link is opened Then an error is shown with an option to request a fresh link
Dual-Guardian Renewal Workflow and Expiry
Given a consent template requires two guardian signatures When the first guardian signs via the renewal link Then the consent status is Pending Second Signature and the second guardian is notified per studio settings Given both signatures are required When the second guardian signs Then the consent status becomes Active and the expiry date is calculated from the timestamp of the second signature Given only one signature is received by the expiry date When a booking or check-in is attempted Then policy-based blocking and messaging are enforced until both signatures are on file
Validity Windows on Profiles and Rosters with Audit History
Given a child has active and historical consents When viewing the child profile Then each consent shows status (Active, Expiring Soon, Expired, Missing), validity window (effective and expiry dates), and template version Given a class roster is opened When it loads Then each child displays consent status badges and a quick filter for Missing/Expired is available Given renewals occur over time When viewing consent history or exporting records Then all prior consent records and artifacts are preserved as immutable, time-stamped entries without overwriting historical data
Roster Compliance Dashboard & Actions
"As an instructor, I want a roster view that highlights missing consents so that I can resolve issues before class starts and avoid delays at check-in."
Description

Provide instructors and staff with a real-time roster view showing each child’s consent status (complete, missing, expired, pending dual signature), with filters, bulk resend of links, and export. Integrate compliance checks into attendance and check-in flows, including optional hard blocks for non-compliant attendees. Log all actions for audit and provide quick-view access to signed records when needed on-site.

Acceptance Criteria
Real-Time Compliance Roster View and Filters
Given an upcoming class session with enrolled children and stored eConsents, When the roster is opened, Then each child displays a compliance badge of one of [Complete, Missing, Expired, Pending Dual Signature]. Given a new consent is submitted or expires, When the roster is open, Then the affected child’s badge updates within 5 seconds without a full page reload. Given status filter controls, When a user filters by one or more statuses, Then only matching children are shown and the visible count equals the number of rows displayed. Given a search by child name, When a search term is entered, Then results are narrowed to names containing the term (case-insensitive) combined with any active status filters. Given sorting controls, When sorting by name or status is applied, Then the roster reorders accordingly and the sort persists while navigating within the session.
Bulk Resend Consent Links
Given selected children with Missing, Expired, or Pending Dual Signature status, When the staff clicks Resend Links, Then consent request messages are sent to all required guardians for those children (including both guardians when dual signature is required). Given the resend is triggered, When sending completes, Then the UI shows a summary with counts for Sent, Skipped (already Complete), and Failed, including failure reasons per guardian. Given organizational rate limits, When bulk resend is executed, Then the system does not exceed 200 messages per minute per organization and queues remaining sends without data loss. Given a bulk resend, When messages are queued or sent, Then an audit log entry is created with initiator, recipients, class/session context, and timestamp.
Export Roster Compliance Data
Given any active roster filters and search, When the user clicks Export, Then a CSV is generated within 10 seconds containing only the currently filtered children. Given the CSV is generated, Then it contains columns: class_id, class_name, session_start_at (ISO 8601 with timezone), child_id, child_name, compliance_status, requires_dual_signature (boolean), consents_expire_at, guardian_primary_name, guardian_primary_email, guardian_secondary_name, guardian_secondary_email, last_request_sent_at, last_signed_at, check_in_status. Given the export completes, Then the CSV row count equals the number of rows currently displayed on the roster and a secure download link is provided that expires after 24 hours. Given an export occurs, Then an audit log entry is recorded including filter parameters and exported row count.
Compliance Gate in Attendance Check-In (Soft Warning and Hard Block)
Given the setting Hard block non-compliant check-in is disabled, When staff attempts to check in a child with Missing or Expired status, Then a modal warning explains the issue and offers Proceed anyway and Send Link options; selecting Proceed anyway allows check-in and records a warning event; selecting Send Link triggers a consent request to the required guardian(s) and logs the action. Given the setting Hard block non-compliant check-in is enabled, When staff attempts to check in a non-compliant child, Then check-in is prevented and the modal displays an Override option only to users with role Manager. Given a Manager selects Override, When a reason of at least 10 characters is entered, Then check-in completes and an audit entry captures the override with user, timestamp, IP, and reason. Given a compliant child, When check-in is attempted, Then check-in completes with no warning within 1 second on a typical broadband connection.
Quick-View Access to Signed Consent Records On Site
Given a child with completed consents, When staff taps the compliance badge from the roster or check-in screen, Then a side panel opens within 2 seconds showing latest signed versions for liability, photo, medical, and policies. Given the quick-view panel is open, Then each record displays guardian name, signature method, signed_at (timestamp), IP address, user agent, and a secure view link to the full record. Given role-based access control, When a user without Instructor or Manager role attempts to open quick-view, Then access is denied and the attempt is logged. Given a quick-view is opened, Then an audit event is recorded with actor, child, class/session, and document types viewed.
Dual-Guardian Signature Status and Expiry Handling
Given a class requires dual-guardian signatures, When only one guardian has signed, Then the roster status shows Pending Dual Signature and the child is treated as non-compliant for check-in per settings. Given consent expiry has passed, When the roster or check-in screen loads, Then the status displays Expired and the child is treated as non-compliant until renewal is completed. Given a renewed consent is submitted, When processing completes, Then the child’s status updates to Complete immediately and the previous version remains accessible in the audit trail. Given multiple consent types are required, When any required consent is Missing or Expired, Then overall status is non-compliant and reflects the most restrictive state.
Comprehensive Audit Logging for Compliance Actions
Given any of the following occurs: roster view, filter change, bulk resend initiation/completion, export initiation/completion, check-in attempt, warning shown, block enforced, override used, quick-view opened, record viewed, When the action completes, Then an immutable audit event is stored with fields: event_type, actor_id, actor_role, org_id, class_id, session_id, child_id, guardian_id(s), timestamp (UTC ISO 8601), client_ip, user_agent, outcome, and metadata. Given audit retention policy set to 7 years, When events age, Then they are retained for at least 7 years and are exportable by users with Manager role. Given an audit export request for up to 100,000 events, When the export is started, Then a CSV is produced within 30 seconds; if larger, Then the job is queued and a secure download link is emailed upon completion; both actions are logged.

Household Wallet

Share payment methods across guardians with optional spend caps per child, approval rules for higher-cost items, and automatic receipt tagging by child for reimbursements. Works with Apple Pay/Google Pay for one-tap checkout, keeping payments trusted, fast, and organized for split households.

Requirements

Household Linking & Roles
"As a guardian, I want to link another guardian and our children under one wallet so that we can both manage payments and rules without duplicating profiles or methods."
Description

Enable creation of a Household Wallet that links multiple guardians to one or more child profiles. Support role-based access (Owner, Co‑Guardian, Payer‑Only) with distinct permissions for adding/removing payment methods, setting spend caps, and approving purchases. Provide invitation flows via email/SMS, secure acceptance with identity verification, and the ability to merge existing ClassTap child/student profiles. Integrate with existing booking accounts so households can be selected during checkout. Handle edge cases such as guardian removal, transfer of ownership, and split households across studios while preserving data privacy. Ensure data model supports multiple households per guardian and per child without double-booking or misattribution.

Acceptance Criteria
Household Wallet Creation and Guardian–Child Linking
Given a signed-in guardian with a ClassTap account When they create a Household Wallet, provide a household name, and add one or more existing or new child profiles Then the wallet is created with a unique ID, the guardian is assigned Owner role, and the selected children are linked to the wallet And the operation is atomic: all intended links succeed or the transaction is rolled back with an error And an audit record is stored capturing initiator, timestamp, household ID, and child IDs
Role-Based Permissions: Owner, Co‑Guardian, Payer‑Only
Given a guardian’s role within a Household Wallet When they attempt an action (invite/remove guardian, add/remove payment methods, set spend caps, approve purchases, transfer ownership, delete household) Then permissions are enforced as: Owner can perform all listed actions; Co‑Guardian can add payment methods, set caps, approve >cap purchases, and invite guardians but cannot remove Owner, delete household, or transfer ownership; Payer‑Only can book and pay using permitted household methods but cannot add/remove payment methods, set caps, invite/remove guardians, approve >cap purchases, transfer ownership, or delete household And unauthorized attempts return HTTP 403 and are logged with user ID, role, action, and timestamp
Guardian Invitation via Email/SMS with Secure Acceptance
Given an Owner or Co‑Guardian initiates an invitation by entering an email or mobile number and selecting a role When the invitation is sent Then the system delivers a message containing a unique single‑use tokenized link that expires in 24 hours When the invitee opens the link Then they must verify via OTP sent to the same channel and sign in or create a ClassTap account tied to that email/number And on successful verification, the guardian is added to the household with the selected role; otherwise the invite remains pending or is invalidated on expiry/use And all events (sent, opened, verified, accepted, expired) are captured in an audit trail
Merge Existing Child Profiles into Household Wallet
Given duplicate or existing ClassTap child profiles are detected or selected for merge When the Owner initiates a merge from the household management flow and confirms the canonical profile Then the system consolidates profiles into one child record, preserving and unifying past payments, enrollments, waitlists, and future bookings And any conflicting future bookings are surfaced and require explicit user resolution; no duplicate bookings are created And all merges are logged with before/after IDs and are non-destructive to underlying transaction records
Household Selection and Child Attribution at Checkout
Given a user belongs to one or more households and has access to one or more children When they proceed to checkout for a class Then the UI requires selecting a child and a household wallet (defaulting to the last used for that studio) before payment can be submitted And the charge is processed against the selected household’s payment method and the transaction is tagged with the selected child And receipts, booking records, and reports display household and child attribution, and double‑booking prevention validates across all of the child’s households
Per‑Child Spend Caps and Approval Workflow
Given per‑child monthly spend caps and optional approval thresholds are configured by an Owner or Co‑Guardian When a purchase would exceed a child’s remaining cap or meets the approval threshold Then the booking is placed on hold and an approval request is sent to Owner/Co‑Guardian via configured channels (email/SMS/push) And only Owner/Co‑Guardian can approve or deny; Payer‑Only approvals are rejected with HTTP 403; all decisions are timestamped and audited And if approval is granted within the hold window (configurable, default 24 hours) the payment is captured and booking confirmed; otherwise the hold is released, no charge is captured, and the reservation is canceled with notification
Guardian Removal, Ownership Transfer, and Multi‑Household Data Isolation
Given an Owner manages household membership and ownership When removing a guardian Then the guardian’s access to the household, payment methods, and approvals is revoked immediately; pending approval requests are canceled or reassigned; historical records remain intact and auditable And the last remaining Owner cannot be removed without first transferring ownership and obtaining the new Owner’s acceptance When transferring ownership Then the recipient must accept before the transfer completes; until then the current Owner retains control And both guardians and children may belong to multiple households without cross‑household visibility of payment methods, approvals, or activity; studios only see data pertinent to their classes for the selected household/child
Shared Payment Method Vault
"As a guardian, I want to add a payment method once and let authorized co‑guardians use it so that checkout stays fast and secure across all our devices."
Description

Implement a secure, tokenized payment vault at the household level that allows approved guardians to use shared payment methods (cards, ACH where supported, Apple Pay, Google Pay). Support device‑bound network tokens for Apple Pay/Google Pay while maintaining household‑level authorization controls. Allow defaults per guardian and per household, last‑4 display, and nickname/labeling. Enforce PCI‑DSS and SCA/3DS flows, retry logic, and graceful fallbacks to add a new method. Expose APIs/UI to restrict which guardians can use which methods. Ensure methods can be scoped to certain studios or global, and log all usage for auditability.

Acceptance Criteria
Create and Tokenize Shared Payment Method in Household Vault
Given household H exists and guardian G has Manage Payment Methods permission When G adds a new card via a PCI-compliant hosted field/iframe and completes required verification Then only the PSP token and non-sensitive metadata (brand, last4, expiry month/year) are stored and no PAN/CVV is persisted in DB or logs And the method is associated to household H and becomes available for authorization-scoped use And the method is displayed with a user-provided nickname and masked format "Brand •••• last4" And for ACH (where supported), the account is added only after bank verification succeeds (micro-deposit or instant) and is tokenized before use
Guardian-Specific Authorization for Shared Methods
Given household H has guardians G1 and G2 and shared method M exists in H And G1 is allowed to use M and G2 is not When G1 opens checkout in any ClassTap booking flow Then M is displayed as a selectable payment method for G1 When G2 opens checkout Then M is not displayed to G2 When G2 attempts to charge using M via API Then the API returns 403 Unauthorized and no authorization is sent to the PSP And admin UI and API allow toggling guardian access to M and changes propagate within 5 seconds
Set and Apply Default Payment Methods per Household and Guardian
Given household H has methods M1 and M2 and guardians G1 and G2 And household default is set to M1 And G1 personal default is set to M2 When G1 opens checkout Then M2 is preselected for G1 When M2 becomes invalid (expired or removed) Then G1's default falls back to household default M1 And if no defaults are configured, no method is preselected And default selections persist across sessions and devices within 5 seconds of change
Use Apple Pay/Google Pay with Device-Bound Tokens Under Household Controls
Given guardian G1 on device D1 adds an Apple Pay/Google Pay method A1 to household H Then A1 is stored as a device-bound network token linked to D1 and household H When G1 checks out on D1 Then the Apple Pay/Google Pay button is shown and A1 can be charged successfully When G1 checks out on a different device D2 without A1 provisioned Then the Apple Pay/Google Pay button is hidden unless another eligible device token exists for D2 When guardian G2 (not authorized for A1) checks out on any device Then A1 is not available to G2 and API attempts to use A1 return 403 Unauthorized
Scope Payment Method to Selected Studios and Filter at Checkout
Given household H has method M scoped to studios S1 and S2 only When a guardian checks out for a class at studio S1 or S2 Then M is displayed as a selectable method for that checkout (subject to guardian authorization) When a guardian checks out for a class at studio S3 Then M is not displayed When the charge API is called with M for studio S3 Then the API returns 403 ScopeViolation and no authorization is attempted And default selections never auto-select M where it is out of scope
Handle SCA/3DS Challenges with Retry and Add-Method Fallback
Given a card method M in household H requires SCA under issuer rules When a guardian initiates payment with M Then a 3DS challenge flow is initiated and must complete before authorization And on successful challenge, the payment proceeds per studio capture settings And on failure or timeout, the user is offered one retry with M And after two failed attempts, the user is prompted to select another method or add a new method via secure entry without losing checkout context And API responses reflect outcome states (ChallengeRequired, ChallengeFailed, Retried, Success)
Comprehensive Audit Logging of Payment Method Lifecycle and Usage
Given audit logging is enabled for household H When any of the following occurs: add/update/remove method, label change, default change, guardian authorization change, scope change, payment attempt, SCA challenge outcome Then an immutable audit record is written within 2 seconds containing: event type, UTC timestamp, householdId, actor guardianId (if any), methodId (token ref), studioId (if applicable), outcome (success/failure + code), requestId, and IP hash And no PAN/CVV or unmasked token values are stored in logs And authorized admins can query logs via API by time range, event type, methodId, guardianId, or studioId And records are retained for at least 24 months
Per‑Child Spend Caps & Time Windows
"As a guardian, I want to set per‑child spending limits with time windows so that we can control our budget without manually tracking every booking."
Description

Provide configurable spend caps per child by transaction, daily, weekly, monthly, and custom date ranges. Allow optional category filters (class fees, passes/packages, add‑ons) and studio scoping. Enforce caps automatically at checkout with clear messaging, pre‑authorization checks, and cumulative tracking that resets on schedule. Include warnings when approaching limits, pro‑rate or exclude taxes/fees based on configuration, and update cap usage on refunds, cancellations, and no‑shows. Ensure caps are considered for waitlist auto‑promotions and recurring bookings, with deterministic outcomes and no partial overages.

Acceptance Criteria
Enforce Per-Child Caps at Checkout Across Time Windows
Given a household wallet with per-child caps configured for transaction, daily, weekly, monthly, and custom date ranges And the child’s cumulative usage is tracked across all guardians in the household And the cart contains line items for that child When a guardian attempts to checkout Then the system computes the applicable amount for each active cap window before payment authorization And blocks checkout if adding the order would exceed any cap And shows a blocking message that states the cap type, remaining amount, and the next reset timestamp And does not permit partial overages, split tenders, or edits that bypass the cap in a single checkout flow And logs the decision with cap IDs, window boundaries, computed amounts, and a deterministic rule ID And if no caps are configured for the child, checkout proceeds without cap checks
Pre-Authorization Cap Check with Apple Pay/Google Pay
Given Apple Pay or Google Pay one-tap checkout is selected When the user initiates the payment sheet Then a cap pre-check is performed before authorization or token capture And if any cap would be exceeded, the payment sheet is dismissed and an in-app error explains the cap and remaining amount And if within caps, the payment authorization proceeds and the result is consistent with the pre-check And the event is recorded with payment method and pre-check outcome
Cumulative Tracking and Scheduled Resets with Timezone
Given a cap window configured with a specific timezone (e.g., America/New_York) And the child has usage recorded within the current window When the window boundary is reached (e.g., daily at 00:00, weekly Monday 00:00, monthly on the 1st 00:00, or end of custom range) Then the cap usage resets to $0 for that window at the exact boundary in the configured timezone And a reset event is written to the audit log with timestamp and window definition And new charges after the boundary are accumulated only in the new window And historical windows remain immutable for reporting
Approaching-Limit Warnings and Messaging
Given a warning threshold of 80% of cap (default) unless configured otherwise When the child’s cumulative usage crosses the threshold within a window Then the cart and checkout surfaces display a non-blocking warning showing remaining amount and next reset And a notification is sent according to wallet preferences (in-app/SMS/email) And warnings are throttled to show at most once per 24 hours per threshold unless usage increases by ≥5 percentage points or reaches 100% And at 100% of the cap, checkout is blocked with a clear error as defined in enforcement
Category/Studio Scoping and Tax/Fee Handling
Given a cap configured to apply to specific categories (e.g., class fees, passes, add-ons) and scoped to selected studios And the cart contains multiple line items across categories and studios When calculating the amount counted toward the cap Then only eligible line items within the configured categories and studios are included And taxes/fees are included, excluded, or pro-rated according to the cap’s configuration And the system displays a breakdown of counted vs. excluded amounts on request And checkout is blocked only if the counted amount would exceed the cap
Adjustments on Refunds, Cancellations, and No-Shows
Given an order previously counted toward a cap When a full refund is processed Then the cap usage for the relevant window decreases by the counted amount and not below $0 When a partial refund is processed Then the cap usage decreases by the refunded portion of the counted amount When a cancellation occurs with a policy fee Then only the fee amount configured as cap-eligible remains in usage; otherwise usage is restored When a no-show fee is applied Then only the cap-eligible fee amount counts toward the cap And all adjustments are written to the audit log with references to the original charge
Cap Checks for Waitlist Auto-Promotions and Recurring Bookings
Given a child is on a waitlist with auto-promotion enabled When a spot opens and the system attempts to promote and charge Then a cap pre-check runs before charging; if over cap, no promotion or charge occurs and the guardian is notified with the reason And if within cap, the promotion and charge complete successfully and usage is updated Given a recurring booking with scheduled charges When each charge attempt occurs Then the cap pre-check runs per occurrence with deterministic outcomes; over-cap attempts are skipped and notified, and no partial overages or holds are created
High‑Value Purchase Approval Workflow
"As a guardian, I want high‑cost bookings to require my approval so that I can prevent unexpected charges while still allowing quick scheduling."
Description

Allow household owners to define approval rules based on thresholds, item types, studios, or classes. When a transaction triggers a rule, hold the seat for a configurable window, send approval requests via SMS/email/push to designated approvers, and allow one‑tap approve/deny with biometric confirmation where supported. Support multi‑approver logic (any‑one, all, fallback approver), expiration handling, and automatic release of holds on timeout. Persist an approval audit trail with timestamps, approver identity, and decision reason. Integrate with checkout so approval happens inline without losing cart state and so charges only capture upon approval.

Acceptance Criteria
Rule Configuration by Threshold and Context
Given I am a Household Owner on Wallet > Approval Rules When I create a rule with amount threshold >= 100.00, item types = ["Workshops"], studios = ["Downtown Studio"], classes = ["Advanced Pottery"], approvers = ["Alex"], logic = "Any-One", expiry window = 10 minutes Then the rule saves, is active by default, and is returned by the Rules API with an ID Given a cart contains a class from "Downtown Studio" named "Advanced Pottery" totaling 120.00 When rules are evaluated Then the newly created rule matches and is marked as triggered Given a cart totaling 80.00 or a different studio/class When rules are evaluated Then the rule does not trigger Given an inactive rule matching the transaction When rules are evaluated Then it is ignored
Inline Checkout Seat Hold and Timeout
Given a rule triggers during checkout When the buyer taps Pay Then inventory for the selected seat(s) is held immediately and the payment is authorized but not captured Given a rule-triggered hold with expiry window = 10 minutes When approval remains pending Then the checkout UI shows a countdown timer and prevents duplicate pay attempts Given the hold expires without approval When the timer reaches 0 Then the seat hold is released, the authorization is voided, the cart state is preserved, and the buyer is notified of expiration Given approval arrives before expiry When approval is recorded Then the seat hold is extended for capture processing up to 60 seconds and the checkout proceeds to confirmation
Multi-Approver Any-One and All with Fallback
Given a rule with approvers = ["Alex", "Jamie"], logic = "Any-One" When Alex approves Then the purchase is approved and Jamie receives a resolution notification Given a rule with approvers = ["Alex", "Jamie"], logic = "All" When only Alex approves before expiry Then the purchase remains pending and is auto-denied on expiry Given a rule with approvers = ["Alex", "Jamie"], logic = "All" When both approve before expiry Then the purchase is approved Given a rule with fallback approver = "Pat" and window = 10 minutes When no primary approver responds by minute 5 Then Pat is notified and can approve/deny before the original expiry
Approval Requests via SMS/Email/Push with One‑Tap Biometric
Given a rule triggers When notifications are sent Then SMS, email, and push are dispatched to designated approvers per their notification preferences within 5 seconds Given an approver opens the approval link on a device supporting biometrics When they tap Approve or Deny Then the action requires biometric confirmation and is recorded in under 2 seconds Given an approval link When it is used once Then subsequent attempts with the same token are rejected as already used Given an approval link with expiry tied to the window When accessed after expiry Then the action is blocked and the approver sees that the request has expired
Approval Audit Trail Persistence and Access
Given an approval event occurs When recording the audit trail Then the system stores rule ID, cart ID, child, approver identity, channel, decision (approve/deny/timeout), decision reason (if provided), timestamps (requested, responded), and device fingerprint Given an approved or denied checkout When viewing the booking details as the household owner Then the full audit trail is visible and exportable as CSV Given audit records When attempting to edit or delete them via UI or API Then the system prevents modification and logs the attempt
Payment Authorization and Capture on Approval Only
Given a rule triggers When the buyer initiates checkout with Apple Pay or Google Pay Then the system performs authorization only and defers capture until approval Given an approval is recorded before authorization expiry When capture is attempted Then the charge captures successfully and a receipt is issued tagged to the child Given the authorization has expired by the time of approval When capture is attempted Then the system fails the capture, notifies the buyer, releases inventory, and restores cart state without charging
Denial Handling and Automatic Release
Given any approver denies the purchase in Any-One or All mode When denial is recorded Then the seat hold is released immediately, the authorization is voided, and the buyer is notified with the denial reason Given no approver responds before expiry When timeout occurs Then the request status is set to "Timed Out", the seat hold is released, the authorization is voided, and the buyer is offered to re-request approval or change payment Given a hold was released due to denial or timeout When the buyer resumes checkout Then the cart contents and selections remain intact but inventory availability is re-checked
Automatic Receipt Tagging & Reimbursement Export
"As a guardian, I want receipts automatically tagged by child and exportable so that reimbursements are quick and well documented."
Description

Automatically tag all receipts and invoices with child, class/session, studio, and payer metadata. Surface a household receipts view with filters by child, date range, studio, and status (paid/refunded). Provide export to CSV/PDF and share‑via‑email for reimbursement or FSA/HSA submission, including itemized lines and approval references. Ensure tags persist through refunds/partial refunds and support custom memo fields per child. Integrate with existing email/SMS receipts and allow administrators to resend tagged receipts without exposing full payment details.

Acceptance Criteria
Automatic Tagging on New Purchase
Given a guardian completes a booking for one or more children using any supported payment method (card, Apple Pay, Google Pay) When the payment is successfully captured Then the generated receipt and invoice are automatically tagged with child_id(s), child_display_name(s), class_id, class_title, session_datetime, studio_id, studio_name, payer_id, payer_display_name, household_id, payment_method_type, and masked_payment_last4 And each line item is tagged with the corresponding child_id and class/session metadata And the tagged receipt is visible in the household receipts view and in the studio/admin ledger And the email/SMS receipt sent for the transaction includes the tags in the attached/linked receipt while masking full payment details
Tag Persistence Through Full and Partial Refunds
Given a paid receipt that is tagged with child, class/session, studio, and payer metadata When a full refund is issued Then the refund record inherits all original tags and the original receipt status changes to Refunded while preserving tags And the household receipts view reflects status=Refunded for the receipt and displays the refund date and amount Given a paid receipt with multiple line items tagged per child When a partial refund is issued for one or more line items Then negative refund line(s) are created and tagged to the same child and class/session as the original line(s) And the receipt status shows Partially Refunded with accurate net totals And exports (CSV/PDF) include both original and refund lines with correct signs and tags
Household Receipts View Filtering and Access Control
Given a guardian belongs to a household with multiple children and purchases across multiple studios When the guardian opens Household > Receipts Then the view defaults to the last 90 days, sorted by transaction date descending And filters are available for Child (multi-select), Date Range, Studio (multi-select), and Status (Paid, Refunded, Partially Refunded) When the guardian applies any combination of filters Then the results update within 1 second for up to 500 records and only matching receipts are displayed And the subtotal and count reflect the filtered results And the guardian only sees receipts for their own household; receipts from other households or studios are not visible
CSV Export with Itemization and Approval References
Given the guardian is viewing the household receipts list with filters applied When the guardian clicks Export CSV Then a CSV file is generated within 10 seconds containing one row per line item (including refund lines) And required columns include: Receipt ID, Line Item ID, Transaction Date/Time (ISO 8601), Child Name, Class Title, Session Date/Time, Studio Name, Payer Name, Status, Quantity, Unit Price, Tax, Line Total, Currency, Approval Reference (if present), Child Memo, Payment Method Type, Masked Last4 And numeric totals in the CSV match the UI totals for the same filtered set within ±$0.01 And for any transactions requiring approval, the Approval Reference column is populated with the captured approval id/code
PDF Export and Share via Email for Reimbursement/FSA
Given the guardian selects one or more receipts in the household receipts view When the guardian chooses Export PDF Then a single PDF is generated within 15 seconds containing an itemized receipt per transaction with child, class/session, studio, payer, status, approval reference (if present), and child memo And all payment details are masked (brand + last4 only) and no full PAN or token is displayed And the PDF is branded to the studio/white‑label configuration and includes totals, dates, and currency formatted to the household locale When the guardian chooses Share via Email and enters a valid email address Then the recipient receives the PDF (attached or secure link) within 2 minutes and the action is logged for the household
Custom Memo Fields per Child Included in Receipts and Exports
Given a guardian has entered a memo for a child in Household Wallet settings (max 140 characters, plain text) When the guardian completes a purchase that includes that child Then the child memo is associated to each applicable line item and is visible in the household receipts view And the memo appears on the emailed/SMS receipt, PDF export, and CSV export in the Child Memo field And updating a child memo affects future receipts only and does not alter historical receipts
Admin Resend of Tagged Receipts Without Exposing Full Payment Details
Given a studio or platform administrator has permission to view transactions When the admin searches for a receipt and selects Resend via Email/SMS Then the resent receipt includes full tagging (child, class/session, studio, payer) and line itemization And payment details are masked (brand + last4 only) and no full card number, token, or billing address is exposed And the resend event is recorded with timestamp, admin id, channel, and recipient And rate limiting prevents more than 3 resend attempts per receipt per hour
One‑Tap Wallet Checkout
"As a guardian, I want one‑tap checkout that respects our wallet rules so that paying for classes is fast and error‑free."
Description

Integrate the Household Wallet into the booking flow to provide one‑tap checkout with Apple Pay/Google Pay or a saved method, pre‑selecting eligible payment options based on spend caps and approval rules. Autofill payer and household details, show clear cost breakdowns, and pre‑check cap availability before presenting biometric prompts to avoid declines. Support guest‑to‑account upgrade during checkout to attach the transaction to a household. Handle edge cases such as failed biometric auth, 3DS challenges, and switching households mid‑flow without losing the cart or seat.

Acceptance Criteria
One‑Tap Apple/Google Pay with Cap Pre‑Check
Given I am logged into a household with Apple Pay or Google Pay available and the cart items are assigned to child(ren) with sufficient remaining cap When I tap the one‑tap Pay button Then the system validates spend caps and approval rules server‑side before showing the OS pay sheet And if validation passes, the OS pay sheet appears within 1 second And upon successful biometric authorization, the payment is captured, the booking is confirmed, and the confirmation screen shows order summary and masked payment details And no additional data entry is required in the payment step Given pre‑check detects a cap shortfall or missing approval for any cart item When I attempt one‑tap Pay Then the OS pay sheet is not displayed And I see an inline error specifying which child/item is ineligible and options to switch payment method, reassign child, or request approval
Eligible Method Pre‑Selection and Rules Enforcement
Given I have multiple saved methods and one‑tap options When checkout loads Then only methods that satisfy per‑child caps and approval rules for the current cart are enabled And ineligible methods are disabled or hidden with a reason tooltip/message And the most recently used eligible one‑tap method is preselected by default Given I reassign any item to a different child or change household When the selection change is made Then eligibility is recalculated and the preselected method updates accordingly within 500 ms without a full page reload
Autofill Payer and Household Details
Given I am a signed‑in household member When I open checkout Then payer name, email, phone, household name, and child participants auto‑populate from the household profile And I can edit these fields inline And if I check “Save to household,” my edits persist to the profile; if unchecked, changes apply only to this transaction
Transparent Cost Breakdown and Cap Usage
Given the payment review step is visible When viewing totals Then I see an itemized breakdown of class fees, taxes, discounts, surcharges, wallet credits, per‑child allocations, and the final total And per‑child cap remaining is shown both before and after purchase And all displayed amounts sum precisely to the total with consistent rounding rules Given I switch payment method, household, or child assignment When the change occurs Then the breakdown and cap deltas refresh immediately before any biometric or 3DS prompt
Guest‑to‑Account Upgrade During Checkout
Given I am checking out as a guest with items in my cart When I choose “Create household to use Wallet” Then I can verify via email or SMS, add at least one child and a payment method, and return to checkout at the same step And my cart and seat hold are preserved throughout And the one‑tap option becomes available using the newly added method Given the seat‑hold duration elapses during the upgrade flow When I return to the payment step Then I am informed the hold expired and I am offered to retry after re‑adding the item
Biometric Failures and 3DS Challenges
Given the OS pay sheet is displayed When biometric authentication fails or is canceled Then I can retry up to 2 times or change payment method without losing my cart or seat hold And no partial charges or duplicate transactions are created Given a card payment triggers 3DS When the challenge is presented Then the challenge renders inline or via redirect and, on success, the booking completes And on failure, no charge is captured and I can retry or pick another method with the seat hold intact
Switch Household Mid‑Flow with Seat Preservation
Given my account is linked to multiple households When I switch households during checkout Then my cart and seat hold remain intact, child assignments reset to the new household context, eligibility and caps re‑evaluate, and the cost breakdown refreshes And no personal or payment details from the previous household persist in the payment step Given the seat hold expires while I am switching households When I attempt to continue to payment Then I am shown a clear message and payment is blocked until the item is re‑added
Notifications & Audit Trail
"As a guardian, I want timely alerts and a clear activity history so that I can monitor spending and resolve issues quickly."
Description

Provide real‑time notifications for cap approaches/exceedances, approval requests, approvals/denials, successful payments, refunds, and failed charges. Support delivery via in‑app, email, and SMS with user‑level preferences. Maintain a tamper‑evident audit log for household actions (role changes, payment method use, cap edits, approvals) with export and retention controls to meet privacy regulations. Expose an activity timeline in the household dashboard and include correlation IDs on receipts and logs for supportability. Ensure notifications localize to user locale and respect quiet hours where configured.

Acceptance Criteria
Real-Time Cap Threshold Notifications
Given a household child has an active spend cap and guardian recipients with at least one channel enabled When cumulative approved + pending spend reaches 80% of the cap within the current period Then send a “Cap Approaching” notification within 5 seconds via each recipient’s enabled channels (in‑app/email/SMS), localized to recipient locale and timezone, and include child name, amount, cap value, remaining amount, and a correlation ID And ensure idempotency so the same threshold event for the same transaction sends at most one notification per channel And respect each recipient’s quiet hours configuration for non‑critical alerts by deferring email/SMS until quiet hours end Given an attempted charge would exceed the cap When the authorization is evaluated Then send a “Cap Exceeded” notification immediately with child name, attempted amount, cap value, overage amount, merchant, and correlation ID, masking payment method to brand + last4 only, and respecting channel preferences
Approval Request and Decision Notifications
Given a purchase triggers an approval rule for the household When the approval request is created Then notify all configured approvers via their enabled channels with Approve/Deny actions, amount, merchant, child, cap impact, expiry time, and correlation ID, localized to their locale And log the request with correlation ID in the audit log Given an approver takes an action before expiry When the approver approves or denies Then notify the requester and other guardians per their preferences with the outcome, amount, merchant, child, and correlation ID within 5 seconds And update the audit log with the decision, actor, timestamp, and correlation ID Given the approval request expires without action When the expiry time is reached Then apply the configured household rule (auto‑deny or escalate), notify involved parties accordingly, and record the outcome in the audit log with correlation ID
Payment Event Notifications (Success, Refund, Failure)
Given a payment is successfully captured When the capture completes Then notify the payer and opted‑in guardians via enabled channels within 5 seconds, including child, amount, merchant, masked payment method (brand + last4), receipt link, and correlation ID; exclude full PAN or CVV Given a refund is processed When a full or partial refund posts Then notify the payer and opted‑in guardians with refund amount, original amount, merchant, child, correlation ID, and link to the updated receipt, respecting channel preferences and quiet hours for non‑critical events Given a payment attempt fails When the processor returns a failure Then notify the payer and opted‑in guardians within 5 seconds with failure reason code, next‑step guidance, child, merchant, and correlation ID; mark as high‑urgency and deliver per user’s quiet hours policy And write an audit log entry for each event (success/refund/failure) containing event type, actor (if any), timestamp, and correlation ID
User-Level Notification Preferences and Quiet Hours
Given a guardian configures notification preferences When they enable/disable channels per event category (cap alerts, approvals, payments, refunds, failures) and set quiet hours (local start/end) Then subsequent notifications are delivered only via enabled channels and queued outside quiet hours for non‑urgent categories, with delivery attempted within 60 seconds after quiet hours end Given a guardian opts in/out of “Allow urgent during quiet hours” When a high‑urgency event (e.g., failed charge) occurs Then deliver during quiet hours only if the guardian explicitly opted in; otherwise queue until quiet hours end Given preferences are updated on one device When another session is active Then the changes propagate and take effect within 60 seconds across sessions and devices And all preference changes are recorded in the audit log with actor, previous value, new value, timestamp, and correlation ID
Localization and Timezone in Notifications
Given a recipient has a stored locale and timezone When any notification is generated Then render all text using the recipient’s locale templates, format currency appropriately, and display dates/times in the recipient’s timezone; if a locale template is unavailable, fall back to English with correct currency formatting And support pluralization and right‑to‑left rendering where applicable Given an SMS channel with character limits When a localized message exceeds single‑message length Then the message is segmented without breaking variables and still includes the correlation ID or a link containing it
Tamper‑Evident Audit Log with Retention and Export
Given any household action occurs (role changes, payment method use, cap edits, approval requests/decisions, notification preference changes) When the action is committed Then append an audit log entry within 2 seconds containing actor ID and role, household ID, child ID (if applicable), action type, normalized payload digest, UTC timestamp (ISO 8601), IP, user agent, and correlation ID And link entries using a cryptographic hash chain so that modification or deletion invalidates subsequent hashes Given an admin requests verification When the verification API is run over a time range Then it returns a pass/fail integrity status and the first failing entry index if tampering is detected Given a household admin requests an export When export is initiated Then provide CSV and JSON downloads within 60 seconds for up to 50k entries, including a file‑level signature to verify integrity; restrict access via RBAC to household admins; audit the export event Given retention policy is configured (default 24 months, configurable within allowed regulatory bounds) When entries exceed retention Then purge them within 24 hours, producing a purge audit event and ensuring verification continues to pass for the remaining chain (via tombstone links or segment re‑sealing per design)
Household Dashboard Activity Timeline & Correlation Linking
Given a guardian opens the household dashboard When viewing the Activity Timeline Then display a reverse‑chronological feed of notifications and audit events for the last 90 days by default, loading the first 100 items in under 1 second And support filters by child, event type, actor, and date range; support search by correlation ID; provide pagination or infinite scroll with consistent ordering Given a timeline entry is selected When the user clicks it Then open a detail view showing full event context, links to related receipt/approval screens, and the same correlation ID visible and copyable across all surfaces (timeline, receipt, email/SMS) Given a new relevant event occurs while the timeline is open When the event is written Then the item appears in the feed within 5 seconds without a full page refresh, respecting the active filters

Co‑Guardian Roles

Invite another caregiver and assign granular permissions—book, pay, view health notes, receive reminders, or manage pickups. Each guardian controls their own notification preferences while both see a unified family schedule. Reduces coordination friction and ensures the right adult gets the right messages.

Requirements

Co-Guardian Invitation & Account Linking
"As a primary guardian, I want to invite another caregiver to my child’s account so that they can help book classes and receive the right updates."
Description

Enable primary guardians to invite a co-guardian via email or SMS from the child profile or family settings. The invite flow includes tokenized links, account creation or linking for existing users, verification (email/phone), consent capture, and association to one or more children. Support revoking invitations and access, expiration of tokens, rate limiting, and audit logs of invite/accept events. Handle edge cases where the invitee already has a ClassTap account or is linked to another family, with clear conflict resolution. Ensure full white-label theming, localization, and accessibility. All linkage must integrate with ClassTap’s scheduling, payments, reminders, and waitlists so downstream permissions and routing are enforced consistently.

Acceptance Criteria
Invite Co‑Guardian via Email from Child Profile
Given I am the primary guardian viewing a child's profile When I enter a valid email address and click Send Invite Then a single‑use, signed token is generated and a localized, white‑labeled email with the invite link is sent within 60 seconds And an invitation record is stored with status "Pending", invitee email, inviter ID, selected child IDs, timestamp, and token expiry And the UI displays a success confirmation and shows the pending invite with resend and revoke controls And email content meets accessibility checks (readable at 200% zoom, descriptive link text, alt text on images) And no duplicate active invites exist for the same email and identical child set; attempts return a user warning and do not create a new token
Invite Co‑Guardian via SMS from Family Settings
Given I am in Family Settings When I enter a valid mobile number in E.164 format and click Send Invite Then a localized SMS with a short invite link is delivered and the link resolves to the tenant‑branded domain And an invitation record is stored with channel=SMS, phone number, inviter ID, selected child IDs, timestamp, and token expiry And invalid numbers or carrier rejection produce an inline error and do not create a pending invite And SMS content is under 320 characters and excludes PII beyond inviter name and provider brand And the sending form supports screen readers and keyboard‑only navigation
Accept Invite — Existing User Account Linking
Given I have an existing ClassTap account and click a valid invite link When I authenticate successfully Then I am shown the invited child(ren) and a summary of requested permissions and notifications to be applied When I consent Then my account is linked to the specified child(ren), and the invite status becomes "Accepted" And my unified family schedule is available within 5 seconds, with booking/payment/reminder/waitlist routing enforced per permissions If my account is already linked to another family Then the flow presents conflict resolution options allowed by tenant settings (add this family if permitted, or decline), and the chosen action is confirmed before proceeding And all actions (view, consent, accept, conflict resolution) are audit‑logged with user IDs, timestamp, IP, and user agent
Accept Invite — New User Creation, Verification, and Consent
Given I click a valid invite link and do not have an account When I register Then the form enforces password policy (min 12 characters with complexity or passphrase) and collects required profile fields And the system verifies my email or phone based on invite channel via a 6‑digit OTP that expires in 10 minutes with a maximum of 5 attempts When verification succeeds and I provide consent to be a co‑guardian Then the account is created, linked to the specified child(ren), and the invite status becomes "Accepted" If verification fails or OTP expires Then I can request a resend up to 3 times per hour, and all attempts are audit‑logged And all text is localized per invite locale, and the flow meets WCAG 2.2 AA for focus order, labels, and error messaging
Associate Invite to Multiple Children
Given I select multiple children when creating an invite in Family Settings When the invite is sent Then the invitation record includes the exact list of child IDs and is visible in the pending invites list When the invitee accepts Then association to all selected children occurs in a single atomic transaction; partial linking is not allowed If any selected child is removed from the family or access settings change before acceptance Then acceptance fails gracefully with a clear message to both parties and the invite remains unusable; a new invite is required After successful linking Then both guardians see the unified schedule for the linked children and permissions are applied per child as configured
Revoke Invitation and Access with Downstream Enforcement
Given a pending invite When the primary guardian clicks Revoke Then the token is immediately invalidated, the invite status changes to "Revoked", and the invite link cannot be used Given an active co‑guardian link When access is removed by the primary guardian Then within 5 seconds the co‑guardian loses access to the child's schedule, health notes, bookings, and any restricted actions; reminders and pickup notifications stop, and payment/waitlist actions are blocked And localized notifications of the access change are sent to both parties unless suppressed by tenant policy And existing bookings remain intact; only future communications follow updated permissions And all changes are audit‑logged with actor, target, timestamp, and reason (if provided)
Token Security, Expiration, Rate Limiting, and Audit Logs
Invite tokens are single‑use, signed, have 256‑bit entropy, are bound to tenant and invitee email/phone, and expire after 7 days by default (tenant configurable 1–30 days) Using an expired or consumed token displays a localized error and performs no linking Invitation creation is rate‑limited to 5 invites per guardian per hour and 20 per family per day; exceeding limits returns a friendly error and logs the event Resend is limited to 3 times per invite per day; resends rotate tokens, invalidate prior tokens, and reset expiry All invite lifecycle events (create, resend, accept, decline, revoke, access removal) record immutable audit entries with actor, target, timestamp, IP, user agent, and delivery provider status/error codes where applicable Audit logs are filterable by date range, family, and child, and exportable by admins
Granular Permission Matrix & Role Templates
"As a primary guardian, I want to assign exactly what a co-guardian can do so that I stay in control while sharing responsibilities."
Description

Introduce a permission system that controls co-guardian capabilities at a granular level: book classes, cancel bookings, join/leave waitlists, pay, view unified schedule, view/edit health notes, manage pickups, edit child profile, and manage notifications. Provide role templates (e.g., Full Guardian, Booking Only, Pickup Only) plus custom toggles per child. Enforce permissions server-side on all relevant APIs and in the UI (disabled controls, tooltips). Display permission context to users and return consistent error codes on denial. Include an admin API for studios to query effective permissions in white-label deployments. Maintain an auditable change log for role and permission updates.

Acceptance Criteria
Server-Side Enforcement Across Actions
Given a co-guardian lacks 'book' for child X, When POST /bookings with child X, Then response is 403 and no booking record is created Given a co-guardian has 'book' for child X, When POST /bookings with child X, Then response is 201 and booking exists Given a co-guardian lacks 'cancel' for child X, When POST /bookings/{id}/cancel, Then response is 403 and booking status remains unchanged Given a co-guardian has 'cancel' for child X, When POST /bookings/{id}/cancel, Then response is 200 and booking status is canceled Given a co-guardian lacks 'waitlist' for child X, When POST /waitlists or DELETE /waitlists/{id}, Then response is 403 and waitlist membership is unchanged Given a co-guardian has 'waitlist' for child X, When POST /waitlists, Then response is 201 and membership exists; When DELETE /waitlists/{id}, Then response is 204 and membership is removed Given a co-guardian lacks 'pay' for child X, When POST /payments, Then response is 403 and no charge/payment intent is created Given a co-guardian has 'pay' for child X, When POST /payments, Then response is 201 and payment intent/record exists Given a co-guardian lacks 'view_schedule', When GET /family/schedule, Then response is 403 and no schedule data is returned Given a co-guardian has 'view_schedule', When GET /family/schedule, Then response is 200 with schedule entries limited to permitted children Given a co-guardian lacks 'view_health' for child X, When GET /children/{X}/health-notes, Then response is 403 Given a co-guardian lacks 'edit_health' for child X, When PATCH /children/{X}/health-notes, Then response is 403 and no change occurs Given a co-guardian has 'view_health' or 'edit_health' accordingly, When calling GET/PATCH, Then responses are 200 and data reflects changes when applicable Given a co-guardian lacks 'manage_pickups' for child X, When POST/PATCH/DELETE /children/{X}/pickup-authorizations, Then response is 403 and no change occurs Given a co-guardian has 'manage_pickups' for child X, When modifying pickup authorizations, Then responses are 2xx and records reflect changes Given a co-guardian lacks 'edit_child_profile' for child X, When PATCH /children/{X}, Then response is 403 and no change occurs Given a co-guardian has 'edit_child_profile' for child X, When PATCH /children/{X}, Then response is 200 and the profile reflects changes Given a co-guardian lacks 'manage_notifications', When PUT /notification-preferences, Then response is 403 and preferences remain unchanged Given a co-guardian has 'manage_notifications', When PUT /notification-preferences, Then response is 200 and preferences reflect changes
Role Templates: Apply and Persist
Given inviter selects a role template (e.g., Full Guardian, Booking Only, Pickup Only) configured with a default permission set, When the invite is accepted, Then the co-guardian's permissions for the selected children match the template's configured defaults Given a co-guardian with a template applied, When the template is changed to another template, Then the effective permissions update to the new template's configured defaults for the scoped children Given a template is applied, When the guardian's role is reloaded in the UI or fetched via API, Then the same template name and derived permissions are returned consistently Given a guardian is assigned a template for multiple children, When fetching effective permissions per child, Then each child's permissions reflect the template defaults unless overridden per child Given a template definition is updated by an authorized admin, When a new assignment occurs after the update, Then the latest template defaults are applied; When fetching existing assignments, Then they retain the defaults from the time of assignment unless explicitly re-applied
Per-Child Permission Overrides and Effective Evaluation
Given a guardian assigned 'Booking Only' template for children A and B, And an override sets 'book=true' for A and 'book=false' for B, When POST /bookings for A, Then response is 201; When POST /bookings for B, Then response is 403 Given no override exists for child C, When fetching effective permissions, Then child C's permissions equal the assigned template defaults Given an override is added for child D toggling 'manage_pickups=true', When POST /children/{D}/pickup-authorizations, Then response is 2xx; When the override is removed, Then subsequent requests follow template defaults Given multiple overrides exist, When computing effective permissions, Then precedence is: explicit per-child override > template default, deterministically and consistently across UI and APIs Given per-child overrides are edited in the UI, When saving, Then changes persist and are reflected on subsequent API reads and page reloads
UI Enforcement and Permission Context Display
Given a user lacks permission for an action, When viewing a control for that action in the UI, Then the control is disabled (not hidden) and non-interactive, And a tooltip explains the missing permission Given a disabled control, When inspected with assistive tech, Then it exposes aria-disabled=true and an accessible description of the permission limitation Given a user navigates directly to a restricted screen (deep link), When the page loads, Then the primary action elements are not rendered, And a permission context banner lists allowed and disallowed capabilities for the selected child(ren) Given the user has 'view_schedule=false', When viewing the family schedule page, Then no schedule entries are shown and an explanatory message is displayed; When 'view_schedule=true', Then entries are visible Given permissions are updated server-side, When the user refreshes the UI, Then the visible controls and permission context reflect the latest effective permissions
Consistent Permission Denial Error Codes
Given any API request is denied due to permissions, When the response is returned, Then the HTTP status is 403 and the body contains a stable machine-readable code 'PERMISSION_DENIED' Given a denial occurs, When inspecting the error body, Then it includes fields: action (e.g., 'book','cancel','pay'), resource_type (e.g., 'booking','child','payment'), resource_id/child_id when applicable, and correlation_id Given different endpoints enforce permissions, When the same type of denial occurs across endpoints, Then the code and schema are consistent and documented Given a denial occurs, When inspecting the error message, Then it contains no sensitive data about restricted resources beyond identifiers the caller already supplied
Admin API: Effective Permissions Query
Given a studio admin with a valid token for tenant T, When GET /admin/tenants/{T}/guardians/{G}/children/{C}/permissions, Then response is 200 with a boolean map of permissions and associated metadata (template name, overridden flags) Given a studio admin requests permissions for a guardian-child pair outside tenant T, When calling the endpoint, Then response is 403 Given the guardian or child does not exist within tenant T, When calling the endpoint, Then response is 404 Given a studio admin queries without specifying a child, When calling GET /admin/tenants/{T}/guardians/{G}/permissions, Then response is 200 with a list keyed by child_id Given the endpoint is called repeatedly with the same inputs, When comparing responses, Then results are consistent and cache-safe (no leakage across tenants)
Audit Log for Role and Permission Changes
Given a co-guardian invite is created, accepted, or revoked, When the action completes, Then an audit log entry is written with timestamp (UTC ISO-8601), actor_user_id, target_guardian_id, action, and details Given any role template change or per-child permission override add/update/remove, When the change is saved, Then an audit entry records before and after values for each changed permission and the affected child_id(s) Given audit logs exist, When an authorized admin calls GET /admin/audit/permissions?guardian_id=... (and optional child_id filters), Then response is 200 with immutable, chronologically ordered entries; entries cannot be modified or deleted via API by non-super-admins Given an unauthorized user attempts to access or delete audit logs, When calling the audit endpoints, Then responses are 403 and no changes occur Given a correlation_id is present in write requests, When inspecting audit entries, Then the same correlation_id is stored to enable end-to-end traceability
Unified Family Schedule with Ownership Indicators
"As any guardian, I want a single view of our family’s class schedule so that I can coordinate without back-and-forth."
Description

Provide a consolidated calendar view that aggregates all booked classes, waitlist holds, and reminders for all children linked to the guardians. Indicate booking owner, designated pickup guardian, and payer attribution. Include filters by child, guardian, location, and class type; conflict warnings across children and guardians; and real-time updates when bookings change. Ensure timezone awareness, mobile responsiveness, and optional ICS export. Integrate with existing ClassTap scheduling and reminder services so changes propagate to notifications and instructor rosters without duplication.

Acceptance Criteria
Unified Calendar Aggregation and Ownership Indicators
Given a family with multiple children and two or more guardians have active Co‑Guardian roles When the Unified Family Schedule is opened Then it displays in one view all booked classes, waitlist holds, and scheduled reminders for all linked children without duplicates And each item visibly indicates booking owner guardian, designated pickup guardian, and payer attribution as distinct labels or icons with accessible tooltips And items show class modality (in‑person/hybrid) and location And past items are visually distinguished from upcoming ones with correct chronological ordering
Multi‑Filter by Child, Guardian, Location, and Class Type
Given the unified calendar contains items for multiple children, guardians, locations, and class types When the user applies any combination of Child, Guardian, Location, and Class Type filters Then only matching items remain visible and a visible result count updates accordingly And when filters yield zero results, an empty state is shown with a Reset Filters action And selected filters persist during navigation within the Family Schedule until cleared or the session ends And applying or clearing filters completes in under 300ms on a typical 4G connection for up to 300 items
Cross‑Child and Cross‑Guardian Conflict Detection
Given two or more items overlap in time for the same child or require the same designated pickup guardian at overlapping times When the calendar loads or items are added/edited Then a conflict badge is shown on each conflicting item and a summary list of conflicts is available And overlaps of any duration ≥ 5 minutes in the displayed timezone are flagged And resolving by rescheduling or removing an item clears the conflict indicators within 2 seconds And no false positives occur for back‑to‑back items with a gap ≥ 5 minutes
Real‑Time Updates and Waitlist Promotions
Given a booking is created, updated, canceled, or a waitlist hold is promoted by any guardian or instructor When such a change occurs Then the unified calendar reflects the change within 2 seconds without full page reload And changed items retain accurate ownership, pickup, and payer indicators And no duplicate items appear after successive updates And a transient Updated indicator is shown on modified items for 3 seconds
Timezone‑Aware Display and ICS Export
Given the primary guardian has a timezone preference When viewing the unified calendar Then all item start/end times display in that timezone, with an indicator if the class’s original timezone differs And when the guardian changes their timezone preference or device timezone Then displayed times update on next render and within 2 seconds of change And when exporting the currently filtered calendar to ICS Then the ICS includes all visible items with correct DTSTART/DTEND in UTC with timezone components and includes title, location, child name, booking owner, pickup guardian, and payer in the description And the ICS respects active filters, excludes duplicates, and omits past items older than 90 days by default
Mobile‑Responsive Unified Schedule
Given a mobile device with viewport width ≤ 414px When accessing the unified calendar Then the view adapts to a vertical agenda layout with tap targets ≥ 44px and legible typography And ownership, pickup, and payer indicators remain visible via inline labels or expandable details And filtering, item detail navigation, and conflict indicators are fully usable with touch and screen readers And scrolling performance sustains 60fps for lists up to 200 items on mid‑tier devices
Integration with Reminders and Instructor Rosters
Given a guardian updates an item from the unified calendar (e.g., change pickup guardian or payer) When the change is saved Then reminder recipients are recalculated per each guardian’s notification preferences and the correct adult receives subsequent reminders And instructor rosters reflect the change within 5 seconds with no duplicate roster entries And audit logs record who made the change, when, and what fields changed And if the scheduling service is unavailable, a retryable error is shown and no partial or duplicate notifications are sent
Independent Payment Methods & Charge Controls
"As a primary guardian, I want the co-guardian to pay for their own bookings so that finances remain clear and controlled."
Description

Allow each guardian to maintain their own payment methods and billing details. Let the primary guardian set charge policies for co-guardians: allow/deny charging, per-transaction caps, monthly limits, class-type restrictions, and optional approval workflows. Attribute each transaction to the paying guardian, route receipts accordingly, and process refunds back to the original payer. Ensure compliance with PCI DSS via gateway tokenization (e.g., Stripe/Adyen), and handle failed payments, dunning, and chargeback notifications targeted only to the payer. Provide clear UI labels showing who will be charged and what policies apply. Expose payment attribution in reports and exports.

Acceptance Criteria
Independent Payment Methods per Guardian
- Given I am a logged-in guardian, When I add a new card or bank account, Then the method is tokenized by the configured PCI-compliant gateway and no raw PAN or sensitive auth data is stored by ClassTap. - Given two guardians on the same family, When each manages payment methods, Then each can view and manage only their own methods and cannot see the other guardian’s billing details (only payment method brand and last4 may appear in shared contexts where necessary). - Given I set a default payment method, When I check out as myself (payer = me), Then my default is preselected and does not affect the co‑guardian’s default. - Given a payment method has an in-flight charge, When I attempt to delete it, Then the system prevents deletion and explains the reason; otherwise deletion succeeds. - Given any add/update/delete of a payment method, Then an audit event records guardian ID, action, masked method, and timestamp.
Primary Guardian Charge Permissions and Limits
- Given I am the primary guardian, When I set the co‑guardian’s charge permission to Deny, Then that guardian cannot be selected as payer at checkout and an explanatory message is shown. - Given I set a per‑transaction cap of X, When the co‑guardian attempts to pay an amount > X, Then the attempt is blocked or routed to approval per configuration and the UI explains the limit; amounts ≤ X proceed if otherwise eligible. - Given I set a monthly limit of Y, When the co‑guardian’s cumulative successful charges in the current calendar month reach Y, Then further charges are blocked or routed to approval per configuration and remaining allowance is shown in checkout. - Given I restrict class types to an allowed list, When the co‑guardian attempts to pay for a disallowed class type, Then payment with that guardian as payer is prevented and another payer must be chosen. - Given policies are updated, When a checkout occurs, Then the latest policies are enforced at authorization time.
Approval Workflow for Co‑Guardian Charges
- Given approval is required for co‑guardian charges, When the co‑guardian initiates checkout, Then an approval request is sent to the primary guardian and the checkout is marked Pending Approval with no charge captured. - Given the primary guardian approves within the configured window, When approval is received, Then the payment is authorized against the co‑guardian’s method and the booking is confirmed. - Given the primary guardian rejects or the approval window expires, When the decision is recorded, Then no charge occurs, the booking is canceled or returned to open, and both guardians are notified according to their preferences. - Given any approval decision, Then an audit trail stores approver, decision, timestamp, and linked booking/payment IDs.
Checkout UI Indicates Payer and Applicable Policies
- Given a family with multiple guardians, When I reach the payment step, Then the UI clearly shows “Payer: <Guardian Name>” and a concise policy summary (allow/deny, per‑transaction cap, monthly remaining, class‑type restrictions). - Given I change the payer selection, When I choose a different guardian, Then the policy summary, eligibility state, and any warnings update instantly without page reload. - Given payer eligibility is not met (denied, over cap, disallowed class type), When I view the payment action, Then the Pay/Confirm button is disabled with an accessible reason label announced by screen readers. - Given a payment completes, When the confirmation is displayed, Then it states which guardian was charged and where the receipt was sent.
Transaction Attribution, Receipts, and Refunds to Original Payer
- Given a successful transaction, When it is stored, Then the payer guardian ID is attributed on the payment, booking, and ledger entries. - Given a receipt is generated, When notifications are sent, Then only the payer guardian receives the receipt per their notification preferences. - Given a refund is initiated, When processed, Then funds are returned via the original transaction to the original payment method and cannot be redirected to another guardian. - Given partial or full refunds, When completed, Then confirmations are sent only to the original payer and attribution is reflected in reports/exports.
Failed Payments, Dunning, and Chargebacks Target Only the Payer
- Given a payment attempt fails, When the dunning schedule triggers retries, Then notifications are sent only to the payer guardian and include next steps, and no notices go to other guardians. - Given a payment is recovered during dunning, When a retry succeeds, Then dunning stops and only the payer receives a success notification. - Given a chargeback webhook is received, When it is processed, Then the dispute is attributed to the original payer and only that guardian is notified; internal dispute status is updated on the transaction. - Given dunning reaches its final failure, When attempts are exhausted, Then only the payer is notified of failure and follow‑up actions are logged for reporting.
Reporting and Export Payment Attribution
- Given an admin or instructor generates a payments report, When data is rendered or exported, Then each transaction row includes payer guardian name/ID, guardian role (primary/co‑guardian), payment method brand and last4, transaction/refund linkage, and charge policy snapshot at time of charge. - Given a payer guardian filter is applied, When the report is run, Then only transactions attributed to that guardian are returned. - Given a family‑level export is downloaded, When the file is generated, Then transactions for all guardians are included with per‑transaction payer attribution and receipt routing captured. - Given guardian access controls, When a guardian views their history, Then they can see which guardian paid each booking but cannot view another guardian’s full billing details (beyond brand and last4).
Per-Guardian Notification Preferences & Routing Rules
"As a co-guardian, I want to choose which messages I receive and how so that I get relevant updates without spam."
Description

Enable each guardian to configure notification preferences by event type (booking confirmation, reminder, waitlist release, cancellation, payment receipt, pickup confirmation, health note request) and by channel (SMS, email, push), with language, timezone, quiet hours, and verification status. Implement routing logic so messages go to the most relevant guardian based on role and context (e.g., pickup alerts to the assigned pickup guardian, payment receipts to the payer) with fallback recipients when required by policy. Deduplicate messages, provide delivery/read logs, and support studio branding. Ensure compliance with consent and opt-out regulations (e.g., TCPA/GDPR) and maintain per-guardian contact verification.

Acceptance Criteria
Guardian Preference Setup & Channel Verification
- Given a guardian is on Notification Preferences, When they enable/disable a channel (SMS, email, push) per event type (booking confirmation, reminder, waitlist release, cancellation, payment receipt, pickup confirmation, health note request), Then the system saves the setting and displays a confirmation state. - Given a guardian enables a channel for an event type with an unverified contact, When they attempt to save, Then the system initiates verification (OTP/link) and does not activate the channel until verification succeeds. - Given a guardian provides or withdraws consent for a channel, When saved or received via inbound keyword/action (e.g., SMS STOP, email unsubscribe), Then consent status updates immediately with timestamp and the channel is blocked/unblocked accordingly for all event types. - Given a guardian sets preferred language, timezone, and quiet hours window, When saved, Then the settings persist and are associated to the guardian profile for use during message composition and scheduling. - Given regulatory requirements apply (e.g., TCPA/GDPR), When preferences are saved, Then the system records consent/verification artifacts (method, timestamp, source) and prevents enabling channels without required consent.
Per-Guardian Localization, Quiet Hours, and Preference Application on Send
- Given an event is triggered for a family, When evaluating recipients, Then only guardians with permission for the event type and with enabled, verified, consented channels are considered eligible. - Given a guardian has quiet hours active at the send time, When the event is non-urgent, Then the notification is scheduled to send at the end of the guardian’s quiet hours and the scheduling decision is logged. - Given a notification is composed for a guardian, When sending, Then all date/time values are rendered in the guardian’s timezone and the body is localized to the guardian’s selected language. - Given studio branding settings are configured, When any notification is sent, Then the message uses the studio’s branding (name/logo/colors/sender identity) across supported channels.
Pickup Alerts Routed to Assigned Pickup Guardian with Policy Fallback
- Given a student has an assigned pickup guardian for a session, When a pickup alert is triggered, Then the alert is sent to the assigned pickup guardian via their enabled, verified, consented channels. - Given the assigned pickup guardian has no eligible channels, When routing the pickup alert, Then the system sends to the fallback guardian(s) per studio policy and logs the fallback reason. - Given a pickup assignment is changed, When the next pickup alert is triggered, Then the alert routes only to the new assignee and not to the former assignee. - Given both guardians receive pickup alerts per policy, When messages are sent, Then each guardian receives at most one message per channel for the alert and localization/timezone are applied per guardian.
Payment Receipts Routed to Payer with Studio Branding
- Given a payment is successfully completed, When issuing the receipt, Then the receipt is sent only to the paying guardian via their enabled, verified, consented channels. - Given the paying guardian has no eligible channels, When issuing the receipt, Then the system routes to the account owner or designated billing guardian per studio policy and logs the fallback. - Given a co-guardian has opted to receive copies of payment receipts, When the receipt is issued, Then a copy is sent according to the co-guardian’s preferences; otherwise, no copy is sent. - Given studio branding settings are configured, When the receipt is sent, Then the receipt includes studio branding and mandatory receipt details (amount, currency, transaction ID, date/time in payer’s timezone).
Waitlist Release Routing with Timed Escalation
- Given a guardian placed a student on a waitlist, When a spot is released to the family, Then the release notification is sent to that guardian via their eligible channels. - Given no response is recorded within the studio-configured hold period, When the hold expires, Then the system escalates per policy by notifying the co-guardian and records the escalation with timestamps. - Given neither guardian has eligible channels at release time, When attempting to notify, Then no messages are sent and the system records a failed notification with reason "no eligible channels". - Given the initial guardian declines the spot, When the decline is captured, Then the co-guardian is notified immediately per fallback policy.
Deduplication and Idempotent Sends
- Given multiple triggers occur for the same event within the deduplication window, When processing notifications, Then at most one message per guardian per channel per event is sent. - Given a provider retry or duplicate webhook is received, When the idempotency key matches a prior send, Then no additional message is sent and the prior result is returned/logged. - Given both primary and fallback routing paths select the same guardian, When sending, Then the guardian receives only one message per channel for the event. - Given a guardian has multiple channels enabled for an event, When sending, Then deduplication does not suppress distinct channels (e.g., SMS and email both send once).
Delivery and Read Logs Per Guardian
- Given notifications are sent, When viewing the communication log, Then each entry shows guardian, event type, channel, message ID, routing decision, and timestamps for queued/sent/delivered/read (where supported), plus current status. - Given filters are applied, When filtering by guardian, date range, event type, channel, or status, Then the log updates to show only matching entries. - Given a notification fails, When viewing its log entry, Then the provider error code/message and the system retry outcome are visible. - Given auditing is required, When inspecting a log entry, Then the consent and verification statuses used at send time are accessible from the entry.
Health Notes Access Controls & Audit
"As a primary guardian, I want to control who can see and edit my child’s health notes so that privacy and safety are protected."
Description

Gate access to child health notes (allergies, medications, cautions) using the permission matrix with read vs. edit scopes and per-field sensitivity flags. Require explicit consent before sharing sensitive data with co-guardians and instructors. Provide an audit trail of all views and edits including actor, timestamp, and changes. Allow guardians to request updates and receive confirmations of changes. Limit instructor visibility to need-to-know fields during rosters and check-in. Support data retention, export, and deletion requests consistent with regional privacy laws, and mask sensitive fields in notifications where applicable.

Acceptance Criteria
Enforce Read/Edit Permissions on Health Notes for Co‑Guardians
Given a child has health notes with fields marked as non-sensitive and sensitive And a co‑guardian has read:health_notes=true and edit:health_notes=false When the co‑guardian opens the child’s Health tab Then the co‑guardian can view all non-sensitive fields And all edit controls are disabled for all fields And all sensitive fields are fully hidden Given a co‑guardian has read:health_notes=true and edit:health_notes=true And explicit consent exists for sensitive field A When the co‑guardian edits field A and saves Then the change persists And a success message is shown And an audit event is recorded for the edit Given a co‑guardian has read:health_notes=false When they call the health notes API for the child Then the API returns 403 Forbidden and no field values are returned
Explicit Consent Before Sharing Sensitive Health Data
Given a child has sensitive health fields And no consent exists to share with Co‑Guardian X When the primary guardian attempts to grant Co‑Guardian X any permission that would expose those fields Then the system requires explicit consent capture specifying recipient, data categories, purpose, scope (read/edit), and revocability And access to sensitive fields remains blocked until consent is confirmed Given explicit consent is granted for sensitive field B (read-only) to Co‑Guardian X When Co‑Guardian X views the child’s Health tab Then field B is visible in read-only state and other non-consented sensitive fields remain hidden Given consent for field B is revoked by the primary guardian When Co‑Guardian X refreshes the Health tab or accesses the API Then field B is hidden and API returns 403 for that field And a consent_revoked audit event is logged
Comprehensive Audit Trail of Health Note Access
Given any user (guardian, co‑guardian, instructor, staff) views or edits a health note field When the operation completes Then an audit event is recorded with actor_id, actor_role, child_id, action (view|edit|export|delete|consent_grant|consent_revoke), field_ids, timestamp (UTC), request_ip, and user_agent And for edits the audit includes before_value and after_value with sensitive values masked in exported reports Given an authorized admin with AuditViewer role filters audits by child and date range When they export to CSV Then the file contains matching events with masked sensitive values and unmasked metadata Given an unauthorized user requests audit data When they hit the audit endpoint Then the system returns 403 Forbidden
Guardian Update Request and Confirmation Flow
Given a co‑guardian lacks edit:health_notes permission When they submit an update request specifying target field and proposed value Then a request record is created with requester_id, child_id, field_id, proposed_value, timestamp, and status=pending And an authorized editor (primary guardian or designated role) is notified via their chosen channel Given an authorized editor applies the change When the update is saved Then the request status becomes resolved And the requester receives a confirmation notification containing field name, old value masked if sensitive, new value, editor identity, and timestamp And an audit event is recorded linking the request to the edit Given an authorized editor declines the request When they provide a reason and submit Then the requester receives a decline notification showing the reason And the request status becomes declined
Instructor Roster Need‑to‑Know Visibility
Given an instructor opens a class roster or check‑in view When health info is shown inline with attendees Then only fields with share_with_instructor=true and (non-sensitive OR sensitive with explicit guardian consent) are visible And all other fields are concealed with a Restricted indicator Given the instructor attempts to open concealed health details When they click or call the API Then the UI blocks access and the API returns 403 Forbidden Given a guardian changes a field’s share_with_instructor flag or revokes consent When the roster is refreshed Then the change is reflected immediately and any previously visible data becomes concealed
Privacy Compliance: Retention, Export, and Deletion
Given a guardian requests an export of their child’s health notes When the export is generated Then the system provides machine‑readable files (JSON and CSV) containing current health fields, consent records, and audit metadata with sensitive values masked in notification content and audit exports Given a guardian requests deletion of their child’s health notes and the request is approved When deletion is executed Then health note fields are removed from the active dataset and hidden from all UIs And a purge is queued for hard deletion after the region’s configured retention window And an audit purge_requested and purge_completed event is logged Given the regional retention threshold is reached for soft‑deleted health notes When the nightly retention job runs Then data is permanently deleted in scope and a purge summary report is generated for admins
Mask Sensitive Health Data in Notifications
Given the system sends SMS, email, or push notifications relating to classes, bookings, or health updates When a message references health information Then no sensitive field values are included And only minimal indicators (e.g., “Health info updated”) and a link to the secure portal are present Given a co‑guardian has read access but no consent for sensitive fields When a health note is updated Then they receive a notification without the sensitive values And the secure view enforces the same access controls upon click‑through Given an instructor receives a roster reminder When health indicators are included Then only need‑to‑know flags permitted by share_with_instructor and consent are included, without specific substances unless consent exists
Pickup Authorization & Check-in Handoff
"As an instructor, I want to verify authorized pickups quickly so that children leave with the right adult and I can focus on teaching."
Description

Manage authorized pickup contacts per child with time-bound and class-bound permissions that can be granted by either guardian if allowed. Allow assignment of a pickup guardian per booking. At check-in/out, provide instructors a quick verification flow with QR code or PIN, show authorized contacts on the roster, and capture a secure handoff confirmation. Notify the designated guardian upon pickup events or attempted pickups by unauthorized parties, with escalation paths. Log all handoff events for audit. Integrate with existing attendance/check-in modules and notification routing rules.

Acceptance Criteria
Authorize Pickup Contacts with Time/Class Constraints
- Given a guardian with "Manage pickups" permission and a child profile, when the guardian adds a contact with required fields (full name, mobile, verification doc type+last4), then the contact is saved encrypted and visible in the child's Authorized Pickups list. - Given the guardian sets a validity window (start/end) and optional class/session scope, when current time is within the window and session matches the scope, then the contact is authorized; otherwise they are not authorized. - Given the child has "Either guardian may grant pickups" enabled, when either guardian adds/edits/removes a contact, then the changes are accepted and versioned; otherwise only the primary guardian may do so. - Given a contact is revoked or its window expires, when the roster is refreshed, then the contact no longer appears as authorized within 5 seconds and check-out verification fails for them. - Given duplicate entries (same contact + same scope), when saving, then the system prevents duplicates and prompts to edit the existing record.
Designate Pickup Per Booking
- Given a guardian books a class for a child, when selecting "Pickup Person" from the authorized list or "Self", then the booking stores a designated pickup for that session. - Given no selection is made, when booking completes, then default is "Any authorized pickup" for that session. - Given the guardian edits the booking before class start, when updating the designated pickup, then the roster reflects the change within 5 seconds and the previous designation is superseded. - Given the designated pickup is not currently authorized (revoked/expired), when class check-out occurs, then verification fails unless an instructor performs a manual override with reason and staff PIN.
QR Code Verification at Check-out
- Given a designated or authorized pickup, when they open the pickup pass link, then a rotating QR code valid for 60 seconds is shown. - Given an instructor scans the QR code at check-out, when the code is valid, session matches, and child matches, then the system confirms identity and checks out the child. - Given the QR code is expired, reused, or does not match the session/child, when scanned, then check-out is blocked and an unauthorized attempt is logged. - Given the device is offline, when scanning occurs, then cached authorization (synced within last 24 hours) may be used to allow check-out and the event queues for sync; otherwise block and prompt for PIN fallback.
PIN Verification at Check-out
- Given an authorized pickup, when they provide their 6-digit pickup PIN to the instructor, then the system verifies it with rate limiting (max 5 attempts per 10 minutes) and checks out the child on success. - Given three consecutive failed PIN attempts, when further attempts occur, then attempts are locked for 10 minutes and an alert is sent to guardians. - Given the guardian resets a pickup contact's PIN, when reset completes, then old PINs are invalid immediately and a confirmation notice is sent.
Roster Shows Authorized and Designated Pickups
- Given an instructor opens today's class roster, when viewing a child row, then the roster displays the designated pickup (if any), plus currently authorized contacts filtered by time/class scope. - Given a contact has photo and relationship, when displayed, then the roster shows name, relationship, thumbnail photo, and notes flagged "medical/safety" only. - Given the roster is exported or printed, when generated, then only the designated pickup and minimal identifiers (name, relationship, last4 of ID) are included—no full PII.
Notifications and Escalations for Pickup Events
- Given a successful check-out, when the handoff is confirmed, then the designated guardian receives a notification via their preferred channels within 60 seconds; the other guardian receives per their preferences. - Given an unauthorized pickup attempt is blocked, when it occurs, then both guardians are notified immediately with details and a link to update authorizations; after 2 failed attempts within 30 minutes, escalate to studio admin via email/SMS. - Given a check-out occurs using offline cached verification, when the device syncs, then notifications are sent and deduplicated to avoid duplicates.
Audit Logging and Attendance Integration
- Given any check-in/out or pickup attempt, when the event occurs, then the system logs child ID, booking ID, pickup contact ID, method (QR/PIN/manual), instructor ID, timestamp, device ID, and outcome in an immutable audit record. - Given a successful pickup, when logged, then the attendance module marks the child as "Checked out" and prevents duplicate check-outs for that session. - Given a manual override is used, when recorded, then a reason is required and a second staff member PIN is prompted for co-signature. - Given an authorized user queries audit logs, when filters (date range, child, class, outcome) are applied, then results return within 2 seconds for the last 90 days and export to CSV is available.

Sibling Swap

Easily transfer a booked seat from one child to another without calling the front desk. Eligibility and consents are rechecked automatically, price differences are handled as a quick credit/charge, and rosters plus reminders update instantly—preventing no-shows and keeping families flexible.

Requirements

Household Sibling Linking
"As a guardian, I want all my children recognized under one household so that I can quickly swap a booking between them without re-entering information."
Description

Enable guardians to designate children as siblings under a single household payer account and expose a reliable sibling selector during swap. The system must resolve identities across duplicate child profiles, confirm shared guardianship, and restrict swaps to children within the same payer/household and organization. Data model updates include a household entity linking guardian(s), payment methods, memberships/passes, and dependent child profiles with age, grade, and risk flags. The swap flow must only surface eligible siblings, mask PII per role, and gracefully handle missing or incomplete child profiles by prompting completion before proceeding.

Acceptance Criteria
Household creation and sibling linking under single payer
Given a guardian with a verified payer account and two child profiles they own When the guardian designates the children as siblings Then the system assigns a single Household ID linking guardian(s), payer account, payment methods, memberships/passes, and both child profiles And both children return the same Household ID via API and UI queries And the sibling relationship is available to the swap flow within 5 seconds of save And an audit log entry records actor, timestamp, affected IDs, and previous state
Duplicate child profile resolution during linking
Given two potential duplicate child profiles detected by match rules (e.g., legal name + DOB ±2 days OR guardian phone + school match) When the guardian initiates sibling linking Then the system requires duplicate resolution before linking continues When the guardian selects Merge and confirms Then a canonical child is created with attribute precedence rules applied, future enrollments and risk flags moved, duplicate profiles deactivated, and audit references preserved And only the canonical child appears in the sibling selector When the guardian declines merge Then linking is blocked and an error states "Duplicate profile unresolved"
Shared guardianship verification for sibling linking
Given two child profiles the guardian attempts to link as siblings When verification runs Then the system confirms at least one common verified guardian and matching payer account within the same organization And verification succeeds only with a valid email or SMS OTP within 10 minutes or an admin override with explicit reason code When verification fails Then linking is prevented, no household changes are persisted, and the user sees "Shared guardianship not verified" When verification succeeds Then sibling linking completes and eligibility appears in the swap selector
Eligible sibling selector during swap
Given a booked seat for Child A in Organization X When a guardian opens the Sibling Swap flow Then the selector lists only siblings sharing the same Household ID and payer account within Organization X And ineligible children are not displayed And each listed sibling shows masked identifiers: first name, last initial, age, and risk flag indicator When a sibling is selected Then eligibility checks (consents current, age/grade constraints, risk flags) return pass/fail within 2 seconds When no eligible siblings exist Then the UI displays "No eligible siblings" and disables Continue
PII masking per role in sibling contexts
Given a staff user with Instructor role views the sibling selector Then only first name, last initial, age, and risk flags are visible; DOB, full last name, contact details, and address are hidden Given a guardian user views the selector Then they see their own children's first and last names and age only; DOB and address remain hidden Given an Admin with "View Sensitive Child Data" permission views the selector Then full PII is shown And API responses enforce the same masking by role, verified via contract tests
Profile completeness gating before swap
Given the selected sibling is missing required fields (age, grade, emergency contact, or required consent) When the guardian selects the sibling for swap Then the system blocks progression and presents a guided form listing missing fields When the guardian completes and submits the form Then server-side validation runs and, if successful, the swap flow resumes at the confirmation step without losing context When the guardian cancels the form Then no changes are saved and the original booking remains unchanged
Household-scoped memberships and passes linkage
Given a household with active household-scoped memberships/passes When a new child is linked as a sibling to the household Then the child becomes eligible to use household-scoped passes, visible in booking and swap flows And child-scoped passes remain attached only to their original child and are not auto-transferred And all entitlements propagate to roster and selector views within 10 seconds And database referential integrity shows no orphaned pass references
Eligibility and Consent Recheck
"As an instructor-admin, I want eligibility and waivers rechecked automatically during a sibling swap so that compliance is maintained without manual review."
Description

On swap initiation, revalidate the target child against class eligibility rules (age/grade bands, skill prerequisites, roster caps by cohort, membership requirements) and verify all required waivers, medical info, and emergency contacts. If any consent or prerequisite is missing or expired, block the swap and present an inline flow to capture or renew it with timestamped, versioned records. Ensure provider-specific policies are respected and stored, and persist an eligibility decision artifact on the booking for auditability. The flow must be idempotent and performant, returning a decisive pass/fail with actionable remediation steps.

Acceptance Criteria
Swap Pass: All Checks Valid
Given a sibling swap is initiated to a target child who meets age/grade band, skill prerequisites, membership requirements, and cohort cap availability, and all required waivers, medical info, and emergency contacts are current When the eligibility and consent recheck runs Then the system returns decision = "pass" with remediationSteps = [] And no consent/medical/emergency forms are shown And the booking is eligible to proceed to seat transfer without additional input
Swap Blocked with Inline Consent Remediation
Given one or more required consents/info are missing or expired (e.g., waiver, medical info, emergency contacts) When the recheck runs Then the system returns decision = "fail" and includes remediationSteps enumerating each missing/expired item with machine-readable codes and required actions And the UI presents an inline capture/renewal flow for those items without leaving the swap And upon successful completion of the inline steps, a new recheck is triggered automatically And if all remediable items are resolved, the subsequent decision = "pass" within 3 seconds end-to-end And newly captured records are stored with timestamp, policy version, actor identity, and are marked current
Ineligible by Age/Grade/Skill or Roster Cap
Given the target child is outside the class age/grade band, lacks a required skill prerequisite or membership, or the cohort cap for the child’s cohort is full When the recheck runs Then the system returns decision = "fail" with immutable failure reasons and does not display consent capture flows And the current booking remains unchanged (no seat transfer) And remediationSteps provide actionable alternatives (e.g., select different class/cohort or join waitlist) with machine-readable reason codes
Provider Policy Versioning and Enforcement
Given the provider has an active eligibility/consent policy configuration with a version identifier When the recheck runs Then evaluation uses the provider’s active policy version at evaluation time And the decision payload and persisted artifact include policyVersionId and rule identifiers used And modifying the policy after evaluation does not retroactively alter existing decision artifacts
Eligibility Decision Artifact Persistence
When any eligibility recheck completes (pass or fail) Then an immutable decision artifact is persisted on the booking containing: decision status, reasons, remediationSteps, evaluated rules and their versions, timestamp (UTC), evaluator service version, idempotency/correlation key, and evaluated input snapshot (childId, classId, cohortId, membershipId) And the artifact is retrievable via API and visible in admin within 5 seconds of decision And artifacts are retained for at least 24 months and are tamper-evident
Idempotent Recheck Under Retries and Concurrency
Given identical swap inputs and an idempotency key are submitted multiple times within 5 minutes When the recheck is invoked concurrently or sequentially Then only one decision artifact is created and returned across requests And all callers receive the same decision and artifactId And no duplicate consent/medical records are created or modified more than once And if any input changes (e.g., a consent is completed), a new artifact with a new artifactId is produced on the next recheck
Performance and Reliability SLA
Given normal load up to 50 eligibility rechecks per second When the recheck API is called Then server-side latency is P95 ≤ 500 ms and P99 ≤ 800 ms for the eligibility decision And the weekly error rate for the endpoint is < 0.1% excluding 4xx client errors And 99.9% of responses contain exactly one decisive status in {pass, fail}; when fail, at least one remediationStep with a machine-readable code and localized label key is present
Pricing Delta and Payment Adjustment Automation
"As a guardian, I want any price difference handled automatically during a swap so that I don’t have to call the front desk to settle charges or credits."
Description

Upon selecting a target child, recompute the booking price using the child’s pricing context (age-tier pricing, memberships/passes, sibling discounts, taxes, and promo codes) and automatically settle the difference. If the target child’s price is higher, attempt a seamless charge using the household’s default payment method; if lower, issue a refundable credit to the household wallet or original payment instrument per provider policy. Generate itemized receipts/credit memos, update the ledger, and expose webhooks for accounting. Handle edge cases: partial authorizations, expired cards, pass consumption vs. refund, and tax recalculation. No double-charging and strict transactional integrity are required.

Acceptance Criteria
Higher Price Delta — Auto Charge via Default Method
Given a Sibling Swap where the target child’s repriced total exceeds the original booking total And the household has a valid default payment method on file When the user confirms the swap Then the system computes the exact delta as target total minus original total And authorizes and captures the delta on the default method in a single flow And the booking is reassigned to the target child only after a successful capture And no additional charge beyond the computed delta is created And if the request is retried with the same idempotency key, no duplicate charge is made And payment, booking reassignment, and pass adjustments (if any) commit atomically
Lower Price Delta — Auto Credit per Provider Policy
Given a Sibling Swap where the target child’s repriced total is less than the original booking total And a provider refund policy is configured for refund destination (household wallet or original instrument) When the user confirms the swap Then the system computes the negative delta accurately And issues a refundable credit to the household wallet or initiates a refund to the original payment instrument per policy And generates a credit memo reflecting the delta and tax impact And updates wallet balance immediately if wallet is the destination And completes the swap without staff intervention
Zero Price Delta — No Financial Movement
Given a Sibling Swap where the repriced total equals the original booking total When the user confirms the swap Then no charge, refund, or wallet credit is created And the booking is reassigned to the target child And no financial documents (receipt/credit memo) are generated And the ledger remains unchanged
Repricing Engine — Eligibility, Discounts, Promos, and Taxes Recalculated
Given a Sibling Swap with a selected target child When repricing executes Then the system applies the target child’s pricing context: age-tier base price, membership/pass eligibility, sibling discounts, valid promo codes, and jurisdictional taxes And ineligible discounts or promos from the source child are not applied And taxes are recomputed on the adjusted taxable base using provider rules And the UI displays an itemized breakdown and the delta versus the original before confirmation
Pass/Membership Reconciliation — Consumption vs. Refund
Given the original booking consumed a pass or membership benefit And the target child may have different pass/membership eligibility When the user confirms the Sibling Swap Then the system determines whether a pass should be applied to the target booking based on eligibility and availability And if a previously consumed pass unit is no longer needed, it is returned to the original pass balance And if additional pass units are required but unavailable, the uncovered portion is included in the price delta and charged per charging rules And pass/membership balances and usage logs are updated atomically with payment adjustments
Payment Failure Paths — Partial Auth/Decline/Expired Card
Given a positive delta must be charged for the Sibling Swap And the default payment method is expired or the authorization is partial/declined When the swap attempt is made Then the swap is rolled back and the original booking remains unchanged And all pending payment intents/holds are voided or canceled without settlement And no ledger entries, receipts, or credits are finalized And the user is shown a clear failure message with reason code and a timestamp And an audit log entry is recorded with swap ID, household ID, and failure reason
Accounting Outputs — Itemized Documents, Ledger, and Webhooks
Given a Sibling Swap that results in a non-zero financial delta or pass adjustment When the adjustment completes successfully Then an itemized receipt (for charges) or credit memo (for refunds/credits) is generated with lines for base price, discounts, promos, pass value, taxes, and totals And double-entry ledger entries are recorded with a correlation ID linking booking, payment, pass changes, and documents And webhooks are emitted for accounting (e.g., swap.pricing_adjusted, payment.charge_succeeded, payment.refund_initiated) with idempotency keys and payloads including swap ID, booking ID, household ID, amounts, tax, and pass impacts And webhooks are delivered at-least-once and are safe to process idempotently by consumers
Conflict and Policy Guardrails
"As a guardian, I want the system to prevent swaps that would conflict with my child’s schedule or break studio policies so that I don’t create problems unintentionally."
Description

Enforce schedule and policy constraints before confirming a swap. Validate that the target child is not already booked for overlapping sessions, daily/weekly maximums are respected, and provider-defined swap windows (e.g., up to 2 hours before class) and swap limits per booking are enforced. Honor pass and bundle rules (e.g., non-transferable items) and sponsorship/grant restrictions. Provide clear, localized error states and a staff override capability with reason capture. The guardrail engine must be reusable and configurable per organization.

Acceptance Criteria
Prevent Swap When Target Child Has Overlapping Booking
Given a booked class A with start and end times And a target child who has an existing booking B that overlaps class A time (B.start < A.end AND B.end > A.start) within the organization timezone When a user initiates a sibling swap to move the seat to the target child Then the swap is blocked And a localized error is shown stating the conflict details (child name, conflicting class name, date/time) And no roster, waitlist, reminder, or payment records are created or modified And the block is logged with rule_id "overlap_conflict" and correlation_id of the request
Enforce Participation and Swap Limits
Given organization policies define daily_max_enrollments, weekly_max_enrollments, and per_booking_swap_limit And the target child’s existing enrollments and swaps-to-date for the relevant periods are known When a sibling swap is attempted that would exceed any applicable limit Then the swap is blocked And the error message identifies which limit was exceeded and the current vs allowed counts And counts are computed in the organization’s configured timezone and include only active enrollment statuses And the block is logged with rule_id "limit_exceeded" and the specific limit key
Enforce Provider Swap Window Cutoff
Given the provider has configured a swap cutoff window (e.g., 120 minutes before class start) And current time is within the cutoff window relative to the booked class start When a user attempts a sibling swap Then the swap is blocked And the error states the cutoff and the class start time in the user’s locale And the block is logged with rule_id "cutoff_window" and the configured cutoff minutes
Honor Non-Transferable Passes and Sponsorship/Grant Restrictions
Given the original booking is associated with a pass/bundle or sponsorship/grant And the item is marked non-transferable or restricted to a specific child/eligibility profile When a sibling swap to a different child is attempted Then the swap is blocked unless the item is explicitly transferable and the target child meets eligibility And the error cites the specific policy or item preventing the transfer And the block is logged with rule_id "benefit_nontransferable" or "sponsorship_restricted" with the item/grant identifier
Localized Error Messaging for Blocked Swap
Given the user has a locale preference and the organization has configured supported languages And a guardrail blocks a sibling swap When the error is displayed Then the message is shown in the user’s locale with correctly substituted variables (child name, class, time, policy) And if the locale translation is missing, the system falls back to the organization default, then to English And the message includes a human-readable title, actionable guidance, and a reference code And the response contains a machine-readable error_code and rule_id for client handling
Staff Override With Reason Capture and Audit Trail
Given a staff user with permission "Swap.Override" reviews a blocked sibling swap When the staff user selects Override and enters a required reason Then the swap proceeds despite the guardrail And the audit log records user_id, timestamp, overridden rule_ids, original child, target child, booking id, and reason text And the UI labels the enrollment as "Overridden" and the affected rule(s) in the admin timeline And notifications to roster/reminders reflect the final state post-override
Organization-Level Guardrail Configuration and Reusability
Given an organization admin configures guardrail rules via settings (overlap check on/off, limits values, cutoff minutes, override availability) When a sibling swap is evaluated through any channel (public booking page, staff console, API) Then the same guardrail engine applies the organization’s configuration consistently And defaults are applied only where the organization has not set a value And configuration changes are audited and take effect for new evaluations within 1 minute And rule definitions and outcomes are exposed via a versioned API for reuse across surfaces
Instant Roster and Communications Sync
"As an instructor, I want rosters and reminders to update instantly after a sibling swap so that I know exactly who is attending."
Description

When a swap succeeds, atomically replace the participant on the class roster, update attendance lists, badges, and check-in artifacts, and propagate the change to all downstream communications. Regenerate and send updated SMS/email reminders and calendar invites to the correct guardian/child, notify instructors of the roster change, and update exports and integrations (e.g., CRM, payroll/attendance). Ensure messages avoid duplicate sends, respect quiet hours, and reflect the latest consent preferences. Provide real-time UI updates for staff and a webhook/event for external systems.

Acceptance Criteria
Atomic Roster Swap and Attendance Artifact Regeneration
Given a sibling swap S (Child A -> Child B) for Class C has been approved When the swap S is executed Then the Class C roster contains Child B and no longer contains Child A within 1 second And the attendance list for Class C shows Child B for the swapped seat And badges and check-in QR/barcodes regenerate for Child B and any prior artifacts for Child A are invalidated within 1 second And concurrent reads see either the full pre-swap state or the full post-swap state, never a partial mix And an audit log entry is recorded with swapId S, oldParticipantId, newParticipantId, classId, actorId, and timestamp
Updated Guardian Communications and Calendar Invites
Given swap S (Child A -> Child B) for Class C has completed When reminders and invites are (re)generated Then any pending reminders/invites for Child A’s guardian are canceled or superseded And new SMS/email reminders are generated for Child B’s guardian using the latest consent preferences and templates And no duplicate messages are sent for the same swap; deduplicated by (swapId, channel, templateId) And if current time is within quiet hours, messages are queued for the next allowable window and marked as scheduled And calendar invites are updated: a new ICS is sent to the correct guardian for Child B and the prior ICS for Child A is canceled (UID preserved with method:CANCEL) And delivery outcomes are tracked per message (queued, sent, failed) with correlation to swapId
Instructor Roster-Change Notification
Given swap S for Class C has completed When notifying instructors Then each assigned instructor receives a single notification per configured channel within 60 seconds And the notification includes Child A name, Child B name, class date/time, location, and swapId And channel preferences are respected (e.g., SMS opted-out -> send email only) And duplicate notifications are prevented for the same swap and instructor
Real-time Staff UI Refresh
Given a staff user is viewing the Class C roster or check-in screen When swap S is executed Then the roster view updates to show Child B (replacing Child A) within 3 seconds without manual refresh And the check-in kiosk view only permits check-in for Child B and prevents check-in for Child A And a visible activity item/toast cites swapId S and the actor who performed the swap
Swap Webhook/Event for External Systems
Given a tenant has subscribed an endpoint E to class.swap.completed events When swap S completes Then a POST is sent to E within 10 seconds containing eventType, swapId, classId, oldParticipantId, newParticipantId, occurrenceId, timestamp, and idempotencyKey And the request is HMAC-signed with the tenant secret and includes a replay-protected timestamp header And deliveries are retried with exponential backoff for up to 24 hours until a 2xx is received And duplicate deliveries for the same idempotencyKey are not produced
Downstream Exports and Integrations Sync
Given CRM and attendance/payroll integrations and exports are enabled When swap S completes prior to the class start Then real-time integrations update to reference Child B instead of Child A for the impacted occurrence within 2 minutes And the next scheduled export includes an entry for Child B and excludes Child A for that occurrence And if an export already ran, a correction record is emitted referencing swapId S And all downstream records include swapId S for traceability
Audit Trail and Role-Based Controls
"As an owner, I want a complete audit trail and permissions around sibling swaps so that we remain compliant and can resolve disputes."
Description

Record a tamper-evident audit log for every swap including actor (guardian/staff), timestamp, original and target child, eligibility decision data, payment adjustments, and notifications sent. Expose RBAC to limit who can initiate swaps, approve exceptions, or view sensitive fields. Provide an easy-to-read change history on the booking and a downloadable report for compliance. Include privacy controls to redact child data from users without appropriate permissions and support data retention policies per organization and region.

Acceptance Criteria
Tamper-Evident Audit Entry on Sibling Swap
Given a sibling swap completes successfully When the system writes the audit entry Then the entry includes actor_type, actor_id, actor_role, timestamp_utc (ISO-8601), booking_id, class_id, original_child_id, target_child_id, eligibility_rules_evaluated (with pass/fail per rule), payment_adjustment {type, amount, currency, transaction_id|invoice_id}, notifications_sent [{channel, destination_masked, sent_at, status}], and request_id And the entry has immutable audit_id and previous_hash/current_hash values enabling chain verification And any attempt to modify or delete the entry via UI/API is blocked with 403 and is separately logged And verifying the chain via the integrity check endpoint returns "verified" for this entry and its neighbors And the entry is linked to the booking’s change history view
Role-Based Swap Initiation Controls
Given RBAC policies are configured for the organization When a guardian initiates a swap for a child they manage and self-service is enabled Then the swap initiation is allowed and logged with actor_type=guardian When a guardian attempts a swap for a child they do not manage or self-service is disabled Then the action is blocked with HTTP 403/UI error, no state change occurs, and a denied audit log is written When a staff member without permission "Swap:Initiate" attempts to start a swap Then the action is blocked with 403 and a denied audit log is written When a staff member with permission "Swap:Initiate" starts a swap Then the action proceeds and is logged with actor_type=staff
Exception Approval Workflow and Logging
Given a sibling swap fails eligibility due to one or more rules When a user with permission "Swap:ApproveException" approves the swap Then the system requires entry of reason_text (min 10 chars), scope (one-time|child|course), and optional expiry_at And the approval is logged with approver_id, reason_text, rules_overridden, scope, expiry_at in the audit entry And the swap proceeds and audit field approved_exception=true is recorded When a user without "Swap:ApproveException" attempts approval Then the action is blocked with 403 and a denied audit log is written
Privacy Redaction by Role in UI and Exports
Given a user without permission "Child:Sensitive:View" opens booking history or requests an audit export Then sensitive fields are redacted: child_full_name, date_of_birth, medical_notes, accommodations, guardian_contact (masked), while child_id and booking_id remain visible And exported files include requester_id, requested_at, and redacted=true in the export metadata When a user with permission "Child:Sensitive:View" accesses the same data Then full values are displayed/exported And all data accesses are audited with actor_id, resource, fields_redacted (true|false)
Booking Change History Readability and Linkage
Given a booking with at least one sibling swap event When a user opens the Change History tab Then entries are shown in reverse chronological order with event_type, timestamp_local, actor, before_child -> after_child, decision_summary, payment_delta, notifications_count, and integrity_status And each entry links to a detailed audit view by audit_id And the list can be filtered by event_type=SiblingSwap and date range And the first page loads under 2 seconds for up to 100 history entries
Compliance Report Export with Integrity Proof
Given a user with permission "Audit:Export" selects an organization/site and date range When an export is requested Then the system produces a downloadable package containing: audit.csv (required columns), manifest.json (total_count, hash_algorithm, per_entry_hashes, overall_digest), and signature.sig (signed by platform key) And the export content respects the requester’s RBAC redaction rules And the export request and download are logged with requester_id and IP And exports up to 100,000 entries complete within 5 minutes in staging benchmarks
Data Retention and Legal Hold Enforcement
Given retention_policy is set (EU=24 months, US=36 months) and legal holds can be placed When an audit entry exceeds its region’s retention window and is not under legal_hold Then the entry is purged or irreversibly anonymized per policy, and a retention_tombstone is recorded with audit_id, purge_at, performed_by=system And attempts to access the purged entry return 404 and are audited When a legal hold is placed for a defined scope Then entries in scope are exempt from purge until hold removal, and hold metadata (case_id, placed_by, placed_at) is visible to admins with "Retention:Manage" And only users with "Retention:Manage" can modify retention policies or place/remove legal holds; all changes are audited
Swap UX and API Surface
"As a platform partner, I want an API and polished UI for sibling swapping so that I can embed it in my branded experience with minimal work."
Description

Deliver a frictionless swap experience in the booking details view: a prominent Sibling Swap action, searchable sibling selector, real-time eligibility and price preview, explicit summary of changes, and one-tap confirmation with accessible error handling. Ensure mobile-first design, screen-reader compatibility, localization, and clear copy. Provide public/admin API endpoints and webhooks to initiate and monitor swaps programmatically, with idempotency keys and standardized error codes, enabling white-label partners to embed the flow. Include analytics events to measure usage, drop-off, and outcomes.

Acceptance Criteria
Sibling Swap Action Visibility (Mobile-First)
Given a signed-in guardian viewing Booking Details on a mobile viewport (≤414px width) When the page loads for an eligible booking Then a primary “Sibling Swap” button is visible without scrolling, has role="button", and accessible name "Sibling Swap" And the button is aria-disabled="true" with explanatory helper text when the booking is ineligible And tapping the button opens the Sibling Selector modal within 300ms on a 4G connection
Sibling Selector Search and Eligibility Indicators
Given a family account with two or more children When the Sibling Selector opens Then all siblings are listed with a search field and eligibility badges When the user types at least 2 characters in the search field Then results filter within 300ms per keystroke for up to 100 dependents And each sibling shows Eligible (with reason tooltip on focus) or Ineligible (with first failing reason), exposed to screen readers And list navigation is fully operable via keyboard (Arrow keys to move, Enter to select, Esc to close)
Real-time Eligibility & Price Preview
Given established eligibility rules, discounts, taxes, and credits When a sibling is selected in the selector Then eligibility is evaluated and a price delta preview is displayed within 400ms And the preview shows one of: "+$X.XX charge", "-$X.XX credit", or "Not eligible: <reason>" And the Confirm action is enabled only when eligible; otherwise it remains disabled with an inline error summary linked to detailed reasons And currency, date/time, and number formatting match the user locale
One-Tap Confirmation, Atomic Swap, and Payment Adjustment
Given an eligible selection and a stored payment method or wallet is available When the user taps Confirm Then the swap executes atomically: the original child is removed and the target child is added without exceeding capacity and without double-booking And if price delta > 0, the stored method is charged successfully; if no method exists, an inline add-payment flow is shown and required before proceeding And if price delta < 0, a credit is issued to the account wallet or original instrument per policy within 5 seconds And a confirmation screen summarizes child, class, schedule, location, instructor, and price delta; on failure, an accessible error is shown with retry and support options And duplicate taps or retries using the same idempotency key within 60 seconds do not create duplicate swaps or charges
Roster, Waitlist, and Reminder Updates
Given a successful swap Then the class roster reflects the new child and removes the original within 2 seconds, with an auditable swap record (who, when, from->to) And waitlists are not auto-enrolled during the atomic operation; capacity state remains consistent before/after the transaction And SMS/email reminders and calendar invites are updated to the new child/guardians within 1 minute and canceled for the original child And instructor/admin views display the updated roster on refresh with a "Swapped" badge for the affected entry
Accessibility and Localization Compliance
All interactive elements in the swap flow meet WCAG 2.2 AA: visible focus, contrast ≥4.5:1, target size ≥24x24 px, keyboard navigable Dynamic updates (eligibility result, price preview, errors) are announced via aria-live regions and have meaningful labels The flow supports LTR and RTL; all text, dates, times, and currency are localized; translations exist for at least en, es, fr; no hard-coded strings remain User-facing copy is clear and plain-language at or below Grade 8 reading level
API Endpoints, Webhooks, Error Codes, and Analytics
Given partner integration needs When partners call POST /v1/swaps with {booking_id, to_child_id, idempotency_key} Then the API returns 201 with {swap_id, status, price_delta, currency}; repeated requests with the same idempotency_key return 200/409 without creating duplicates And GET /v1/swaps/{id} returns current status and details And webhooks emit swap.initiated, swap.succeeded, swap.failed with signed payloads (booking_id, from_child_id, to_child_id, price_delta, currency, error_code), retried with exponential backoff for up to 24 hours And error codes are standardized (ELIGIBILITY_FAILED, PAYMENT_DECLINED, DOUBLE_BOOKING_PREVENTED, RATE_LIMITED) with appropriate HTTP statuses and machine-readable details And analytics events are emitted client- and server-side: swap_viewed, sibling_search_used, eligibility_result, price_preview_shown, swap_confirmed, swap_failed with properties {org_id, booking_id, from_child_id, to_child_id, device, locale, duration_ms, reason_code}, with ≥95% delivered within 5 minutes and deduplicated by event_id

Family Waitlist

Join waitlists as a family with options to “keep together” or “okay to split.” When spots open, seats are held per child and sent via timed pay-to-claim links. Smart rules prioritize keeping siblings together when possible, filling classes fairly while reducing staff juggling.

Requirements

Multi-Child Waitlist Enrollment
"As a parent, I want to add all my children to a class waitlist in one flow so that I don’t have to submit separate entries and can manage our place together."
Description

End-to-end support for adding multiple children to a single waitlist entry from a family account. Captures child profiles, class selection, and per-child eligibility data (age, level, waivers). Validates schedule conflicts and prevents duplicate or overlapping entries. Persists a family-scoped waitlist record with child-level line items and positions. Integrates with ClassTap’s booking data model and guards against double-bookings across classes.

Acceptance Criteria
Add Multiple Children to a Single Waitlist Entry
Given a logged-in family account with at least two child profiles and an open waitlist for Class X When the parent selects Class X, selects Children A and B in a single flow, chooses a split preference (Keep Together or Okay to Split), and submits Then the system creates exactly one family-scoped waitlist record with a unique waitlistId, stores the split preference, and creates one child-level line item per selected child And the response returns the waitlistId, the list of child line items with childId and initial status=PENDING, and the recorded split preference And an activity log entry is created for the family account
Per-Child Eligibility Validation at Enrollment
Given Class X has eligibility rules (age range, level, required waivers) When the parent attempts to add Children A and B to the waitlist Then each child is validated against Class X rules using the child profile and the class start date And any child failing validation is blocked with an inline error specifying the failing rule And submission is allowed only when all selected children pass validation or ineligible children are removed from the selection
Detect and Block Schedule Conflicts and Duplicate Entries
Given children may already have bookings or waitlist entries When the parent submits a multi-child waitlist request for Class X Then any child with an existing booking overlapping Class X’s time window is blocked with a conflict message and not added And any child already on the waitlist for the same class/time is not duplicated and an "Already on waitlist" message is shown And any child with an overlapping waitlist hold or pending booking in another class is blocked and not added And the submission proceeds for non-conflicting children only; if none remain, no new waitlist record is created
Persist Family-Scoped Waitlist Record with Child Line Items
Given a successful multi-child waitlist submission When the system saves the record Then it persists a family waitlist entity with fields: waitlistId, familyId, classId, splitPreference, createdAt And it persists child line items with fields: waitlistId, childId, eligibilitySnapshot(ageAtStart, level, waivers), status=PENDING, position (assigned per rules) And the operation is atomic; either all records are saved or none And fetching the waitlist via API returns the family entity and associated child line items
Assign Waitlist Positions Respecting Split Preference
Given positions are assigned in chronological order of submission When a family submits with Keep Together for N children Then the children receive consecutive positions as a contiguous block starting at the next available position, with no other entries interleaved When a family submits with Okay to Split for N children Then each child receives the next available position independently at submission time And position timestamps reflect the submission time to ensure ordering fairness
Seat Holds and Pay-to-Claim Links per Child
Given waitlist entries exist and seats become available for Class X When N seats open and the next family group with Keep Together has K children waiting Then if N >= K, the system creates K seat holds (one per child), generates unique pay-to-claim links, sets an expiry window, and delivers links via configured channels (email/SMS) And if N < K and the preference is Keep Together, no holds are issued for that family and the system evaluates the next entry without breaking fairness And if the preference is Okay to Split, the system issues holds for up to N children in position order And upon timely payment, a booking is created for that child, the hold is consumed, and the line item status updates to BOOKED; upon expiry, the hold is released and the line item returns to WAITING
Idempotent and Resilient Submission
Given the client supplies an idempotency key with a multi-child waitlist submission When the same payload is retried within 5 minutes Then the system returns the original waitlist record without creating duplicates or new positions And if an identical submission arrives without an idempotency key within 60 seconds (same familyId, classId, children set), the system de-duplicates and returns the existing record And any partial failure results in a full rollback and an error response with no partial records persisted
Keep-Together/Split Preference
"As a guardian, I want to choose whether my kids must attend together or can be split so that the system offers seats that match our needs without manual coordination."
Description

Allow families to specify whether siblings must be kept together or can be split across available spots. Store the preference per waitlist entry and enforce it during offer generation. If “keep together” is selected and insufficient seats are available, the family retains position until enough seats open. If “okay to split” is selected, partial offers are generated per child as seats become available, tracking remaining unmet seats. Support updating the preference after join, with audit trail and rules to avoid queue gaming.

Acceptance Criteria
Preference capture at waitlist join
Given a family selects children to join a class waitlist When they attempt to submit without selecting a preference Then the system blocks submission and displays an inline error requiring selection of either "Keep Together" or "Okay to Split" Given a family selects a valid preference and submits When the request is processed Then the waitlist entry stores the preference value, the child IDs, and required_seats = number of selected children And the stored preference is retrievable via API and staff UI and is one of {"KEEP_TOGETHER","OK_TO_SPLIT"} and non-null
Keep Together offer generation and blocking logic
Given a head-of-queue family with preference KEEP_TOGETHER and required_seats = K When available_free_seats S < K Then no offer is generated to that family and their queue position remains unchanged And subsequent families may be evaluated for offers using remaining seats according to their preferences Given the same family with S >= K When an offer cycle runs Then a single group offer is generated holding K seats with a timed pay-to-claim window (configurable) And partial acceptance is not allowed; attempts to claim fewer than K seats are rejected And on acceptance, K seats are booked atomically; on expiry or decline, all K seats are released and the family's position is unchanged
Okay to Split partial offers and remaining need tracking
Given a family with preference OK_TO_SPLIT and required_seats = K and remaining_needed = R (initially K) When available_free_seats S > 0 Then the system generates up to min(S, R) individual child offers, each with a unique pay-to-claim link and hold window (configurable) And each accepted offer books exactly one child and decrements remaining_needed by 1 And no more than one active offer exists per child at a time And when an offer expires or is declined, the held seat is released and remaining_needed is unchanged And when remaining_needed reaches 0, no further offers are generated for that family And the family's relative queue position for any remaining_needed is preserved based on original join timestamp
Preference update after join with anti-gaming safeguards
Given a family has a waitlist entry When they attempt to change preference while any active (unexpired, unaccepted) offers exist for that entry Then the update is blocked with a clear explanation Given no active offers exist and last update was at least cooldown_hours (configurable, default 12) ago When the family submits a preference change Then the update succeeds and applies prospectively And the waitlist entry's original join timestamp and position are preserved And changing from OK_TO_SPLIT to KEEP_TOGETHER does not cancel already accepted bookings; it only affects remaining_needed And changing from KEEP_TOGETHER to OK_TO_SPLIT enables partial offers in the next offer cycle based on current remaining_needed
Preference audit trail and staff visibility
Given a preference is set at join or updated later Then an audit event is recorded with: waitlistEntryId, familyId, actorId, actorRole, timestamp (UTC), oldValue, newValue, reason (optional), channel (UI/API), requestId And staff with appropriate permissions can view the chronological audit history on the waitlist entry detail screen and export it as CSV And an API endpoint returns the audit trail for an entry; unauthorized requests receive 403
Fair allocation with mixed preferences and changing seat availability
Given the offer engine runs When the first-in-queue family is KEEP_TOGETHER with required_seats = K and available_free_seats S < K Then no offer is issued to that family, and the engine evaluates subsequent families in FIFO order, issuing offers consistent with their preferences until seats are exhausted Given later seat changes increase available_free_seats to >= K and there is no active group offer for that first family When the next offer cycle runs Then the engine prioritizes issuing a group offer to that family before issuing any new offers to later families And already-issued offers to later families are honored and not revoked; availability for the head-of-queue family is computed using only unheld, unbooked seats And tie-breaking for identical join timestamps uses waitlistEntryId ascending for deterministic ordering
Timed Pay-to-Claim Holds
"As a parent on a waitlist, I want to receive a timed link to claim and pay for my child’s seat so that I can secure the spot quickly without calling the studio."
Description

Automatically create time-bound seat holds when spots open and send secure, tokenized pay-to-claim links via SMS and email. Pre-reserve seats per child according to the family’s preference and class capacity. Configure hold duration per organization; show countdown in the checkout. On expiration or decline, release seats and advance the next eligible family. Prevent link reuse, enforce single-claim per hold, and deep-link to a pre-filled checkout with the correct children, class occurrences, and pricing.

Acceptance Criteria
Auto-Hold for Keep-Together Family
Given an organization hold duration is set to 15 minutes and a class occurrence has 2 available seats and a waitlisted family with 2 children is marked "keep together" When the system detects the availability Then a single hold is created for 2 seats assigned to those children with an expiry exactly 15 minutes from creation And tokenized pay-to-claim links are generated and dispatched via SMS and email to the primary contact within 30 seconds And the hold is visible in admin with status "Held", the child names, seat count, delivery statuses, and expiry time And no other waitlisted party receives a hold for those seats while this hold is active
Okay-to-Split with Partial Seats
Given an organization hold duration is set to 10 minutes and a class occurrence has 1 available seat and a waitlisted family has 2 children marked "okay to split" with per-child waitlist order Child A before Child B When the single seat becomes available Then a hold is created for 1 seat assigned to Child A with a 10-minute expiry And a tokenized link referencing Child A is sent via SMS and email to the primary contact And Child B remains on the waitlist When a second seat opens while the first hold is still active Then a separate hold is created for Child B with its own 10-minute expiry and tokenized links And each hold can be claimed independently without affecting the other
Secure Tokenized Links, Deep-Link Pre-fill, Single-Claim
Given a valid active hold with a tokenized pay-to-claim URL When the link is opened Then checkout loads the correct class occurrence with the correct pre-selected child(ren), pricing items, and total per the organization’s rules And a countdown shows the remaining hold time When payment is successfully completed once Then the token is invalidated and subsequent attempts to use the link return "Hold already claimed" and block checkout When multiple devices attempt to pay with the same token concurrently Then only the first successful payment confirms the booking and all other attempts are blocked with a non-retryable error When the link is opened after the expiry timestamp Then the user sees "Hold expired" and is redirected to the class page and no seats are held
Checkout Countdown Accuracy and Expiry Enforcement
Given hold duration is set to 10 minutes and a hold was created 3 minutes ago When the recipient opens checkout via the pay-to-claim link Then the countdown displays 7:00 remaining and decrements in real time synchronized to server time And refreshing or reopening the page does not reset or increase the remaining time When the countdown reaches 0 before payment confirmation Then the Pay/Confirm action is disabled, an "Hold expired" message is shown, and the hold is released And the user cannot complete payment more than 5 seconds after expiry
Expiration/Decline Releases Seats and Advances Next Family
Given a waitlist with multiple eligible families and an active hold for Family A When Family A’s hold expires or is explicitly declined Then the held seats are immediately returned to inventory And a new hold is created for the next eligible family according to waitlist order and rules within 30 seconds And Family A’s tokenized link is invalidated and returns "Hold expired" on access And an audit log captures hold creation, notification sends, expiry/decline, seat release, and next-hold creation events
Decline Handling for Split vs Keep-Together Holds
Given an active hold for 2 children under a "keep together" preference When the recipient chooses "Decline" Then both seats are released and the system advances the next eligible family Given two independent holds for children under an "okay to split" preference When the recipient declines one hold Then only that child’s held seat is released while the other hold remains active and claimable And the declined hold’s token becomes invalid immediately
Fairness Queueing and Promotion Engine
"As an administrator, I want a fair and transparent promotion process so that families trust the waitlist and staff spend less time resolving disputes."
Description

Implement a deterministic, auditable algorithm to prioritize families and promote children to holds. Preserve FIFO order at the child-entry timestamp while applying sibling cohesion rules that attempt to seat siblings together when possible. Handle edge cases such as partial capacity, class cancellations, and schedule overlaps. Enforce anti-gaming constraints (one active entry per child per class, join/leave throttles) and maintain an audit log of promotions, skips, and reasons. Provide configuration flags for tie-breakers where needed.

Acceptance Criteria
FIFO Promotion by Child Timestamp
- Given multiple waitlist entries with distinct child join timestamps, When seats open, Then children are promoted strictly in ascending child join timestamp order, subject to sibling rules. - Given two or more entries share an identical timestamp, When ordering is needed, Then the configured tie-breaker (e.g., deterministic seeded order by waitlistId+childId) is applied and recorded in the audit log. - Given promotions occur, When reviewing the audit log, Then each promotion record includes childId, familyId, classId, fromPosition, toHoldId, ruleApplied, timestamp, and deterministic ordering key.
Sibling Cohesion — Keep Together
- Given a family with N siblings marked "keep together" on the same class waitlist, When fewer than N seats are available, Then none of those siblings are promoted and a skip event is logged with reason "insufficient seats to keep together". - Given a family with N siblings marked "keep together", When at least N seats are available concurrently, Then exactly N holds are created simultaneously for those siblings and notifications are sent per child. - Given a "keep together" family is skipped due to insufficient seats, When other children behind them can be promoted, Then the engine temporarily bypasses the family and continues promotions, preserving the family's relative FIFO position and logging the bypass.
Sibling Cohesion — Okay to Split
- Given a family with M siblings marked "okay to split", When K seats (K < M) are available, Then up to K siblings are promoted in FIFO order by individual child timestamp and separate holds are created per promoted child. - Given remaining siblings of an "okay to split" family are still on the waitlist, When additional seats open, Then they are considered next according to their original child timestamps; prior sibling promotions do not alter their priority. - Given mixed families with "keep together" and "okay to split" preferences exist, When promotions run, Then the engine honors each family's preference without allowing "okay to split" promotions to leapfrog earlier children beyond the configured tie-breaker rules, and records applied preference per event.
Timed Holds and Pay-to-Claim Links
- Given a child is promoted, When a hold is created, Then a unique pay-to-claim link is generated and sent via configured channels within 30 seconds, and the hold expires after the configured duration (default 15 minutes). - Given a hold expires unpaid, When the expiration occurs, Then the seat is released immediately, the next eligible child is evaluated for promotion within 60 seconds, and an audit log entry with reason "hold expired" is recorded. - Given a child has an active hold for a class, When the family attempts to rejoin the waitlist for the same class, Then the attempt is rejected with error "active hold exists" and an audit entry is recorded.
Anti-Gaming and Throttling Enforcement
- Given a child attempts to join a class waitlist where they already have an active waitlist entry, hold, or confirmed enrollment, When the join request is made, Then it is rejected with a 409 conflict and reason "duplicate per class per child". - Given a user performs more than X join/leave actions for the same child and class within Y minutes (configurable), When additional actions are attempted, Then the system returns a 429 rate limit error and logs a throttle event with counts and window. - Given a child leaves a waitlist and later rejoins, When the rejoin is accepted, Then the child's join timestamp is set to the new time and the audit log links the new entry to the previous one with reason "rejoin resets priority".
Partial Capacity, Cancellations, and Schedule Overlaps
- Given class capacity increases by K seats due to cancellations, When the promotion engine runs, Then exactly K holds are created for the next eligible children per FIFO and sibling rules, unless blocked by preferences, in which case skips are logged and bypass applied. - Given a child is eligible for promotion but the class time overlaps with another active enrollment or hold for that child, When evaluating promotion, Then the child is skipped with reason "schedule overlap" and the next eligible child is evaluated; the skipped child remains in the waitlist unless removed by the family. - Given a class is fully canceled, When cancellation occurs, Then all active holds are voided, all waitlist entries are closed with reason "class canceled", notifications are sent, and no promotions are attempted.
Deterministic Runs and Configuration Flags
- Given identical system state and configuration snapshot, When the promotion engine is run multiple times (including dry-run mode), Then it produces the same ordered list of promotions, holds, and skips with identical audit records and deterministic ordering keys. - Given tie-breaker configuration is changed, When promotions subsequently run, Then audit entries include the configuration version/hash and the applied tie-breaker name for each decision. - Given an administrator exports the audit log for a class and date range, When the export is generated, Then it contains immutable records for joins, leaves, promotions, skips, holds created, holds expired, cancellations, and throttle events with ISO-8601 timestamps and actor identifiers.
Staff Waitlist Management Console
"As a studio manager, I want tools to view and adjust family waitlists so that I can resolve edge cases and keep classes full with minimal effort."
Description

Provide an admin view to see class waitlists with family grouping, per-child status, preferences, and positions. Enable manual actions: promote/demote, override “keep together,” convert holds to bookings, resend or cancel offers, merge duplicate family entries, and add staff notes. Include filters, search, and export. Display real-time timers for active holds and a history of notifications and claim outcomes for support.

Acceptance Criteria
View Waitlist with Family Grouping and Child-Level Details
Given I am a staff user with Waitlist Admin permissions When I open the Waitlist Management Console for a class with active waitlist entries Then I see families grouped with a family header and each child listed as a row And each child row displays: child name, family name, preference (Keep Together/Okay to Split), per-child status (Waiting/Offered/Held/Expired/Booked/Removed), list position, and joined timestamp And the list is ordered by position ascending by default And active holds display a countdown timer in mm:ss that decrements every second And when a hold expires, the child status updates to Expired and the held seat is released within 5 seconds And selecting a family opens a history panel showing notifications sent and claim outcomes in chronological order And changes made by any user are reflected in my view within 2 seconds without a full page refresh And the initial list loads within 2 seconds for up to 500 waitlist children
Promote and Demote Positions with Audit Trail
Given a class waitlist with multiple children And I have Waitlist Admin permissions When I promote or demote a child or an entire family group Then the new positions persist across sessions And position changes respect active holds (children with active holds cannot be moved below their held rank) And siblings maintain relative ordering when Keep Together is set unless I explicitly override And the UI immediately reflects the updated positions And an audit entry is recorded with user, timestamp, action type, and before/after positions
Override Keep Together Preference
Given a family with preference Keep Together on the waitlist When I choose Override Keep Together for the family Then I am required to enter a reason of at least 10 characters And the override is recorded with user, timestamp, and reason in the audit history And the family header indicates Keep Together (Overridden) And the system allows separate offers and promotions for the siblings thereafter And I can revert the override to restore the original preference
Convert Hold to Booking
Given a child has an active timed hold for a seat in a class And I have Waitlist Admin permissions When I select Convert Hold to Booking Then a booking record is created for that child in the class And the hold timer stops and the waitlist status changes to Booked And the class seat inventory is decremented accordingly And I am prompted to set payment status (Captured/Comp/Unpaid) and my selection is stored on the booking And conversion is blocked for expired or canceled holds with a clear error message And a confirmation appears with a link to the booking detail and an audit entry is recorded
Resend and Cancel Offer Links
Given a child has an active offer or hold When I click Resend Offer Then an SMS/email is sent to the family’s primary contacts with the existing claim link and unchanged expiry time And a history entry is added with user, timestamp, channel(s), and delivery attempt status When I click Cancel Offer Then the offer/hold is immediately voided, the status becomes Canceled, the timer stops, and the held seat is released And the family is notified via the same channels And the history log records the cancel action and notifications
Merge Duplicate Family Entries
Given two or more waitlist entries are identified as the same family When I select Merge and choose the canonical family contact details Then all children from the selected families are combined under one family group And notes from all merged entries are preserved and appended with source references And positions are recalculated so the merged family’s position equals the earliest join position among the merged entries, with children ordered by their original relative order And no duplicate child is created; existing per-child statuses are retained And an audit record captures user, timestamp, pre/post snapshots, and merge rationale
Search, Filter, and Export Waitlist
Given I am viewing a class waitlist with entries When I search by family name, child name, email, or phone Then results are filtered to matching entries with matches highlighted When I apply filters by status, preference, date joined range, or has staff notes Then the list updates within 500 ms to reflect the filters When I click Export CSV Then a CSV downloads containing only the currently filtered results with columns: familyId, familyName, childName, status, preference, position, joinedAt, offerStatus, holdExpiresAt, staffNotes And exported timestamps are ISO 8601 UTC and the file name includes the class id and export timestamp
Branded Notifications and Preferences
"As an operator, I want customizable notifications so that families receive clear, on-brand instructions that drive timely claim conversions."
Description

Deliver white‑label SMS and email messages for waitlist join confirmations, offer sent, reminders, expiry, and outcome. Support per-organization templates, placeholders (child name, class, date, timer), sender branding, and localization. Respect user communication preferences and opt-outs, apply rate limits, and retry on transient failures. Track delivery and engagement metrics and surface them in the admin console.

Acceptance Criteria
Template Rendering with Placeholders and Localization
Given an organization has active SMS and email templates with placeholders {child_name}, {class_name}, {class_start}, {offer_timer_minutes}, {claim_link}, and {org_brand} and a selected locale When a waitlist notification event (join, offer, reminder, expiry, outcome) is generated Then the locale-specific template variant is selected; if missing, fall back to the org default locale And all placeholders are rendered with correct values per child and class And date/time fields are formatted according to the selected locale and org timezone And messages with unresolved or invalid placeholders are not sent; the send is blocked, validation errors are logged with template_id and placeholder names
Sender Branding Configuration Applied
Given an organization has configured email from_name, from_address (verified), reply_to, and an SMS sender number When any notification is sent Then emails use the configured from_name/from_address and reply_to values And SMS messages originate from the organization’s configured sender number And the organization brand name appears per template rules (subject for email or first line for SMS) And if any sender setting is missing or unverified, the message is suppressed, a configuration error is logged, and an admin alert is queued
Communication Preferences and Opt-Out Compliance
Given a recipient has channel preferences (email and/or SMS) and may have opted out per channel or globally When a notification would be sent Then only non-opted-out, allowed channels are used; suppressed sends are recorded with suppression_reason and event_type And each email includes a manage-preferences/unsubscribe link; each SMS includes HELP/STOP footer where required And inbound STOP/UNSUBSCRIBE messages update the recipient’s opt-out within 60 seconds and are honored for subsequent sends And no notification is sent to recipients marked as global opt-out; attempts are logged but not delivered
Rate Limiting, Throttling, and Retry Policy
Given notifications are queued for a recipient across events When multiple messages are eligible within a short time window Then send no more than 3 SMS and 3 emails per recipient per rolling 1-hour window; excess are deferred with reason=rate_limited And identical event notifications to the same channel are deduplicated within a 2-minute window When transient provider errors (HTTP 5xx, timeouts) occur Then retry up to 3 times with backoff of 1m, 5m, 15m; on HTTP 429 respect Retry-After And on permanent errors (HTTP 4xx except 429) do not retry; mark final status=failed with provider_error_code
Family Waitlist Event Coverage and Content Correctness
Given the family waitlist lifecycle includes: join confirmation, offer sent, offer reminder, offer expired, and outcome When a parent joins the waitlist Then send a confirmation per child added with class name and class_start When an offer is created with keep_together=true Then send a single combined message listing all children and one claim_link When an offer is created with keep_together=false Then send separate messages per child, each with its own claim_link When a reminder is due 15 minutes before expiry and the offer is unclaimed Then send a reminder reflecting the remaining minutes at render time When an offer expires Then send an expiry message and invalidate prior claim links When the outcome is determined (claimed or expired) Then send an outcome message reflecting the final status per child or family offer
Timed Claim Link Validity and Tracking
Given each offer has a claim_link token scoped to the child (or family for keep_together) with an expiry_at timestamp When the link is included in notifications Then the link is unique per offer-child or offer-family and cannot be used to claim other seats And the link expires exactly at expiry_at; post-expiry clicks show an expired state And each open/click is tracked with message_id, recipient_id, event_type, and timestamp And click metrics are attributed to the originating notification in analytics
Delivery and Engagement Metrics in Admin Console
Given an organization admin views Notifications analytics When a date range and filters (event type, channel) are applied Then the dashboard displays counts for sent, delivered, failed, and suppressed per channel and event type And shows email open rate, email/SMS click-through rate, unsubscribe rate, and send-to-delivery latency p50/p95 And provides a message log with per-message status timeline (queued, sent, delivered, failed/suppressed) and a safe rendered preview And supports CSV export of the filtered dataset And metrics update within 5 minutes of new events
Checkout and Payment for Multi-Child Claims
"As a parent, I want to pay once for all claimed seats so that the process is quick and I have a single receipt."
Description

Integrate waitlist claims with ClassTap’s checkout to allow claiming seats for one or multiple children in a single transaction. Pre-populate the cart with held seats, apply taxes, discounts, credits, and promo codes, and generate receipts. If payment fails, keep the hold until timer expiry and allow retry. Ensure PCI-compliant tokenization for stored methods and guard against double charges or race conditions when multiple offers exist.

Acceptance Criteria
Multi-child claim cart pre-population
Given a valid waitlist claim link holding seats for Child A and Child B with a 10-minute expiration When the user opens the link and views checkout Then the cart contains separate line items for each child’s held seat with class name, date/time, and per-seat price And the hold countdown timer displays the remaining time synchronized to server time with ≤2 seconds drift And taxes are calculated per seat according to the class’s tax rules And the order subtotal, taxes, and total are displayed and match the pricing configuration And the user can proceed to payment without manually adding items
Discounts, credits, and promo codes calculation
Given the user has $50 account credit and a valid promo code PROMO10 (10% off) and the class has a $100 per-seat price with 8% tax When the user applies PROMO10 at checkout for two held seats Then per-seat discounts are applied first, computing a discounted subtotal of $180.00 And tax is computed on the discounted subtotal at 8% = $14.40 And account credit is applied to the post-tax total up to the remaining balance, reducing the amount due to max($0, $194.40 − $50.00) And the cart prevents stacking multiple conflicting promo codes and validates code eligibility and expiration And the displayed totals match backend calculations to the cent
Successful payment with stored token and receipt generation
Given the user selects a stored tokenized card and all held seats are valid When the user confirms payment Then a single authorization-and-capture is executed for the exact amount due And the seats transition from Held to Enrolled immediately after capture success And an itemized receipt is generated with per-child line items, discounts, credits, taxes, masked card details (brand, last4), and transaction ID And the receipt is emailed to the payer and visible in Order History within 60 seconds
Payment failure retry with hold persistence until expiry
Given the user’s initial payment attempt fails due to card decline or network error When the failure occurs before hold expiration Then the hold remains active with the original expiration time and the user is shown an actionable error And the user can retry payment with the same or a different method without duplicating cart items And no charge is captured for failed attempts and no receipt is issued And if the user completes payment on retry before expiry, the order is processed successfully as a single transaction
Idempotency and double-charge protection across duplicate submits and tabs
Given the user submits payment multiple times rapidly or from separate tabs for the same held seats When the checkout service processes the requests using a consistent idempotency key per cart Then at most one charge is captured and subsequent requests return an idempotent success or already-processed response And the UI reflects the paid state across tabs within 5 seconds, preventing further payment attempts And no duplicate receipts are generated
Hold expiration during checkout
Given the checkout page is open with 10 seconds remaining on the hold When the hold expires before payment capture completes Then the system blocks capture, releases the seats, clears the cart, and displays that the offer expired And no charge is captured and no receipt is generated And the user can return to the waitlist or rejoin per policy
PCI-compliant tokenization for new and stored payment methods
Given the user adds a new card at checkout When the card details are entered Then card data is sent directly to the payment processor over TLS and a token is returned to ClassTap And PAN and CVV are never stored or logged by ClassTap and are redacted in client and server logs And saved methods display only brand and last4 and can be deleted, revoking the token And 3DS/SCA challenges are triggered and completed when required before capture

QuickScan Kiosk

A dedicated, high-contrast kiosk view that displays the class QR, supports continuous scanning, and gives instant green/red feedback with attendee names. Staff see live counts, capacity, and status badges as scans happen, with a fast manual lookup fallback. Speeds lines, reduces errors, and keeps the roster accurate in real time.

Requirements

High-Contrast Kiosk UI
"As a front-desk staff member, I want a simple, high-contrast kiosk screen so that I can process check-ins quickly without visual clutter or mis-taps during rush periods."
Description

Provide a locked, full-screen, high-contrast interface optimized for check-in flow, showing class title, instructor, start time, capacity, and the class-specific QR for quick pairing or self-service access. The UI uses large typography, color-blind-safe palettes, and clear status badges to remain readable at distance and in bright environments. Includes screen orientation lock, inactivity dim/wake, guided setup, and single-app mode support on iOS/Android tablets and browser-based kiosks. Integrates with ClassTap class data and branding theming to preserve white-label consistency while minimizing on-screen distractions during busy arrivals.

Acceptance Criteria
Full-Screen, Locked High-Contrast Kiosk UI
Given a supported tablet or browser device with QuickScan Kiosk launched When the kiosk view loads Then the UI occupies 100% of the screen with system status/navigation UI hidden or blocked And all primary text and controls meet WCAG 2.1 AA contrast (>= 4.5:1 for normal text; >= 3:1 for large text) And all primary tap targets are >= 44x44 px with >= 8 px spacing And no non-kiosk navigation or external controls are visible
Accurate Class Header Information
Given the kiosk is paired to a specific class When the kiosk displays the header Then it shows class title, instructor name, start time, and capacity (current/total) And changes to these values in ClassTap propagate to the kiosk within 5 seconds when online And when offline, an "Offline" badge and last-synced timestamp are shown without stale values being presented as live
Class-Specific QR Display and Pairing
Given the kiosk is in setup or check-in mode When it renders the class QR Then the QR encodes a class-specific, signed tokenized URL valid for the scheduled class window And the QR has edge length >= 240 CSS px, quiet zone >= 4 modules, and error correction level >= M And the QR is scannable on reference iOS/Android devices at 30–80 cm in 300–2000 lux with a successful read in < 2 seconds for >= 95% of attempts
Color-Blind-Safe Status Badges and Feedback
Given an attendee is checked in, denied, or flagged as duplicate When the kiosk shows result feedback Then the feedback includes color, icon, and text label so meaning is not conveyed by color alone And feedback passes color-blind simulations (protanopia, deuteranopia, tritanopia) without loss of meaning And status message text is >= 24 px and badge text is >= 18 px with contrast >= 4.5:1 against background
Orientation Lock and Inactivity Dim/Wake
Given a staff user selects portrait or landscape during setup When the device is rotated Then the kiosk maintains the selected orientation with no layout breakage or clipped controls And after 2 minutes of no scans or taps, the kiosk dims the UI by >= 60% and shows "Tap or scan to wake" And on tap or successful scan, full brightness is restored within 500 ms while keeping the camera ready
Guided Setup and Single-App Mode Support
Given a staff user initiates kiosk setup When following the guided steps Then setup completes in <= 4 steps and <= 60 seconds with network available And guidance includes optional enablement instructions for iOS Guided Access and Android App Pinning, with deep links or OS-specific prompts where supported And in browsers, a prompt enables full-screen mode and disables accidental navigation via clear instructions and re-prompting if exited
Branding Theme Compliance and Minimal Distraction
Given an organization has branding (logo, colors) configured When the kiosk theme is applied Then the logo and brand colors appear on headers/buttons while core content remains uncluttered And applied theme maintains WCAG 2.1 AA contrast; if violated, a high-contrast fallback is auto-applied And the main check-in screen presents no more than 5 interactive elements and no promotional popups or external links
Continuous Scanning Engine
"As a staff member, I want the kiosk to scan attendees continuously without extra taps so that lines move quickly and no one gets double-checked by accident."
Description

Enable continuous QR/barcode scanning via device camera with low-latency decoding, autofocus, glare mitigation, and frame de-duplication to support back-to-back scans without manual taps. Accept common 2D/1D symbologies and prioritize ClassTap signed QR payloads, rejecting unknown formats for security. Implement debounce and per-attendee rate limits to prevent double check-ins. Offline-ready with locally cached roster tokens and queued events that sync when connectivity returns. Designed for ≥4 successful scans per second under good conditions with resilient performance on mid-tier tablets.

Acceptance Criteria
Sustained High-Throughput Continuous Scanning
Given QuickScan Kiosk mode with camera active and a locally cached roster of at least 200 attendees And a test rig presents 200 unique valid ClassTap-signed QR codes at 4–5 codes/second under 500–800 lux at 20–35 cm When scanning proceeds for 40 seconds without manual interaction Then at least 160 unique check-ins are recorded And median time from code entry into frame to visual/audible feedback is ≤200 ms and p95 ≤300 ms And no modal dialogs or navigation interrupt the camera preview And staff-facing live counts update within 300 ms of each successful scan
Duplicate Prevention and Rate Limiting
Given continuous scanning is active And the same attendee QR remains in frame across multiple frames When detections occur within a 750 ms debounce window Then only the first detection is processed and subsequent detections are ignored And if the same attendee QR is re-presented within 10 seconds, the system shows "Already checked in" with amber feedback and no additional check-in event is created And the attendee's check-in count after 10 scans in 3 seconds remains 1 And the event log contains exactly one check-in event and the remaining detections are recorded as ignored-duplicate entries
Symbology Acceptance and Prioritization
Given continuous scanning is active When presented with supported symbologies: QR, Data Matrix, PDF417, Code 128, Code 39, and EAN-13 Then the system decodes each and extracts payloads And when multiple barcodes are present concurrently, a valid ClassTap-signed QR is preferentially decoded and processed over other codes And unsupported or malformed symbologies result in red feedback with "Unsupported code" and no check-in event And non-ClassTap payloads that do not match whitelisted formats are rejected with no state change
Signature Verification and Roster Authorization
Given a ClassTap QR with an invalid signature, an expired token, or for a different class instance When it is scanned Then the system rejects with red feedback within 300 ms, logs a reason code (invalid_signature|expired|wrong_class), and makes no roster changes And given a valid signature for a user not on the roster or when class capacity is exceeded When scanned Then the system rejects with red feedback and does not increase the checked-in count And given a valid, signed QR for a rostered attendee When scanned Then the system accepts, increments the class check-in count by 1, and displays the attendee name with green feedback
Offline Check-In with Local Cache and Deferred Sync
Given the device is offline and the class roster tokens are cached locally When a valid signed QR for a rostered attendee is scanned Then a local check-in event is created with scan timestamp, attendee ID, and token hash, and green feedback includes an "Offline" badge And the event is persisted to durable local storage and survives app relaunch And duplicates are prevented using the same debounce and rate-limit rules while offline And when connectivity is restored, queued events sync automatically within 10 seconds, preserving original scan timestamps And the server-side roster reflects the correct number of check-ins with no duplicates after sync
Autofocus, Exposure, and Glare Mitigation Feedback
Given continuous autofocus and auto-exposure are enabled When a valid QR behind glossy plastic is presented at 20–35 cm under 500–800 lux Then 90% of 50 scans complete successfully within 2 seconds each without user tap-to-focus And if no decode occurs within 1 second, an on-screen prompt appears instructing the user to adjust angle or distance And after the prompt appears, 80% of the remaining attempts decode within the next 1 second
Mid-Tier Tablet Performance and Stability
Given a mid-tier tablet (e.g., iPad 7th gen or Samsung Tab A8) and good lighting conditions When scanning proceeds continuously at an average of 3–4 scans/second for 15 minutes Then the app maintains a preview framerate of ≥24 FPS, average CPU usage ≤70%, memory footprint ≤400 MB, and zero crashes And no frame stalls >500 ms are observed, and end-to-end feedback p95 latency is ≤350 ms And the device does not enter a throttled thermal state during the test
Instant Validation & Feedback
"As a staff member, I want clear green/red feedback with attendee names and reasons so that I can resolve issues instantly and keep the line moving."
Description

On each scan, validate the token against the class roster, payment status, and booking state, then provide immediate, unambiguous feedback: green for successful check-in with attendee name, red for invalid/duplicate/wrong-class with a short reason, and amber for edge cases (e.g., waitlist, unpaid, late arrival). Display feedback for a brief, configurable duration with optional sound and haptic cues, ensuring PII fades after a few seconds. Persist check-in events with timestamps, device ID, and operator session; enforce idempotency to avoid duplicates and update the roster in real time. Target end-to-end validation latency under 300 ms online and graceful offline confirmation when using trusted cached data.

Acceptance Criteria
Online Successful Scan Latency and Feedback
Given a valid, paid booking token for the current class and active network connectivity When the token is scanned by the kiosk Then end-to-end validation completes in ≤300 ms at p95 and ≤150 ms at p50 And the screen flashes green with the attendee full name and “Checked in” And the live attendee count increments within 250 ms of success And a success sound and haptic pulse play if enabled in device settings
Idempotency and Duplicate Scan Prevention
Given a booking has already been successfully checked in for this class When the same token is scanned again on the same or another kiosk Then the feedback is red with the reason “Already checked in” and the original check-in timestamp And no second check-in event is persisted and the live count does not change And duplicate prevention holds even under concurrent scans from multiple devices And an audit event of type “duplicate_attempt” is recorded once per attempt
Wrong Class or Invalid Token Handling
Given a token is not found on the class roster, is for a different class/time, or is malformed/expired When the token is scanned Then the feedback is red and includes a concise reason code: “Wrong class”, “Invalid token”, or “Expired” And no roster updates occur and no check-in event is created And validation latency meets the ≤300 ms SLA at p95
Edge Cases Amber State (Waitlist, Unpaid, Late)
Given a token matches a booking with a waitlist status, unpaid balance, or late-arrival rule trigger When the token is scanned Then the feedback is amber with the specific reason: “Waitlist”, “Unpaid”, or “Late arrival” And the system does not mark the attendee as checked in without an explicit staff action via manual lookup/override And an audit event with result “amber_flag” and the reason code is persisted
Feedback Display Duration, Privacy, and Accessibility
Given kiosk feedback is displayed after any scan When feedback is shown Then the banner remains visible for a configurable duration (default 2 s, range 1–5 s) And personally identifiable information (attendee name) is removed or blurred within 3 s (configurable 1–5 s) And green/red/amber visuals meet a minimum 7:1 contrast ratio and include iconography to avoid color-only signaling And sound/haptic cues follow per-device settings and are disabled in Do Not Disturb mode
Graceful Offline Confirmation with Trusted Cache
Given the kiosk is offline and holds a locally cached roster updated within the last 15 minutes When a valid token on the cached roster is scanned Then the kiosk shows green feedback with an “Offline” badge and records a queued check-in event with a client-generated idempotency key And the live count updates locally and syncs to the server within 10 s of reconnect And if a token is not in cache, the kiosk shows amber “Offline—unknown token” and no check-in is recorded
Event Persistence and Real-Time Roster Update
Given any scan attempt (success, duplicate, red, or amber) When the system processes the scan Then an audit record is persisted with timestamp (UTC ms), device ID, operator session ID, class ID, token hash, result code, and latency ms And for successful check-ins, the class roster reflects the checked-in status within 1 s on all connected kiosks and staff views And idempotency keys prevent duplicate success records across devices and sessions
Live Capacity & Roster Sync
"As a studio manager, I want live counts and automatic waitlist handling so that capacity is accurate and we avoid overbooking during busy arrivals."
Description

Maintain a real-time connection to update capacity, checked-in counts, and waitlist status across all devices. Use websockets or server-sent events for push updates and apply optimistic concurrency with conflict resolution to prevent race conditions during simultaneous scans. Auto-promote waitlisted attendees when spots free up, respecting studio rules and payment requirements. Show always-current counters and status badges on the kiosk, prevent over-capacity check-ins, and reconcile any offline events on reconnect without creating duplicates.

Acceptance Criteria
Real-time Push Updates Across Devices
Given a class session with defined capacity and multiple devices subscribed via WebSocket or SSE When attendee A is checked in on any device Then the originating device updates checked-in and remaining capacity counts within 100 ms of server acknowledgment And all other subscribed devices update checked-in count, remaining capacity, and attendee status badge within 500 ms And exactly one broadcast update is received per check-in event by each device And no device displays counters that differ from server state for more than 1 second
Optimistic Concurrency on Last Available Spot
Given remaining capacity is 1 and two kiosks attempt check-in within 100 ms of each other When both check-ins are submitted Then exactly one check-in is persisted by the server And the winning device shows green success and incremented counters within 300 ms And the losing device receives a 409 Conflict (or equivalent) within 300 ms and displays red "Capacity full" And no over-capacity is recorded on the server or any device
Duplicate Scan Deduplication for Same Attendee
Given attendee A is not checked in When attendee A's QR is scanned multiple times within 2 seconds or on multiple devices Then the first scan succeeds and marks attendee A as checked in And subsequent scans return "Already checked in" within 300 ms And the roster contains only one check-in record for attendee A
Auto-Promote Waitlist with Payment Enforcement
Given the class is full, a waitlist exists, and studio rule requires successful payment before promotion When a spot becomes available due to cancellation or check-in undo Then the next eligible waitlisted attendee is identified per studio priority rules And if a valid payment method is on file, a charge is captured successfully before promotion And if payment fails or no method exists, the attendee remains on waitlist with "Payment Required" badge and is not checked in And all devices reflect roster, counters, and badges within 500 ms of the promotion decision
Offline Scans Queue and Reconcile on Reconnect
Given the kiosk loses network connectivity while scanning When scans occur offline Then each offline scan is stored with a unique idempotency key and timestamp And upon reconnection, scans are replayed to the server in chronological order And the server deduplicates events and applies capacity rules, rejecting any that would exceed capacity And all devices reflect the reconciled counts and statuses within 2 seconds And no duplicate check-in records are created
Always-Current Counters and Status Badges
Given the kiosk is displaying checked-in count, remaining capacity, and attendee status badges When check-ins, undos, cancellations, or promotions occur Then the kiosk updates the displayed counters and badges within 500 ms of server state change And badges are mutually exclusive and correspond to server-assigned states (Checked-in, Waitlisted, Payment Required, Capacity Full) And the displayed totals match server totals at all times with zero tolerance for mismatch persisting beyond 3 seconds
Admin Roster Changes Propagate to Kiosk
Given a staff member performs a check-in, undo, or roster edit from the admin dashboard When the change is saved on the server Then the kiosk receives and applies the update within 500 ms And duplicate scans for the affected attendee are prevented with "Already checked in" or "Not eligible" as applicable And any consequent waitlist promotion or retraction is applied according to studio rules without creating duplicates
Manual Lookup & Check-In Fallback
"As a front-desk staff member, I want a quick manual lookup and check-in option so that I can resolve edge cases when someone doesn’t have their QR or the code won’t scan."
Description

Provide a fast manual search to find attendees by name, phone, or email with tolerant matching and a ten-key optimized input. Allow manual check-in with visible status (paid, membership, drop-in, waiver needed) and capture an optional note when overriding errors (e.g., comp guest). Support creating an on-the-spot attendee placeholder and linking to payment flow on a companion device if required by studio policy. Operate in degraded mode with a cached roster when offline and automatically reconcile manual actions on reconnect with a full audit trail.

Acceptance Criteria
Fuzzy Name Search Returns Accurate Matches Fast
Given the QuickScan Kiosk is in Manual Lookup mode and the class roster is available (online or cached) When staff enters at least 3 characters of an attendee's first or last name with up to one character typo or transposition Then the system returns up to 10 ranked matches within 500 ms online or 800 ms offline, ignoring case and diacritics, and each result displays name and a status badge (Paid, Membership, Drop-in, Waiver Needed) And when no results are found Then the UI suggests switching to Phone or Email search
Ten-Key Optimized Phone Search
Given the kiosk is in Manual Lookup mode When staff selects Phone search and types the last 4–10 digits using the on-screen ten-key or a hardware numeric keypad Then matching attendees are filtered in real time with formatting ignored (spaces, dashes, country codes), and results update within 300 ms of the last keypress And pressing Enter selects the top result; pressing Enter again confirms Check-In when there is a single unambiguous match; total keystrokes to check in a unique last-4 match is <= 6
Email Search With Tolerant Matching
Given the kiosk is in Manual Lookup mode When staff switches to Email search and enters >= 3 characters of the email local-part or a full email Then results are case-insensitive, ignore common separators, support contains and prefix matching, rank exact matches first, and return up to 10 results within 500 ms
Manual Check-In Updates Roster and Capacity
Given an attendee is located via manual search When staff taps Check In on the result Then the attendee is marked Checked In with timestamp and source=KioskManual, the class checked-in count increments immediately, capacity/status indicators update in real time, and a green confirmation with attendee name is shown within 300 ms And if the attendee is already checked in Then an "Already checked in" message with the prior timestamp is shown and capacity is not double-counted
Override Blocked Check-In With Optional Note
Given a check-in attempt is blocked due to unpaid balance, waiver required, or expired membership When staff selects Override Check-In Then the system allows the override and presents an optional Note field (0–140 chars) before finalizing; leaving the note blank still permits the override And the action is logged with attendee, class, timestamp, device_id, blocked_reason, overridden=true, and note_text And the roster item displays an Overridden badge on the attendee for this class
Create Placeholder Attendee and Payment Link
Given a person is not found in search When staff selects Create Placeholder and enters first name and at least one contact (phone or email) Then a placeholder attendee is created and appears on the roster within 1 second with a Placeholder badge And if studio policy requires payment-before-check-in Then a payment link and QR tied to the class and attendee is generated for a companion device, and the attendee shows Payment Required until payment confirmation is received And if studio policy allows post-payment Then staff can check in immediately and the attendee shows Pending Payment until payment is completed or canceled
Offline Manual Lookup, Queueing, and Reconciliation With Audit Trail
Given the kiosk is offline and a cached roster not older than 24 hours is available When staff performs searches, creates a placeholder, or checks in attendees Then all actions execute locally with an Offline badge visible; search latency is <= 800 ms; queued action count is displayed And when connectivity is restored Then queued actions sync within 5 seconds, are de-duplicated via a client action id, and the server audit log records action_type, attendee_id or temp_id, client_timestamp, server_timestamp, device_id, actor=Kiosk, and note_text; roster and capacity are corrected accordingly; any sync failures are surfaced with a retry option
Device Registration & Access Control
"As an admin, I want to register and lock down kiosk devices to specific classes and roles so that check-in is secure and privacy-compliant."
Description

Add secure kiosk provisioning that binds a device to a location and class context using an admin QR or short-lived code, with role-based permissions limiting accessible classes and actions. Include a PIN-protected operator session, auto-lock on inactivity, and scoped tokens that can be remotely revoked. Minimize on-screen PII exposure (name only by default, no contact details) and allow privacy-safe display settings. Log all access and configuration changes for auditability, aligning with ClassTap’s white-label branding and security model.

Acceptance Criteria
Provision Kiosk via Admin QR
Given a device is on the QuickScan Kiosk setup screen and an admin displays the provisioning QR for a specific organization, location, and class When the device scans the admin QR Then the device is bound within 5 seconds to that organization, location, and class with a scoped access token And the kiosk cannot access classes or actions outside that scope And a provisioning audit record is created with timestamp, device ID, admin ID, and outcome And the setup and success screens show tenant branding only with no platform identifiers
Provision Kiosk via Short‑Lived Code
Given an admin generates a short‑lived provisioning code scoped to a specific organization, location, and class When the code is entered on the kiosk within 5 minutes and within 5 attempts Then the device is bound to that scope with a scoped access token and the code is immediately invalidated And expired or invalid codes are rejected with a non‑revealing error and an audit log entry And unused codes expire automatically after 5 minutes
Role‑Based Access Scope Enforcement
Given a device is provisioned for Location A and the operator has a Staff role with Check‑In permission only When the operator uses the kiosk Then only classes within the provisioned scope are selectable and only Check‑In and Manual Lookup actions are available And actions such as Edit Roster, Refund, Change Capacity, and View Payments are not accessible And any attempt to access out‑of‑scope classes or actions is blocked with a non‑revealing message and recorded in audit logs
PIN‑Protected Operator Session with Auto‑Lock
Given the kiosk is provisioned and Operator Mode is enabled When an operator signs in with a 6‑digit PIN Then the session starts and the kiosk remains unlocked while active check‑ins occur And if no interaction occurs for 2 minutes, the kiosk auto‑locks to the PIN screen and hides all attendee data And closing or backgrounding the app locks the session immediately And three consecutive failed PIN entries trigger a 60‑second lockout and an audit log entry
Remote Revocation of Scoped Token
Given a kiosk is provisioned and online When an admin revokes the device’s scoped token from the dashboard Then the kiosk disables scanning and shows a Re‑authenticate view within 30 seconds And no class data is visible after revocation And if the kiosk is offline at the time of revocation, the revocation is enforced within 30 seconds of reconnect
Privacy‑Safe Display Defaults
Given the kiosk is in scanning mode with privacy defaults enabled When a valid attendee QR is scanned Then the success feedback shows only First Name and Last Initial with a green status and no contact details And failure feedback shows a red status with a generic reason and no PII And operators can select Privacy Mode levels: Name+Initial (default), Initials Only, or No Name, and changes take effect immediately And no contact details (phone, email, address) are displayed anywhere in kiosk mode
Access and Configuration Audit Logging
Given provisioning, operator sign‑in/out, privacy setting changes, and token events occur on a kiosk When an admin views the Security Audit log in the dashboard Then each event shows timestamp, device ID, actor (user or system), action type, outcome, and source IP where available And logs are immutable, filterable by organization, device, actor, and date, and retained for at least 90 days And log entries contain no sensitive PII beyond actor name and email And logs can be exported as CSV
Event Logging & Kiosk Analytics
"As a studio owner, I want visibility into check-in throughput and errors so that I can staff appropriately and reduce bottlenecks and mistakes over time."
Description

Capture structured events for scans, validations, manual overrides, errors, and throughput metrics (e.g., scans per minute, average validation latency, duplicate rate) with per-class and per-operator dimensions. Provide export to CSV and dashboard views within ClassTap to analyze peak times, staff efficiency, and error causes, enabling staffing and process improvements. Retain logs per data policy, redact sensitive fields, and ensure compliance with regional privacy regulations while supporting studio-branded reports.

Acceptance Criteria
Structured Scan and Override Event Logging
Given a successful QR validation for a class check-in When the attendee is checked in via QuickScan Then create an event with event_type="scan.validated" and fields: event_id(UUIDv4), timestamp(ISO8601 UTC), class_id, class_start_time, operator_id, kiosk_id, attendee_ref(SHA-256 salted), source="qr", result_code="ok", validation_latency_ms, client_timestamp, server_timestamp, app_version Given a rejected scan When the code is invalid or the attendee is not on the roster Then create event_type="scan.rejected" with result_code in ["not_on_roster","invalid_qr","expired","capacity_full"] Given a duplicate scan for the same class When the same attendee is scanned again Then create event_type="scan.duplicate" including original_event_id and duplicate_count>=1 Given a manual override check-in When staff checks in an attendee without a valid scan Then create event_type="override.checkin" with source="manual" and override_reason in ["name_lookup","device_failure","accessibility","other"] Given a runtime error during scanning or validation When an error occurs Then create event_type="error" with error_code, error_message(redacted), severity, and correlate error_event_id to triggering event_id if present Then 95th percentile time from server receipt to durable persistence is <=200 ms and events are queryable in analytics
Throughput and Latency Metrics Computation
Given active scanning for a class When events are ingested Then compute per-class and per-operator metrics each minute: scans_per_minute, avg_validation_latency_ms, p95_validation_latency_ms, duplicate_rate=duplicate_events/total_scans, reject_rate=rejected_events/total_scans Then metrics are available for intervals 1m, 5m, 15m, session, day, week with timezone-aware bucketing Then real-time widgets refresh within 1 s of new events for classes with <=100 scans/min peak Then historical queries over 30 days and <=100k events return in <=2 s p95
Analytics Dashboard Views and Filters
Given a user with analytics permission When the Kiosk Analytics dashboard loads Then show a "Today" summary (total_scans, duplicate_rate, reject_rate, avg_latency) and a timeline chart When filters (date range, class_id, operator_id, location, kiosk_id) are applied Then all widgets, charts, and tables reflect the filters consistently Then display a table of event samples with columns [timestamp, class, operator, event_type, result_code, attendee_ref_redacted], with pagination and sortable columns Then display peak times heatmap by hour x day and error causes breakdown by result_code Then enforce org scoping so users see only classes and operators within their organization
CSV Export with Redaction and Branding
Given a filtered analytics view When the user clicks "Export CSV" Then generate a server-side CSV using the same filters, with documented columns and a header row in UTF-8 Then exclude PII (no names, phone, email); attendee_ref is hashed; include a metadata row starting with "#" noting redaction=applied, timezone, date_range, org_id Then export timestamps in ISO8601 with the user's selected timezone offset Then for datasets <=250,000 rows complete in <=60 s via streaming; for larger datasets provide an async download link within 5 minutes and notify the user Then the file name includes studio_brand, report_name, and date_range; the download page displays studio logo and brand colors
Privacy, Redaction, and Retention Compliance
Given regional data residency settings When events are stored Then route storage to the configured region matching the studio's policy (e.g., EU, US) Then never store PII in analytics events; implement one-way salted hash for attendee_ref with rotatable salt key Then if consent_required=true and attendee consent is not recorded Then store attendee_ref=null and reason="no_consent" Then enforce per-org retention (default 90 days, max 365) via scheduled deletion with audit log entries for deletions Then process right-to-erasure requests within 7 days by removing/anonymizing matching analytics events and logging erasure_job_id Then audit all access to analytics dashboards and exports with user_id, timestamp, action, and outcome
Data Integrity and Idempotent Processing
Given at-least-once client delivery When duplicate events with the same dedup_key arrive Then persist a single logical event, increment dedup_count, and prevent double-counting in metrics Then order analytics by server_timestamp while retaining client_timestamp; handle clock skew up to ±120 s without mis-bucketing Then incorporate late-arriving events up to 15 minutes into minute buckets and recompute affected aggregates; later events roll into daily aggregates only Then reject events missing required fields with error_code="schema.invalid" (HTTP 400) and mark retryable=false Then run hourly integrity checks reconciling raw vs aggregated counts with zero tolerance; on mismatch, raise a severity=high alert
Studio-Branded Reports and Sharing
Given a studio with custom branding When viewing analytics or exporting reports Then display studio name, logo, and brand colors with contrast ratio AA or higher Then allow saving a filtered report view and generating a secure share link scoped to the org that expires in 14 days by default Then include a footer "Generated by ClassTap" with brand name and generation timestamp in exported CSV metadata and any enabled PDF exports

Family Auto-Scan

One scan checks in linked family members from a single Join Pass. Applies per-child eligibility and consent checks, flags exceptions, and lets staff approve or deny specific children in one tap. Cuts lobby chaos for caregivers and keeps rosters compliant.

Requirements

Single-Pass Family Scan (QR/NFC)
"As a front-desk staff member, I want to scan one family pass to pull up all linked children so that I can check them in quickly without juggling multiple devices or profiles."
Description

Enable check-in of all linked children via a single scannable Join Pass that supports QR code and NFC. On scan, retrieve the guardian profile, associated minors, and active class context, then pre-populate per-child eligibility states. Include fast path performance (<500 ms local decode + <1 s server round trip), camera and external scanner support on iOS/Android/web kiosk, and debounce/rate-limit to prevent duplicate scans. Enforce site and time window constraints (e.g., scans allowed X minutes before/after class). Provide fallback to manual lookup when a pass is invalid or unreadable. Support white‑label theming for branded scan screens and messages.

Acceptance Criteria
Single-Pass Scan (QR/NFC) with Performance and Context Retrieval
Given a valid Join Pass for a guardian and active classes at the site, When the pass is scanned via QR or tapped via NFC, Then the client decodes the payload locally in <= 500 ms and sends a check-in request with an idempotency key. Given network connectivity, When the server receives the request, Then it responds with guardian profile, linked minors, resolved active class context, and per-child eligibility statuses within <= 1,000 ms at p95. Given multiple simultaneously eligible classes for the guardian’s children, When the scan occurs, Then the system resolves the active class based on scanner context (pre-assigned class or staff selection within 1,000 ms) and applies it to all children. Given the pass is signed for the current site, When processed, Then the signature verification passes; otherwise the scan is rejected with a clear message. Given at least one eligible child, When results are returned, Then the UI displays a list of children with status chips and a "Check in all eligible" action and per-child toggles. Given the staff confirms, When check-in is submitted, Then rosters update atomically, occupancy counts reflect additions, and an audit log captures timestamp, device ID, staff ID, and method (QR/NFC).
Device and Scanner Support Across Platforms
Given an iOS (15+) or Android (10+) device with camera permissions granted, When staff opens the scan screen, Then continuous QR detection operates at >= 24 FPS under normal lighting and successfully reads standard-size QR codes at 10–60 cm. Given a web kiosk running Chrome/Edge (last 2 versions) with a camera, When the scan screen is opened, Then QR scanning works via WebRTC and surfaces a permission prompt with retry on denial. Given a USB/Bluetooth handheld scanner configured as HID keyboard, When a QR is scanned, Then the app processes the input line and triggers the same flow as a camera scan. Given an NFC-capable device, When a compatible NDEF pass is tapped, Then the app reads the payload and triggers the same flow as a QR scan; if NFC is unavailable, a non-blocking notice explains that QR can be used instead. Given camera/NFC permissions are denied, When staff retries, Then the app provides guided steps to enable permissions and a link to manual lookup.
Per-Child Eligibility, Consent, and Exception Handling
Given linked minors for the guardian and an active class context, When the scan is processed, Then the system auto-evaluates per-child: enrollment status, capacity/waitlist state, age/grade policy, required consents (e.g., liability, media), medical alerts, pickup restrictions, and outstanding blocks. Given evaluation results, When statuses are presented, Then each child is labeled Eligible, Needs Attention (with reason codes), or Ineligible (with reason codes), and tooltips or info panels show details. Given a child is Needs Attention, When staff taps Approve, Then the system requires an override permission and captures an override reason; When staff taps Deny, Then a denial reason is required; all actions are logged. Given mixed statuses, When staff taps "Check in all eligible", Then only Eligible children are checked in; Needs Attention/Ineligible are not, and notices are displayed.
Time Window and Location Constraint Enforcement
Given a class scheduled at a site with a configured scan window (default 15 minutes before to 15 minutes after start), When a scan occurs outside this window, Then check-in is blocked and the message states how early/late the scan is. Given multiple sites, When a pass signed for Site A is scanned at Site B, Then the scan is rejected with a "Wrong site" message referencing the correct site; staff with override permission can proceed with reason logged. Given early/late arrival overrides are allowed, When authorized staff override, Then rosters mark the check-in as Early or Late accordingly.
Duplicate Scan Debounce and Rate Limiting
Given the same Join Pass is scanned multiple times within 3 seconds on the same device, When processing, Then only the first scan triggers a server call; subsequent scans show "Already scanned" and are ignored. Given high-throughput scanning, When the device processes scans, Then the client enforces a maximum of 10 scans per second and queues additional scans with user feedback, without dropping valid scans. Given potential racing requests from multiple devices, When the server receives requests with the same idempotency key or pass token within a 10-second window, Then only one check-in transaction is committed.
Fallback to Manual Lookup for Invalid/Unreadable Pass
Given a QR cannot be decoded, NFC read fails, or the pass is expired/invalid, When the scan fails, Then the app offers manual lookup by guardian name, phone, email, or child name with masked PII until selection. Given a manual search query, When submitted, Then results return within <= 1,000 ms at p95 with clear disambiguation details, and selecting a guardian launches the same per-child evaluation flow. Given repeated scan failures, When fallback is used, Then failure events are logged with reason codes, device, and OS for analytics; user guidance to improve scan conditions is displayed.
White-Label Theming for Scan Screens and Messages
Given tenant brand settings (logo, primary/secondary colors, font), When the scan screen and check-in results render, Then components apply the theme to headers, buttons, chips, and toasts without layout breakage. Given branded copy settings, When success and error messages display, Then they interpolate the tenant brand name and tone while avoiding ClassTap branding. Given dark mode, When the theme is applied, Then contrast ratios meet WCAG AA for text and interactive elements; if a provided color fails, the system auto-adjusts to the nearest accessible variant. Given missing or slow-loading brand assets, When rendering, Then defaults are used and the screen remains interactive; theme assets load within <= 200 ms at p95 on broadband.
Per-Child Eligibility & Consent Rules Engine
"As a program coordinator, I want the system to automatically check each child’s eligibility and consents so that staff approve only compliant attendees without manual cross-checking."
Description

Evaluate configurable per-child eligibility at scan time, including enrollment or drop‑in validity, age bands, membership/pack balance, waiver and consent statuses (liability, media, medical), emergency contacts, outstanding payments, and guardian-present requirements. Rules are class- and site-scoped, versioned, and return pass/fail with reason codes and recommended actions. Integrate with existing waiver, payments, CRM, and class scheduling modules. Surface clear green/yellow/red statuses to the UI and via API for automation.

Acceptance Criteria
Per‑Child Eligibility Evaluation at Family Scan
Given a Join Pass scan for a scheduled class at site S with linked children C1..Cn and current time T within the class check-in window When the rules engine evaluates each child against the active ruleset Then for each child, it determines enrollment/drop-in validity, age band compliance, membership/pack balance sufficiency, waiver and consents statuses (liability, media, medical), emergency contact presence, outstanding payments, and guardian-present requirements And it returns for each child: decision pass|fail, statusColor green|yellow|red, reasonCodes[], and recommendedActions[] And statusColor is green if decision=pass and no warning-severity reasons; yellow if decision=pass with warning-severity reasons; red if decision=fail And evaluation completes within 500 ms for up to 6 linked children on a warm cache
Class/Site Scoped Ruleset Selection and Versioning
Given multiple published ruleset versions exist for classId X at siteId S And version Vn has an effectiveFrom timestamp <= T and is not retired at T When a scan occurs at time T Then the engine selects the highest published version whose effective window covers T, falling back to the site default if no class-specific version exists, else to a global default And the evaluation response includes rulesetId and rulesetVersion=Vn And an immutable audit record is written with rulesetId, version, classId, siteId, scanId, evaluatedAt, and a hash of the rules content
Standardized Reason Codes and Recommended Actions
Given any blocking or warning condition is detected When the engine compiles the outcome Then it aggregates all applicable reasonCodes using a documented, unique, machine-readable format (e.g., WAIVER_MISSING, AGE_OUT_OF_BAND, PACK_DEPLETED, OUTSTANDING_BALANCE, GUARDIAN_ABSENT) And for each reasonCode it includes a recommendedActions entry chosen from an enumerated set (e.g., COLLECT_WAIVER, VERIFY_AGE, SELL_DROP_IN, TAKE_PAYMENT, LOCATE_GUARDIAN, UPDATE_CONTACTS) And the reasons are ordered by severity (blocking before warning) then by code And duplicate reasons are de-duplicated And all codes and actions map to localization keys without hard-coded display strings
UI Status Colors and One‑Tap Staff Approval
Given the engine returns per-child outcomes with statusColor and reasonCodes When results are presented to staff on the check-in screen Then children with statusColor=green are auto-marked as checked-in And children with statusColor=yellow require an explicit Approve or Deny tap; on Approve they are checked-in and an override log is recorded with staffId, timestamp, childId, and reasonCodes And children with statusColor=red cannot be checked-in; the Deny action records the reasons and no attendance is created And mixed outcomes allow approving/denying children independently within the same family scan And override eligibility is controlled by rule severity: warning-level only; blocking-level rules are non-overridable
API Response Contract for Automation
Given an API client submits a scan evaluation request with joinPassId, classId, siteId, timestamp, and children[] When the engine evaluates the request Then the 200 response body includes for each child: childId, decision pass|fail, statusColor green|yellow|red, reasonCodes[], recommendedActions[], rulesetId, rulesetVersion, evaluatedAt, and correlationId And the schema is versioned (responseVersion) and documented; unknown fields are ignored by clients; breaking changes require a new version And invalid requests return 422 with machine-readable error codes; unauthorized return 401; forbidden return 403 And median response time <= 300 ms and p95 <= 800 ms for up to 6 children
Membership/Pack Balance and Drop‑In Eligibility
Given a child is not enrolled but the class allows drop-in and the site accepts packs or memberships When the engine evaluates payment eligibility Then if an active membership or pack with remaining credits covers the class and site, the child passes with statusColor=green and includes recommendedActions [RESERVE_CREDIT] And if credits are depleted but drop-in purchase is permitted, the child fails with reasonCodes [PACK_DEPLETED] and recommendedActions [SELL_DROP_IN] And if the child has outstanding payments that block attendance, the child fails with reasonCodes [OUTSTANDING_BALANCE] and recommendedActions [TAKE_PAYMENT] And if neither enrollment nor drop-in is allowed, the child fails with reasonCodes [ENROLLMENT_REQUIRED]
Guardian Presence and Emergency Contacts Rules
Given a class is configured with requiresGuardianPresent=true and emergencyContactsRequired severity configured per site When a family scan occurs Then if no authorized guardian for the child is detected within the same scan session, the child fails with reasonCodes [GUARDIAN_ABSENT] and statusColor=red And if emergency contacts are missing and the rule is blocking, the child fails with reasonCodes [EMERGENCY_CONTACTS_MISSING]; if the rule is warning, the child passes with statusColor=yellow and reasonCodes [EMERGENCY_CONTACTS_MISSING] and recommendedActions [UPDATE_CONTACTS] And when the guardian is subsequently scanned within the session window (<= 5 minutes), pending children are re-evaluated and, if all other rules pass, become statusColor=green
One-Tap Child Approval UI with Exception Flags
"As a front-desk staff member, I want a clear list of each child with one-tap approve or deny options so that I can resolve exceptions quickly and keep the line moving."
Description

Provide a touch-optimized screen listing each child with status badges, exception summaries, and one-tap Approve/Deny controls. Require selecting a reason on deny from a configurable list and support partial approvals for families with multiple children. Show capacity position, waitlist position, and inline quick actions for payment/waiver completion. Offer bulk approve when all statuses are green. Confirmation view summarizes final roster updates. Ensure accessibility (WCAG 2.1 AA), large tap targets for lobby devices, and brandable light/dark themes.

Acceptance Criteria
Lobby-Optimized Child List with Exception Badges
Given a caregiver scans a valid Join Pass for a family with 1–12 linked children When the One-Tap Child Approval screen loads Then each child renders as a distinct row/card with: full name, avatar/initials, class title/time, and a status badge (Green=Clear, Amber=Attention, Red=Blocked) plus a one-line exception summary if not Green And Approve and Deny controls are visible on each row And all primary tap targets (Approve, Deny, Quick Actions) are at least 44x44 px with 8 px spacing And initial render completes within 1500 ms on Safari iPad (recent model) over 4G with cached assets And the list vertically scrolls without layout overlap or truncation at 60 fps for up to 12 children
Approve/Deny Flow with Exception Handling and Deny Reasons
Given any child row with visible Approve and Deny controls When Approve is tapped for a child with Green status Then the child is marked Approved locally and ready for submission Given a child with blocking exceptions (e.g., Payment Due, Missing Waiver) When Approve is tapped Then the inline quick action prompt is shown to resolve the exception(s) and Approve remains pending until completion And upon successful completion of all blocking exceptions, the child status turns Green and is marked Approved Given any child row When Deny is tapped Then a modal appears requiring selection of a deny reason from the admin-configured list (at least 1 and up to 10 options), with optional free-text notes And Deny cannot be confirmed without a selected reason, with an accessible error announcement And the selected deny reason (and note if present) is persisted for audit with staff ID and timestamp
Partial Approvals Within a Family
Given a family with multiple children When staff approve some children and deny others before submitting Then the selections are stored independently per child and do not require all-or-nothing action And a running counter displays Approved X / Denied Y / Total N And navigating away and back within the same session preserves unsent selections And attempting to submit with zero selections shows an inline warning and disables submission
Real-Time Capacity and Waitlist Positions
Given a class with defined capacity and optional waitlist When the family list renders Then each child shows either: “Seat N of M remaining” if seats remain, “Full — waitlist pos #K” if waitlisted, or “Confirmed” if already enrolled And these indicators update within 2 seconds of roster changes from other devices (or within 10 seconds via polling fallback) And if a seat opens while a child is waitlisted and Approve is tapped, the system promotes the child to enrolled and reflects the new capacity state before confirmation
Bulk Approve When All Children Are Clear
Given all children in the list have Green status and no unresolved quick actions When the Bulk Approve button is displayed Then tapping Bulk Approve marks all children as Approved in a single step And Bulk Approve is hidden or disabled if any child has a non-Green status or pending exception And a pre-submit confirmation allows canceling or proceeding
Confirmation Summary and Audit Trail
Given at least one child is marked Approved or Denied When Submit is tapped Then a confirmation view summarizes per child: final outcome (Approved/Denied), deny reason(s), and resolved exceptions And upon confirming, roster updates are applied atomically: either all intended changes succeed or detailed per-child failures are presented with retry options And a success banner displays and the flow returns to the scanner-ready state And all actions write to an immutable audit log including: staff ID, device ID, timestamp, child IDs, outcomes, deny reasons, and any exception resolutions
Accessibility and Brandable Light/Dark Themes
Given the One-Tap Child Approval UI is used on shared lobby devices When tested against WCAG 2.1 AA Then all text/iconography meets contrast ratio ≥ 4.5:1, color is not the sole indicator of status, and focus order is logical And all actionable elements support keyboard/switch navigation and expose descriptive labels and states to screen readers And tap targets are ≥ 44x44 px and remain usable with system text scaling up to 200% without loss of content or functionality And brandable light/dark themes can be applied via theming tokens, preserving WCAG contrast in both modes and across status badges, buttons, and banners
Capacity & Waitlist Sync During Family Check-In
"As an instructor, I want the roster to update accurately when families are checked in so that the class stays within capacity and I can prepare materials appropriately."
Description

On approval, atomically allocate class capacity per child, move eligible children from waitlist when space is available, and enforce class caps to prevent overbooking. Handle sibling scenarios where some are approved and others remain waitlisted, with clear messaging. Update instructor rosters in real time, send webhooks for downstream systems, and ensure idempotent operations across multiple devices to avoid double-bookings. Block allocation when required payment or waiver steps are incomplete and provide retry flows.

Acceptance Criteria
Partial Sibling Approval with Per-Child Atomic Allocation
Given a class with capacity C and a family with N linked children, some eligible and some ineligible When a staff member approves a selected subset of children in one action Then for each eligible approved child exactly one seat is allocated atomically and the child is marked checked_in And for ineligible or unapproved children no capacity is consumed and their status remains unchanged And the result returns per-child outcomes in {checked_in, waitlisted, blocked} with reason codes And no child appears more than once on the roster And the total allocated seats increase by exactly the number of eligible approved children
Auto-Promote Waitlisted Siblings on Space Availability
Given a class with capacity C and a FIFO waitlist containing one or more siblings And at least one of the siblings is waitlisted When staff approves the family and seats are available Then eligible waitlisted children are promoted in FIFO order up to available seats and marked checked_in And any remaining siblings stay waitlisted with reason "class_full" And the UI confirms promotions and waitlist holds within 1 second of approval And the waitlist positions update accordingly without gaps or duplicates
Enforce Class Cap Under Concurrent Multi-Device Approvals
Given a class cap C and two or more devices attempting approvals for overlapping children within a 5-second window When approvals are processed concurrently Then the sum of enrolled + checked_in children never exceeds C And any operation that would exceed C is rejected with HTTP 409 and UI message "Class at capacity" within 1 second And no child allocation is partially applied or left in an indeterminate state And audit logs record the rejecting transaction including device_id and request_id
Idempotent Family Check-In Across Devices and Retries
Given each approval request includes an idempotency_key unique per family+class for a 15-minute window When the same Join Pass is scanned and approval submitted multiple times from one or more devices Then only the first successful request allocates seats And subsequent requests with the same idempotency_key return 200 with the original result and no additional capacity change And retried requests after 5xx errors or timeouts with the same idempotency_key do not create duplicates And idempotency records persist for at least 24 hours for reconciliation
Real-Time Instructor Roster Sync on Family Approval
Given an instructor roster view subscribed to the class session When family approval allocates seats or promotes waitlisted children Then the roster reflects per-child status changes (checked_in, waitlisted, blocked) within 2 seconds And roster counts and ordering are accurate and consistent with server state And no stale entries remain visible after 3 seconds And a visual sync indicator confirms last update time
Webhook Emission for Family Approval Events
Given webhooks are configured for class attendance events When a child's status changes due to family approval (allocated, waitlisted->enrolled, denied, blocked) Then a signed webhook is POSTed within 5 seconds containing {event_id, timestamp, class_id, child_id, family_id, previous_status, new_status, reason_code, idempotency_key} And deliveries are retried with exponential backoff for up to 24 hours on non-2xx responses And duplicate deliveries reuse the same event_id to allow downstream deduplication And webhook signature verifies against the shared secret
Payment/Waiver Gate with Guided Retry During Check-In
Given a child lacks required payment or a signed waiver for the class When a staff member attempts to approve the child Then allocation is blocked and capacity is not consumed And the UI shows reason codes {payment_required, waiver_required} with one-tap actions to resolve And after successful completion within the session, a Retry Check-In with the same idempotency_key allocates the seat if capacity remains, else returns "class_full" and offers waitlist And all transitions are logged with timestamps for audit
Offline Check-In Queue with Conflict Resolution
"As a studio owner, I want staff to keep checking in families during internet outages so that classes start on time and data syncs correctly once we’re back online."
Description

Allow family scans and per-child approvals when connectivity is degraded or offline. Validate pass signatures and cached rules within a defined freshness window, queue actions locally with timestamps and device IDs, and display clear offline indicators and limits (e.g., max queue size or duration). On reconnection, synchronize in order with conflict detection (e.g., seat already taken) and provide guided resolution flows for staff. Use deterministic client operation IDs to prevent duplicate check-ins and ensure safe retries.

Acceptance Criteria
Offline Family Scan with Local Signature Validation
Given device connectivity is offline or degraded (no successful ping for >= 5s) When a Join Pass QR is scanned Then the pass signature is verified against a cached public key last refreshed within 24 hours; if cache age > 24 hours, check-in is blocked and a "Stale security data" error is shown And family and linked children data are loaded from local cache; if absent, display "Pass data unavailable offline" and do not enqueue any check-ins And an offline status banner is shown and one pending queue entry is prepared per child subject to per-child approval
Per-Child Eligibility and Consent in Offline Mode
Given a family scan occurs offline with rules and roster data cached When the system evaluates each linked child for the selected class occurrence Then exceptions (e.g., missing consent, age restriction, payment required) are flagged per child from cached data And staff can approve or deny each child individually; decisions are captured locally with staffId and timestamp And children marked Denied are not enqueued; Approved children create queued check-in operations including any exception codes If rules cache age > 24 hours, all children default to "Requires manual approval" and require explicit staff approval to enqueue
Queue Limits and Offline Indicators
Given the device loses connectivity Then an offline banner with last successful sync timestamp and current queue count appears within 1 second And the app enforces a maximum offline queue size of 200 operations and a maximum offline duration of 48 hours since last sync When 80% of either limit is reached, a warning is displayed; at 100%, new scans are blocked with a clear message and no new items are enqueued And the queue persists across app restarts and device lock/unlock
Deterministic Operation IDs and Safe Retries
Given a child is approved for check-in while offline Then the client generates a deterministic opId derived from (eventId, passId, childId, occurrenceStart, staffId) so that repeating the same action produces the same opId When the same child is re-scanned/approved before sync, the existing queued item is reused and a duplicate is not added Upon reconnection, if the server indicates the opId already exists or is already applied, the client marks the item as synced without creating a duplicate roster entry If transmission fails mid-sync, the client retries with the same opId until acknowledged or canceled by the user
Reconnection Sync with Ordered Apply
Given the device reconnects with N queued operations When synchronization starts Then operations are sent oldest-first by createdAt with a stable secondary sort by opId; the order is preserved across retries and partial failures And a progress indicator displays X/N synced and pauses/resumes automatically on connectivity changes If a batch partially succeeds, the client resumes from the next unsynced item without reordering or duplicating
Conflict Detection and Guided Resolution
Given a queued check-in encounters a server-side conflict (SeatTaken, AlreadyCheckedIn, EventClosed, RuleChanged) Then a resolution dialog lists affected children and specific conflict reasons And only policy-allowed actions for the user’s role are offered: Move to Waitlist (if enabled), Deny Check-in, or Override (if permitted) When staff selects an option, a corresponding resolution operation referencing the conflict is enqueued and synced And the decision, staffId, and timestamp are recorded; unresolved conflicts block dependent operations for the same event until resolved
Audit Trail with Timestamps and Device IDs
Given any offline-queued operation Then the record contains: opId, deviceId, staffId, createdAt (device time), receivedAt (server, after sync), eventId, passId, childId, action, offline=true, ruleVersion, cacheAge, approvalDecision, and exceptionCodes And upon successful sync the server stores client createdAt and deviceId alongside server receivedAt; admins can export these fields And the offline queue is encrypted at rest and survives app restarts; uninstalling the app clears the queue only after explicit user confirmation
Immutable Audit Log for Compliance
"As an administrator, I want a detailed, immutable record of each family check-in so that we can demonstrate compliance and investigate incidents when needed."
Description

Capture a tamper-evident audit trail for each scan and child decision, including staff identity, device, location, timestamps, pass ID hash, rules evaluated and outcomes, approvals/denials with reason codes, overrides, and resulting roster changes. Store write-once events with tenant-configurable retention. Provide admin search, filtering, and export (CSV/JSON) while minimizing PII exposure via token hashing and masked identifiers. Ensure time sync and signed event digests for forensic integrity.

Acceptance Criteria
Write-Once Tamper-Evident Event Logging
Given an existing audit event has been written When any actor attempts to update or delete the event via API endpoints (PUT/PATCH/DELETE) or admin UI actions Then the request is rejected with HTTP 405/403 and the original event remains unchanged And a separate audit event is appended capturing the attempted mutation (actor, method, targetId, timestamp) And direct data storage for the audit collection enforces append-only (WORM) semantics validated by storage capabilities And validating the event sequence shows no changes to prior digests since initial write
Per-Scan and Per-Child Event Cardinality and Field Coverage
Given a staff member completes a Family Auto-Scan for a Join Pass linked to N children and records per-child decisions (approve/deny) including any overrides with reason codes When the scan finishes Then exactly N+1 audit events are appended: 1 parent scan event and N child decision events And each event contains: staff identity, device identifier, location or LocationUnavailable with reason, ISO 8601 UTC timestamps, passIdHash, childTokenHash for child events, the set of rules evaluated with outcomes, decision (approve/deny), reasonCode when deny or override, override flag, and resulting roster change And the stored values match the transaction inputs and the final roster state reflects the recorded decisions
Admin Audit Search and Filter (Masked PII, Authorized Access)
Given a user with the Audit.Read admin permission When they search audit events using filters for date range, staff alias/ID, device ID, passIdHash, childTokenHash, decision outcome, reasonCode, override flag, and location-present flag Then only events matching the filters are returned and results can be sorted by timestamp ascending/descending And no plaintext PII is displayed or returned: no full names, emails, or phone numbers; person/device identifiers are masked or one-way hashed And a user without Audit.Read receives HTTP 403 and no audit data is returned
Audit Export to CSV and JSON
Given an admin has applied filters to audit events When they request an export as CSV and as JSON Then the export contains all fields required by compliance (staff identity alias, device identifier, location/flag, ISO 8601 UTC timestamps, passIdHash, childTokenHash, rules and outcomes, decision, reasonCode, override flag, roster change, event digests) And PII minimization is preserved: only hashed tokens and masked identifiers are included; no plaintext names/emails/phone numbers And the CSV includes a header row with stable column names and the JSON matches a documented schema And for up to 50,000 events the export completes within 60 seconds and the record count in the file(s) equals the number of matched results
Tenant Retention Configuration and Automated Purge
Given a tenant sets audit retention to R days (e.g., 365) When the scheduled retention job executes Then all audit events older than R days are purged and a purge summary event is appended (tenant, cutoff, counts, timestamp) And attempts to retrieve a purged event return HTTP 404 And digest verification for remaining events passes after purge And when the tenant lowers retention to a smaller value, the next job purges accordingly; when the tenant increases retention, previously purged data is not restored
Clock Sync and Timestamp Integrity
Given the scanning device may have clock skew relative to server time When audit events are written during Family Auto-Scan Then event timestamps are recorded in ISO 8601 UTC and, when server time is available, drift from server is ≤ 1 second And if server/NTP time is unavailable, events are marked timeSyncStatus = Unverified and use device time; once time sync resumes, subsequent events are marked Verified And within a single scan, the parent scan event timestamp is ≤ each child decision event timestamp
Cryptographic Digest Chain Verification
Given each audit event is appended with a content digest and included in a signed sequence chain segment When an auditor calls the verification endpoint for a range of events Then the system returns Verified = true with proof metadata for untampered ranges And if any stored event payload is altered in testing, verification returns Verified = false and identifies the first failing link in the chain
Secure Join Pass Tokens & PII Minimization
"As a privacy-conscious caregiver, I want our family pass to be secure and show minimal personal information so that our data is protected during check-in."
Description

Issue short-lived, signed Join Pass tokens (e.g., JWT/CBOR) encoding family ID and allowed scopes, with rotation and revocation controls. Support offline-verifiable signatures (EdDSA) and optional dynamic QR with rolling nonce to deter screenshots and replays. Enforce device rate limiting and replay protection via nonce caches. Limit on-screen data to the minimum necessary (e.g., first name, last initial, optional photo), honor tenant data residency, and align with COPPA/GDPR requirements.

Acceptance Criteria
Issue Short‑Lived Signed Join Pass Tokens
Given a caregiver requests a Join Pass for an active family When the token is issued Then it is signed using Ed25519 (alg=EdDSA) and encoded as JWT or COSE/CBOR And it includes claims: fid, scp, jti, iat, exp, kid And exp is no more than 10 minutes after iat And the payload contains no PII beyond fid and scp And the token can be revoked by jti prior to exp and such revocations take effect within 60 seconds for online verifiers And at least two signing keys can be active during rotation and kid selects the correct key
Offline Signature Verification on Scanner Devices
Given a scanner device has cached public keys within the last 24 hours When it scans a Join Pass while offline Then signature verification succeeds using the key referenced by kid if the token is valid And if the kid is unknown or the signature is invalid, check‑in is denied with reason shown: Invalid signature or Unknown key And verification completes in under 150 ms on a mid‑tier device And when connectivity returns, the device refreshes keys before accepting tokens signed with unknown kids
Dynamic QR With Rolling Nonce
Given a tenant has enabled dynamic QR When the Join Pass is displayed Then the QR code refreshes every 15 seconds with a new cryptographically random nonce (>=96 bits) bound to jti And the scanner rejects any QR whose embedded nonce age exceeds 45 seconds And a static screenshot older than 45 seconds is rejected as Expired code And the UI visibly indicates the countdown to the next refresh
Nonce Replay Protection and Token Reuse Prevention
Given a nonce associated with a valid token has been accepted When the same nonce or nonce‑jti pair is presented again within a 10‑minute window Then the system denies the attempt with HTTP 409 Replay detected and no roster changes occur And the event is logged with fid, jti, deviceId, timestamp only And the nonce cache expires entries after 10 minutes
Per‑Device Rate Limiting for Scan Attempts
Given a scanner device identified by deviceId When it submits more than 30 validation attempts within 60 seconds Then subsequent attempts within that window are rejected with HTTP 429 Rate limited and a retry‑after of 15 seconds And limits are enforced per deviceId and per IP to deter abuse And normal multi‑child approvals from a single scan are not rate limited
On‑Screen PII Minimization and Consent‑Based Photo Display
Given a successful scan When child details are shown to staff Then only first name and last initial are displayed for each child And photo is displayed only if guardian photo consent is true for that child and the tenant setting Show photos is enabled And DOB, full last name, contact details, and addresses are never displayed And eligibility/consent statuses are shown as icons or labels without revealing sensitive attributes
Tenant Data Residency and GDPR/COPPA Alignment
Given a tenant region of US, EU, or CA is configured When tokens are issued, validated, logged, and revoked Then all processing and storage occur in the tenant’s region and cross‑region transfer is blocked And token claims contain no child PII beyond fid and scp; UI, not the token, governs display of first name/last initial And photo display for children under 13 requires recorded verifiable parental consent; otherwise photos are suppressed and an exception is flagged And audit logs exclude child PII and record lawful basis and consent reference IDs

Offline Sync Queue

Check-in works even when Wi‑Fi drops. Scans are timestamped and queued locally, then auto-synced on reconnection with conflict resolution to prevent duplicates. Keeps doors moving without risking double-bookings or lost attendance records.

Requirements

Offline Scan Capture & Local Queue
"As a front-desk operator, I want check-ins to be captured and queued when the network drops so that I can keep the line moving without losing attendance data."
Description

Enable the check-in flow to operate without connectivity by capturing scans and check-in actions locally and queuing them for later sync. Each queued item must include a deterministic operation ID, device ID, operator ID, class/session ID, member identifier, action type, and precise timestamps, and must persist across app restarts and low-power states. The queue must be encrypted at rest, support at least 10,000 events, enforce FIFO per user/class, and provide safeguards against data loss (atomic writes, crash recovery). Performance targets: acknowledge a scan within 100 ms and sustain 2+ scans/second. Integrates with the existing QR/barcode scanner and tap-to-check-in UI without altering the current online flow.

Acceptance Criteria
Instant Offline Scan Acknowledgment and Throughput
Given the device has no internet connectivity and the operator is on the check-in screen When a valid QR/barcode is scanned or the tap-to-check-in action is pressed Then the app displays a success confirmation (haptic/visual) within 100 ms and enqueues the event locally And the enqueue operation does not block the scanner for the next scan And the system sustains at least 2 scans per second for 120 seconds with zero dropped events And acknowledgment latency p95 is ≤ 100 ms across 200 consecutive offline scans on a mid-tier device
Durable and Encrypted Local Queue Across Restarts and Low-Power States with Crash Recovery
Given N offline events have been enqueued When the app is force-closed, the OS kills the app, or the device reboots/enters low-power and resumes Then upon relaunch the queue still contains exactly N events in the same order with no data loss And no duplicate events are present after restart Given a simulated power loss/crash occurs during an event write When the app restarts Then the queue remains readable and consistent, with the last record either fully written or not present (no partial/corrupted records) And the storage for the queue is encrypted at rest using platform keystore-managed keys And reading the on-disk queue while the app is not running yields no plaintext PII; queued payloads are not readable without decryption
Queue Capacity of 10,000 Events with Backpressure
Given the queue is empty and the device is offline When 10,000 valid check-in events are recorded Then all 10,000 events are persisted and retrievable from the queue API with zero data loss And acknowledgment latency remains ≤ 100 ms and throughput ≥ 2 scans/second at a queue depth of 10,000 And the app remains responsive with no crashes or ANRs at this queue size And additional events beyond 10,000 continue to enqueue without error until device storage limits are reached (no hard-coded cap below 10,000)
Complete Event Metadata and Deterministic Operation ID
Given an offline check-in is captured Then the queued item includes: operationId, deviceId, operatorId, classSessionId, memberIdentifier, actionType, eventTimestampUtc (ISO 8601, millisecond precision), enqueueTimestampMonotonicNs And deviceId is stable across app restarts on the same device and differs across devices And operatorId equals the currently authenticated staff account at capture time And actionType reflects the user action (e.g., check_in, undo_check_in) And eventTimestampUtc reflects the moment of capture in UTC with ms precision Given two captures with identical tuple {deviceId, operatorId, classSessionId, memberIdentifier, actionType, eventTimestampUtc} When operationId is generated Then the operationId values are identical (deterministic) Given any differing tuple values Then the operationId differs And operationId uniqueness holds across 100k generated IDs (no collisions detected)
FIFO Ordering Per User/Class
Given three offline actions A, B, C for the same memberIdentifier and classSessionId are enqueued in that order When the device reconnects and sync occurs Then the server receives and applies A, then B, then C (FIFO) for that member/class Given interleaved events for different members/classes are enqueued When syncing Then the relative order per memberIdentifier+classSessionId pair is preserved even if global ordering interleaves
Auto-Sync on Reconnection with Deduplication and Conflict Resolution
Given the device has M queued offline events When network connectivity is restored Then auto-sync starts within 5 seconds without user action And events are sent with operationId such that server-side processing is idempotent (no duplicate attendance records are created) Given a member was already checked in online for the class during the offline period When the corresponding offline event is synced Then no duplicate check-in is created and the event is marked as resolved with status Already Checked-In And transient failures (e.g., 5xx/timeouts) are retried with exponential backoff without blocking other events And non-retriable validation errors are marked Failed with an error code and are not retried And after successful sync, the local queue is pruned and the class roster reflects the correct final state
Seamless Integration with Existing Online Check-In Flow
Given connectivity is available When a check-in is performed Then the existing online flow is used with no additional steps and no regression greater than 5% in p95 end-to-end latency versus current production baseline And the same QR/barcode scanner component and tap-to-check-in UI are used (no visual or interaction changes) Given connectivity drops while on the check-in screen When the next scan/tap occurs Then the system transparently switches to offline capture and continues without user intervention And when connectivity returns, the system resumes online flow and triggers auto-sync without altering the operator workflow
Automatic Background Sync & Retry
"As an instructor managing the door, I want queued check-ins to auto-sync when the internet returns so that I don’t have to manually manage uploads or risk missing records."
Description

Automatically detect connectivity restoration and begin background synchronization of queued events in batches with exponential backoff, jitter, and retry on transient failures. Preserve event order per user/class, support partial successes, and resume from the last confirmed offset. Sync must be idempotent using operation IDs, avoid blocking the UI, respect battery/data saver modes, and expose hooks for telemetry. Target: sync 1,000 events in under 60 seconds on a typical 4G/Wi‑Fi connection. Integrates with existing API endpoints or introduces a dedicated idempotent /checkins/sync endpoint.

Acceptance Criteria
Connectivity Restoration Triggers Background Sync Without UI Block
- Given the device has queued check-in events and is offline, When network connectivity changes to online (Wi‑Fi or cellular) and OS battery/data saver policies allow background data, Then background sync starts within 3 seconds, runs on a background task, does not block scanning or navigation, and emits a sync_started telemetry event with endpoint and planned batch size. - Given Android/iOS Data Saver is ON and the app is not whitelisted and only cellular is available, When connectivity returns, Then background sync does not start, a paused_by_data_saver state is emitted, and sync resumes within 3 seconds of Wi‑Fi availability or explicit user “Sync now”. - Given Battery Saver is ON and battery <20% on cellular, When syncing, Then sync throttles to low-power mode: concurrency = 1, batch size ≤ 50 events, and no noticeable UI degradation is observed per UI metrics.
Batched Sync With Exponential Backoff And Jitter
- Given a batch submission returns a transient error (HTTP 429/502/503/504 or network timeout), When retrying, Then exponential backoff is applied starting at 1s, doubling each attempt, capped at 32s, with ±20% jitter, and a maximum of 6 attempts per batch. - Given a non-transient error (HTTP 400/401/403/404/422 excluding idempotent duplicate semantics), When processing outcomes, Then the batch is not retried automatically and is marked permanent_failure with error code captured in telemetry. - Given ≥3 consecutive transient failures for the same batch, When preparing the next attempt, Then the batch size is reduced by half (min 25) and this adaptation is logged via telemetry.
Per-User/Class Event Ordering Preservation
- Given multiple queued events for the same userID and classID with timestamps t1 < t2 < t3, When syncing, Then events are sent and applied in order t1, t2, t3; the server never observes t2 before t1 for that stream. - Given events for different (userID,classID) pairs, When syncing, Then interleaving across different pairs is allowed but order within each pair is strictly preserved. - Given acknowledgements arrive out of order, When updating offsets, Then the per-stream offset advances only after all prior events are confirmed, preventing gaps.
Partial Success And Offset Resume
- Given a batch of 100 events where 80 succeed and 20 fail transiently, When processing results, Then the 80 are marked confirmed and removed from the queue, the 20 remain queued in original order, and the next batch begins from the first unconfirmed offset. - Given the app restarts during an in-flight sync, When it relaunches with connectivity available, Then sync resumes from the last confirmed offset without resending confirmed events, verified by matching client queue counts and server received counts. - Given the server returns per-item statuses, When handling outcomes, Then item-level success/duplicate/permanent_failure are honored and no event is dropped or double-created during resume.
Idempotent Operation IDs And Endpoint Negotiation
- Given each event includes a stable operationId (UUID) persisted with the event, When retries cause duplicate submissions, Then the server creates at most one record and subsequent attempts return 200/208/409-idempotent; the client treats these as confirmed and does not requeue. - Given the server advertises a /checkins/sync capability (via OPTIONS or version flag), When syncing, Then the client uses the dedicated idempotent endpoint; otherwise it falls back to existing endpoints while preserving idempotency via operationId. - Given mixed outcomes including 409 duplicate for some items, When updating the queue, Then those items are marked confirmed and excluded from further retries.
UI Responsiveness During Background Sync
- Given background sync is active, When an instructor performs 50 QR scans in 60 seconds, Then average scan-to-acknowledgement UI response remains <150 ms and frame drops do not exceed +1% over baseline on target devices. - Given navigation across Check-in and Class Roster screens during active sync, When interacting, Then no input is blocked >200 ms by sync tasks and scrolling performance remains ≥55 FPS on 60 Hz devices. - Given OS foreground priority rules, When contention occurs, Then sync reduces concurrency to maintain UI responsiveness within the above thresholds.
Throughput Target And Telemetry Hooks
- Given 1,000 queued events and typical network conditions (Wi‑Fi ≥30/5 Mbps down/up or 4G ≥10/3 Mbps, RTT ≤80 ms), When connectivity is restored, Then 1,000 events are fully confirmed within 60 seconds at the P95 across 3 consecutive runs on reference devices. - Given batches are processed, When emitting telemetry, Then per-batch metrics include: start/end timestamps, attempt count, endpoint used, request/response sizes, item counts (success/transient_failure/permanent_failure/duplicate), and latency histograms; errors include HTTP codes and exception types. - Given telemetry is enabled, When a sync session completes, Then a sync_completed event is emitted with total items, overall duration, success rate, and max backoff applied; when disabled, no telemetry leaves the device.
Conflict Resolution & De-duplication
"As a studio owner, I want duplicates and over-capacity check-ins to be automatically resolved according to policy so that we avoid double-bookings and inconsistent records."
Description

Prevent double-bookings and duplicate attendance by enforcing server-side idempotency and deterministic conflict resolution. On sync, detect duplicate operations (same operation ID) and semantic duplicates (same member + class within defined window) and collapse them. If capacity limits are reached or waitlist rules apply, accept the earliest valid event by timestamp and policy, and set subsequent events to rejected/waitlisted with reasons. Provide clear resolution codes to the client for UI feedback and auditability.

Acceptance Criteria
Idempotent Check-in by Operation ID
Given a class session with available capacity and an offline device queues a check-in with operationId=abc123 for member M and class C And the same operationId=abc123 is later submitted again (from the same or a different client) When the server processes the sync batch Then the first occurrence is persisted as a single attendance record with finalState=accepted And subsequent occurrences are not persisted and return resolutionCode=DUPLICATE_OP_ID with winnerOperationId=abc123 And the response for duplicates includes finalAttendanceId of the winning record and does not change occupancy And repeating the same batch yields identical per-item outcomes (idempotent)
Semantic Duplicate within Dedupe Window
Given dedupeWindowMinutes=120 is configured And two or more check-ins with distinct operationIds exist for the same member M and class C with effectiveTimestamp values within the dedupe window When the server processes the sync Then the event with the earliest effectiveTimestamp is accepted (finalState=accepted) And all later events are collapsed with finalState=rejected and resolutionCode=SEMANTIC_DUPLICATE referencing the accepted winner And exactly one attendance record exists for member M in class C after sync
Capacity Full with Waitlist Enabled
Given class C has capacity=2 and waitlistEnabled=true And three check-ins for distinct members M1, M2, M3 are queued with effectiveTimestamp T1<T2<T3 When the server resolves conflicts and applies policy Then the earliest two valid events are accepted and occupancy becomes 2 And the third event is set to finalState=waitlisted with resolutionCode=WAITLISTED and reason=CAPACITY_FULL And the response includes waitlistPosition for the waitlisted item And reprocessing the same inputs yields the same winners and positions
Capacity Full with No Waitlist
Given class C has capacity=1 and waitlistEnabled=false And two check-ins for distinct members exist with effectiveTimestamp T1<T2 When the server processes the sync Then the earliest event is accepted (finalState=accepted) and occupancy becomes 1 And the later event is finalState=rejected with resolutionCode=REJECTED_CAPACITY and reason=CAPACITY_FULL And no attendance record is created for the rejected event
Deterministic Tie-Breaking for Identical Timestamps
Given multiple check-ins for the same class have identical effectiveTimestamp values When the server determines acceptance order Then the server orders by serverReceivedAt ascending And when serverReceivedAt is also equal, orders by operationId ascending (lexicographic) And the selected winners/losers are identical across any replay of the same inputs
Resolution Codes and Response Contract
Given a sync batch is processed When the server returns per-item results Then each item includes: operationId, finalState in [accepted, waitlisted, rejected], resolutionCode in [ACCEPTED, WAITLISTED, REJECTED_CAPACITY, DUPLICATE_OP_ID, SEMANTIC_DUPLICATE], finalAttendanceId (nullable for rejected), winnerOperationId (when applicable), reason (string), effectiveTimestamp, serverReceivedAt And the HTTP status is 200 for a processed batch regardless of per-item outcomes And the client can render UI feedback without additional server calls using only these fields
Clock Skew and Timestamp Normalization
Given maxClockSkewSeconds=120 And a client submits a check-in with clientEventAt such that |clientEventAt - serverReceivedAt| > maxClockSkewSeconds When computing ordering for conflict resolution Then the server sets effectiveTimestamp = clamp(clientEventAt, serverReceivedAt - maxClockSkewSeconds, serverReceivedAt + maxClockSkewSeconds) And the normalized effectiveTimestamp is returned in the per-item response And ordering and outcomes remain deterministic across replays
Offline Roster & Entitlement Cache
"As a check-in attendant, I want roster and pass information available offline so that I can validate entries and handle edge cases even without Wi‑Fi."
Description

Maintain a secure, time-bounded local cache of today’s classes, rosters, memberships/passes, and eligibility rules required to validate check-ins offline. Include minimal fields needed for validation and messaging (e.g., remaining credits, pass expiry). Define TTLs and invalidation rules, and support a ‘tentative check-in’ mode when data is insufficient, flagging entries for review at sync time. Ensure cache encryption, size limits, and background prefetch when the app detects reliable connectivity before sessions begin.

Acceptance Criteria
Local Cache Composition and Minimal Fields
Given the device has authenticated and the user has access to specific locations/classes When the app builds or refreshes the offline cache Then the cache contains only today’s classes for the user’s locations and time zone And each roster entry includes only: attendeeId, bookingId, classId, classStartTime, bookingStatus (booked/cancelled/waitlisted), remainingCredits, passExpiry, entitlementType (membership/pass/drop-in), eligibilityFlags (e.g., requiresWaiver, firstClassOnly), and waitlistPosition if applicable And each class entry includes only: classId, startTime, endTime, locationId, instructorId, capacity, checkInWindowStart/End, and cancellationStatus And eligibility rules include: allowedEntitlementTypes, creditCostPerCheckIn, blackoutRules, overlapRules, and gracePeriod minutes And no payment instruments, full addresses, or unnecessary PII (email, full phone) are cached And the cache records a sourceVersion/hash to support change detection
TTL and Invalidation Rules
Given the device is online and a prefetch or sync occurs When TTLs are evaluated Then TTL for classes/rosters is 15 minutes, entitlements 5 minutes, and eligibility rules 60 minutes, with absolute expiry at local end-of-day And any item past TTL while online is refreshed in the background before use And any item past TTL while offline is marked stale and may only be used to allow tentative check-ins And receiving a newer sourceVersion/hash or server change event invalidates the affected cached items immediately And crossing a day boundary purges yesterday’s cache entries on next app launch or background task And a manual pull-to-refresh invalidates and rebuilds all cached items for today
Tentative Check‑in When Data Is Insufficient
Given the device is offline or the required entitlement data is stale/missing When a user attempts to check in and eligibility cannot be conclusively determined from cache Then the app creates a tentative check-in with a unique offlineId, deviceTimestamp, reasonCode='insufficient_data', and local booking reference if available And the UI labels the entry as Tentative and requires no more than one tap to proceed And tentative check-ins reserve a local slot for the attendee in that class to prevent duplicates on the device but do not decrement remainingCredits persistently until sync resolution And upon reconnection, the system resolves tentative entries by re-evaluating rules server-side and marks each as Confirmed or Rejected with an audit note And staff are notified in-app of any rejections with the reason (e.g., expired pass, no credits)
Cache Encryption and Secure Access
Given the app stores offline cache on the device When data at rest is written Then the cache is encrypted with AES-256-GCM using a key protected by the OS secure keystore/Keychain and tied to a device lock And cached data is excluded from system backups And accessing cached data requires an authenticated app session and a device screen lock is enforced; otherwise offline mode is disabled And on app logout or 24 hours after the last scheduled session end (whichever comes first), the cache is purged And encryption keys are rotated on re-login and previous cache material is unrecoverable
Cache Size Limit and Eviction Policy
Given the app may operate across multiple locations/classes When the offline cache exceeds its configured size limit of 50 MB Then the app evicts least-recently-used items not related to sessions starting within the next 12 hours And it never evicts the currently in-progress session’s roster and rules And it logs an internal metric when eviction occurs and exposes current cache size in diagnostics And if the cache cannot be reduced below the limit, the app blocks additional prefetch for distant sessions and surfaces a non-blocking warning to staff
Background Prefetch Before Sessions
Given the app is in foreground or allowed background execution and reliable connectivity is detected between 30–120 minutes before the first session When prefetch triggers Then the app downloads and validates today’s classes, rosters, entitlements, and eligibility rules for the next 12 hours of sessions And reliable connectivity is defined as Wi‑Fi connected or cellular with API RTT ≤ 200 ms and < 2% packet loss over three probes And failed prefetch attempts retry with exponential backoff up to 3 times and surface a subtle status indicator to staff And on success, the cache is warmed and marked fresh with timestamps per data group
Offline Eligibility Validation and Double‑Booking Prevention
Given the device is offline and the cache is fresh for the class and attendee When an attendee is scanned or selected for check-in Then the app validates locally that the pass/membership is not expired, has sufficient credits per creditCostPerCheckIn, and falls within the class’s check-in window and blackout/overlap rules And on success, a confirmed offline check-in is recorded with an idempotency key (offlineId) and remainingCredits are decremented locally in a transactional manner And the app blocks duplicate check-ins for the same attendee in the same class and for overlapping classes within the configured window And all offline check-ins are queued with deviceTimestamp and classId for later sync
Clock Drift Correction & Reliable Timestamps
"As an operations lead, I want accurate and consistent timestamps across devices so that attendance order and capacity rules are applied fairly and reliably."
Description

Mitigate device clock drift by maintaining a rolling server time offset and using monotonic clocks for local event ordering. Store both local and server-adjusted timestamps on events, and reconcile final authoritative times during sync to ensure consistent ordering across devices. Handle timezone changes and daylight savings safely, and fall back gracefully when offset is unknown by tagging events as ‘time-uncertain’ for conservative conflict policies.

Acceptance Criteria
Rolling server time offset maintained during intermittent connectivity
Given the device has connectivity to the ClassTap time service When the client requests the server time offset Then it samples the server time at least 7 times within 3 seconds, filters out samples with RTT > 200 ms, computes the median offset, and stores offset value with a retrievedAt timestamp And the offset status is marked Fresh for 10 minutes and becomes Stale after 10 minutes without refresh And if connectivity drops mid-refresh, the last Fresh offset remains in use without change And an offset refresh is scheduled every 5 minutes while online and is skipped if a Fresh refresh completed within the last 2 minutes
Offline check-ins ordered using monotonic clock despite wall-clock changes
Given the device is offline and a user performs multiple check-ins When each check-in is captured Then the app assigns a strictly increasing monotonic sequence number that never repeats within the session And the recorded local wall-clock timestamps may move forward or backward without changing the relative order defined by the monotonic sequence And if the device wall clock is manually adjusted by ±12 hours or a DST change occurs, the monotonic order of new events remains correct and strictly increasing
Events store dual timestamps and metadata for reconciliation
Given a check-in event is captured When the event is written to the offline queue Then it stores localTimestamp (ISO 8601 with milliseconds), tzOffsetMinutes, monotonicSeq, and timeUncertain flag And if a Fresh server offset exists, it also stores serverAdjustedTimestamp = localTimestamp + offset; otherwise serverAdjustedTimestamp is null and timeUncertain = true And all fields persist across app restarts and survive device reboots
Sync assigns authoritative times and resolves cross-device ordering deterministically
Given the device reconnects and sync begins When queued events are uploaded Then the server assigns authoritativeTimestamp using the best-known offset at event capture time or server receipt time if offset is unknown And if two events target the same class and attendee within a 2-minute window, conflicts are resolved deterministically by authoritativeTimestamp, then by monotonicSeq, then by eventId lex order And the outcome is idempotent: re-running the same sync yields the same final ordering and no duplicate attendance records And the client replaces local serverAdjustedTimestamp with authoritativeTimestamp and marks timeUncertain = false for reconciled events
Safe handling of timezone and DST transitions during sessions
Given a device crosses a timezone boundary or a DST transition occurs during an active check-in session When further check-ins are captured Then localTimestamp is stored with the current tzOffsetMinutes at capture time without retroactively altering prior events And authoritativeTimestamp remains strictly increasing for events captured in chronological order by monotonicSeq And no event displays a negative duration or appears before a prior event in the UI timelines for the same session
Unknown or stale offset triggers time-uncertain tagging and conservative conflict policy
Given no offset has been obtained or the last offset is Stale When a check-in is captured Then the event is tagged timeUncertain = true and serverAdjustedTimestamp remains null And the system prevents overbooking by treating time-uncertain events as lower precedence in conflicts until authoritativeTimestamp is assigned And upon sync, time-uncertain events are reconciled and their final status matches deterministic conflict rules without creating duplicates
Operator Feedback & Manual Controls
"As a front-desk operator, I want clear status indicators and simple controls so that I can trust the system and quickly intervene when something goes wrong."
Description

Provide clear in-app indicators for connectivity status, queue depth, last sync time, and error states, with unobtrusive toasts confirming offline captures. Include controls to trigger manual sync, pause syncing, and view or retry failed items. Offer an undo option for the last action within a short window and surface conflict outcomes after sync. Design for kiosk mode with large tap targets and accessibility compliance, ensuring no extra steps are required during high-traffic check-in.

Acceptance Criteria
Status Indicators During High-Traffic Offline Check-In
Given kiosk mode is enabled and the device transitions between Online, Offline, or Reconnecting, When the network state changes, Then the connectivity indicator updates within 2 seconds with distinct icon/color and an accessible label announcing the state. Given a successful sync previously occurred at time T0, When the app is offline, Then Last sync displays T0 in relative time and remains unchanged until the next successful sync, which updates the value within 1 second of completion. Given an attendee is scanned while offline, When the check-in is recorded locally, Then the queue depth counter increments within 500 ms and decrements within 1 second after the item successfully syncs. Given one or more sync failures exist, When the status area renders, Then an error badge displays with the failure count and a View failed action; And when no failures exist, Then no error badge is shown.
Unobtrusive Offline Capture Toasts
Given the device is offline and a scan is successful, When the check-in is captured locally, Then a toast appears within 300 ms stating Saved offline with attendee identifier and timestamp, and it auto-dismisses within 2.5 seconds. Given repeated scans occur within short intervals, When multiple toasts would appear, Then no more than 2 toasts are stacked; additional events collapse into a single counter (e.g., +3 more) updated in real time. Given a screen reader is active, When a toast appears, Then the message is announced via an aria-live polite region without stealing focus from the scan input. Given a toast is visible, When a new scan occurs, Then the toast does not block scanning or require extra taps to proceed.
Manual Sync and Pause Controls
Given connectivity is available and queued items exist, When the operator taps Sync now, Then syncing starts immediately, a progress indicator appears, and scanning remains available throughout the operation. Given Pause syncing is enabled, When connectivity is available, Then auto-sync does not run; And tapping Sync now performs a one-time manual sync without changing the paused state. Given Pause syncing is disabled and connectivity is available, When at least one item is queued, Then auto-sync begins within 2 seconds. Given no connectivity is available, When the operator taps Sync now, Then a non-blocking notice indicates No connection and no sync attempt is made. Given syncing is in progress, When the operator resumes from paused state, Then queued items begin syncing within 2 seconds.
Failed Items View and Retry
Given one or more items failed to sync, When the operator opens Failed items, Then a list shows each item with attendee name/ID, class/session, failure reason, and timestamp. Given the Failed items list is visible and connectivity is available, When the operator taps Retry on an item, Then the item shows In progress, and upon success it is removed from the failed list and reflected in attendance within 1 second. Given multiple failed items and connectivity is available, When the operator taps Retry all, Then retries are attempted with exponential backoff for repeated failures and a summary of successes/failures is displayed. Given connectivity is unavailable, When the operator attempts Retry or Retry all, Then a notice indicates retries require a connection and no retry attempts are made. Given accessibility tools are in use, When navigating the Failed items view, Then all controls have accessible names, are focusable in logical order, and are operable via keyboard and touch.
Undo Last Action Within Short Window
Given a check-in or similar reversible action has occurred within the last 10 seconds, When the operator taps Undo, Then the action is reverted locally within 500 ms and a compensating event is queued for sync if needed. Given the last action has already synced, When Undo is tapped within the 10-second window, Then an uncheck-in (or reversal) event is queued and the UI reflects the reversal within 500 ms. Given more than 10 seconds have passed or another action has been performed, When the operator looks for Undo, Then the Undo control is disabled or not presented. Given Undo is tapped multiple times, When the action is already reverted, Then no additional reversal is queued (idempotent) and the UI continues to show the reverted state. Given Undo is performed, When the reversal is applied, Then a non-blocking toast confirms Undone and scanning remains uninterrupted.
Post-Sync Conflict Outcome Surfacing
Given the device reconnects and a sync completes, When conflicts are detected and resolved (e.g., duplicate scan, canceled booking), Then a non-blocking summary appears within 2 seconds listing the number of conflicts and their outcomes with links to details. Given a duplicate check-in occurred offline, When the sync resolves, Then only one attendance record exists for the attendee/session and the outcome reads Duplicate ignored. Given an item cannot be applied due to capacity or policy, When the sync completes, Then the failed item remains in Failed with a clear reason and suggested next step. Given the activity log is opened, When the last sync is reviewed, Then conflict resolutions are recorded with timestamps and operator/device identifiers.
Kiosk Mode Accessibility and No-Extra-Steps
Given kiosk mode is enabled, When rendering actionable controls (Sync now, Pause syncing, View failed), Then each tap target is at least 44x44 px and has a visible focus state. Given text and iconography are displayed, When measured, Then color contrast meets WCAG 2.1 AA (text >= 4.5:1; large text/icons >= 3:1) and labels are provided for screen readers. Given a continuous stream of scans occurs (>= 20 per minute) while offline, When measuring responsiveness, Then median time from scan to feedback (toast or haptic) is <= 300 ms and 95th percentile <= 500 ms. Given the operator is checking in attendees, When offline or online, Then no additional confirmations or dialogs are required to record a check-in; scanning remains a single action flow. Given keyboard or barcode scanner input is used, When navigating the interface, Then all critical controls are reachable and operable without touch and without timeouts interrupting input.
Audit Trail & Admin Review Console
"As a studio administrator, I want an auditable history of offline check-ins and their sync outcomes so that I can investigate disputes and ensure compliance."
Description

Record a complete event lineage from offline capture through sync and resolution, including device, operator, timestamps (local and adjusted), outcomes, and conflict decisions. Expose an admin console view with filters (date, class, device, outcome), export capability, and per-event detail for troubleshooting. Enforce data retention and PII access controls aligned with compliance. Integrate with existing reporting pipelines to include ‘offline vs online’ source and resolution metrics.

Acceptance Criteria
Audit Log Captures Complete Event Lineage
- Given an attendee check-in is captured while the device is offline When the scan is saved Then a local audit record is created with fields: event_id, class_id, member_id, device_id, operator_id, action, outcome, local_timestamp, timezone_offset_minutes, source="offline" - Given the device reconnects When the audit record is synced Then the server persists the record and stamps server_received_at_utc and adjusted_timestamp_utc and sets sync_status="success" - Given a conflict is detected during sync When conflict resolution executes Then the audit record includes a resolution entry with resolution_type, resolved_by, resolution_timestamp_utc, winning_event_id, losing_event_id, and final_outcome
Admin Console Filters by Date, Class, Device, Outcome
- Given audit events exist across multiple dates, classes, devices, and outcomes When an admin applies any combination of date range, class, device, and outcome filters Then the results list shows only events matching all selected filters - Given no events match the active filters When the list is displayed Then the console shows a zero-results state with an option to clear filters - Given a dataset of at least 10,000 events When filters are applied Then the first page of results renders within 2 seconds and the total count reflects the filtered set
Per-Event Detail Drawer Displays Full Trace
- Given an admin opens an event’s detail When the detail view loads Then it shows local_timestamp, adjusted_timestamp_utc, device_id, operator_id, outcome, source, and the raw capture payload - Given the event has multiple sync attempts or a conflict When the detail view is open Then a chronological timeline displays capture, sync attempts (with timestamps and status), and any resolution steps with reason codes - Given the admin lacks PII permission When the detail view is open Then member name, email, and phone are redacted
Export Audit Trail to CSV and JSON With Filters Applied
- Given filters are applied When Export CSV is requested Then the CSV contains exactly the filtered rows and columns in UTF-8 with headers and respects PII permissions - Given Export JSON is requested When the export completes Then the JSON contains the same records and fields as the CSV, preserving data types - Given the filtered set is up to 100,000 rows When export is requested Then the export completes without truncation or data loss
PII Access Control and Redaction in Audit Console
- Given a user without Audit.PII.View permission When viewing lists, details, or exports Then member PII (name, email, phone) is redacted and excluded from exports - Given a user with Audit.PII.View permission When viewing lists, details, or exports Then member PII fields are visible and included - Given any PII is displayed or exported When the action occurs Then an access log entry is recorded with user_id, timestamp_utc, and scope="audit_pii_access"
Data Retention Policy Enforcement and Purge Logging
- Given retention_days is configured When an audit record’s age exceeds retention_days Then the record and associated PII are purged and no longer retrievable in console, API, or exports - Given a purge job runs When it completes Then a purge log entry exists with purge_job_id, run_timestamp_utc, criteria, and count_purged - Given retention_days is updated When the next purge job runs Then the new policy is applied prospectively and previously purged data are not restored
Reporting Pipeline Includes Source and Resolution Metrics
- Given a synced audit event When the ETL processes it Then the record published to the reporting pipeline includes fields: source (offline|online), outcome, resolution_type (none|duplicate_discarded|merged|denied), and sync_latency_ms - Given daily aggregates are computed When dashboards are refreshed Then counts by source and by resolution_type match the underlying events within a tolerance of zero - Given a late conflict resolution changes an event When the next ETL cycle runs Then the reporting dataset and aggregates are corrected to reflect the latest resolution

Late Window Rules

Set grace periods and cutoffs that auto-mark late or no-show after the scan window closes. Triggers your chosen outcomes—credit return, pass decrement, fee, or waitlist release—without manual reconciliation. Creates clear, fair policies that reduce disputes and admin time.

Requirements

Configurable Late/No‑Show Windows
"As a studio owner, I want to set clear late and no-show time windows per class so that attendance is auto-classified consistently and fairly without manual tracking."
Description

Allow owners to define per-organization defaults and per-class overrides for check-in scan window, post-start grace period, late threshold, and no-show cutoff. Rules are time zone aware, support recurring schedules, and can vary by class type, location, instructor, and product (drop-in vs pass/membership). Enforce a hard close on the scan window that automatically transitions attendance states from On-time → Late → No-show without manual reconciliation. Provide validation (e.g., cutoff cannot precede start time), previews of effective policy, and API/CSV import for bulk updates. Ensure idempotency and deterministic outcomes for edge cases such as early check-ins, back-dated scans, multi-device scans, and daylight saving changes.

Acceptance Criteria
Override Resolution Across Type/Location/Instructor/Product
Given organization defaults exist and multiple overrides are defined at class type, location, instructor, and product levels When a class instance matches more than one override Then the system resolves a single effective policy using precedence: class-specific > product > instructor > location > class type > organization default And the effective policy (window_open, grace, late_threshold, no_show_cutoff) is displayed on class detail and booking pages And GET /classes/{id}/policy returns the resolved values with the winning source for each field And for recurring series, an occurrence-level override supersedes the series-level override for that date only
Hard Close and Auto-Transition of Attendance States
Given class start time T and configured values: window_open_minutes_before_start = O, grace_minutes = G, late_threshold_minutes = L, no_show_cutoff_minutes = N When a member scan with timestamp ts is processed Then if ts is on or after (T - O) and on or before (T + G), mark attendance = On-time And if ts is after (T + G) and on or before (T + L), mark attendance = Late And if ts is after (T + N) or no eligible scan occurs by (T + N), mark attendance = No-show And scans with ts after (T + N) are rejected with error "Check-in window closed" And state transitions at (T + G) and (T + N) occur automatically within 60 seconds without manual action, even if no scans occurred
Configuration Validation and Error Messaging
Given a user attempts to save policy settings via UI or API When values are provided for window_open_minutes_before_start (O), grace_minutes (G), late_threshold_minutes (L), no_show_cutoff_minutes (N) Then O, G, L, N must be integers in [0, 1440] And L >= G and N >= L And (T - O) <= T (window open cannot be after start) And N >= 0 relative to T (no-show cutoff cannot precede start) And invalid inputs block save and display field-level errors in UI; API responds 422 with error codes per field
Time Zone Accuracy and Daylight Saving Handling
Given a class scheduled with an IANA time zone (e.g., America/New_York) When computing scan window open, grace, late, and no-show boundaries for each occurrence Then all boundaries are evaluated in the class’s local time zone And on DST transitions, boundaries occur at intended wall-clock times with correct UTC offsets And API payloads must include ISO 8601 timestamps with offsets; if offset is omitted, the class’s configured time zone is applied And ambiguous fall-back times are resolved deterministically to the first occurrence unless an explicit offset is provided
Idempotent Processing for Early, Back-Dated, and Multi-Device Scans
Given scans include scan_id (idempotency key), member_id, class_id, and timestamp ts When multiple scans are received for the same member and class Then duplicate scan_ids are ignored (no duplicate side effects) And if multiple unique scans exist, the earliest eligible ts within the open window determines attendance state And scans with ts before the scan window open ((T - O)) are rejected with error "Too early to check in" And scans received after (T + N), regardless of ts, do not alter a No-show state and are rejected with error "Check-in window closed"
Bulk Policy Update via API and CSV Import
Given an admin submits bulk policy updates via API or CSV import When the payload contains multiple rows with identifiers (org_id and either class_id or filters for type/location/instructor/product) Then valid rows are applied atomically per row; invalid rows are rejected with row number and field errors And a dry_run=true option returns a diff and affected counts without persisting And requests include Idempotency-Key; retries are safe and do not create duplicate updates And filter-based updates return the count of affected classes and expose a job id for asynchronous processing when count exceeds threshold
Effective Policy Preview and Recurring Schedule Support
Given a user opens the Policy Preview for a class or filter selection When the preview is generated Then it displays the resolved window_open, grace, late_threshold, and no_show_cutoff with labeled On-time/Late/No-show boundaries And it lists the next 10 occurrences with local dates/times and the effective policy per occurrence And occurrence-level overrides are reflected on their specific dates And GET /policy/preview returns the same data structure for the provided inputs
Outcome Rule Engine & Actions
"As an operator, I want late/no-show outcomes to trigger automatically based on my policies so that credits and fees are handled accurately without manual work."
Description

Implement a rules engine that maps time-based triggers (late threshold reached, scan window closed, no-show cutoff) to outcomes including credit return, pass decrement, late/no-show fee, and waitlist release. Support conditional logic (e.g., waive fee for first class, exempt memberships, apply different outcomes for class types), execution ordering, and retries with idempotency keys. Integrate with payments to place and capture fees securely, with configurable tax handling and receipts, and with entitlement systems to adjust passes and credits. Provide an admin UI to configure actions, preview impacts, simulate scenarios, and log decisions for auditability. Expose webhooks/events for external systems.

Acceptance Criteria
Late Threshold: Apply Late Fee with Tax and Receipt
Given a class with a late threshold of 10 minutes after start and a rule "Late Fee $5.00, tax code TX-STD 10%" is enabled And the learner has a valid default payment method and is marked Late at t = start + 12 minutes When the rule engine processes the "late threshold reached" trigger with idempotency key K1 Then exactly one payment is captured for $5.50 in the booking currency And a receipt is sent via email and SMS link with line items "Late Fee $5.00" and "Tax $0.50", including receipt ID, class ID, and timestamp And the attendance status remains Late And reprocessing the same trigger with the same K1 results in no additional charge
No-Show Cutoff: Pass Decrement and Waitlist Release with Ordering
Given a class with a no-show cutoff at start + 15 minutes and rule outcomes [decrement pass by 1; release seat to waitlist] with execution order [entitlement -> waitlist] And the learner has an active 10-class pass with balance 7 and has not scanned in And the waitlist has at least one candidate When the "no-show cutoff reached" trigger fires with idempotency key K2 Then the pass balance is decremented once to 6 and an audit log entry is written with rule version, trigger, and idempotency key And the seat is released to the top waitlisted candidate and a notification is sent And replaying with K2 does not change the pass balance or send duplicate offers And if the pass balance is 0, the waitlist release still occurs and an entitlement-skip reason is recorded
Conditional Outcomes and Precedence: Membership, First-Class Waiver, Class-Type Override
Given rules: - If membership = Gold then waive late/no-show fee (priority 1) - Else if class_type = Workshop then apply pass decrement instead of fee (priority 2) - Else if first_attended_class = true then waive late fee once (priority 3) And evaluation mode is first-match-wins When user U1 (non-member, class_type Yoga, first_attended_class true) triggers "late threshold reached" Then the system applies "Waive Late Fee (first class)" once and records the waiver usage for U1 When user U2 (membership Gold, class_type Yoga, first_attended_class false) triggers "late threshold reached" Then the system applies "Waive Late Fee (membership)" and records the rule matched When user U3 (non-member, class_type Workshop, first_attended_class false) triggers "late threshold reached" Then the system applies "Pass Decrement" and no fee is charged And the decision trace for each user includes evaluated conditions, matched rule priority, and final outcomes
Action Execution Order and Atomicity
Given a rule with actions [charge $10 late fee; decrement pass by 1] and configured order [payment -> entitlement] And the user has a pass balance 3 and a valid payment method When payment succeeds Then pass balance decrements to 2 and both actions are recorded in the audit log in the configured order When payment fails with a transient error Then no entitlement change occurs, a retry job is queued with exponential backoff and idempotency key K4, and the failure is logged And on retry success the entitlement change is applied once and no duplicate charges occur Given a rule with actions [return 1 credit; charge $3 late-cancel fee] and configured order [entitlement -> payment] When payment fails Then the previously returned credit is rolled back and the audit log shows a compensated transaction linked to the failed payment And when the retry later succeeds, exactly one credit is granted and exactly one fee is captured
Admin UI: Configure, Validate, Preview, and Simulate Rules
Given an admin with permissions accesses Late Window Rules When they create a new rule with conditions and actions, set execution order, and save Then the UI validates required fields, prevents conflicting actions, and shows a summary diff When the admin uses Preview on a target class and user Then the UI displays the matched rule, projected outcomes, fees/taxes, and impacted entitlements without persisting changes When the admin runs a Simulation for a past class with sample users Then a non-persistent run is executed and a downloadable decision report is available
Decision Logging and Auditability
Given the rule engine processes any Late/Late Window/No-Show trigger When decisions are made Then a log record is written containing rule ID and version, trigger name and timestamp, inputs (user ID, booking ID, class ID, class start, time offsets), evaluated conditions, matched outcomes, execution order, side-effect references (payment intent ID, receipt ID, entitlement adjustment ID, waitlist action ID), idempotency key, and actor/system And logs are queryable by time range, user, booking, and rule ID and exportable as CSV and JSON And log retention is at least 365 days
Webhooks and Event Delivery with Idempotency and Signatures
Given webhooks are configured for event types [late.applied, noshow.applied, fee.captured, pass.decremented, credit.returned, waitlist.released] When the engine emits events for a processed trigger with idempotency key K7 Then one event per outcome is sent with a shared correlation ID and signature using the configured secret and HMAC-SHA256 And retries follow 1m, 5m, 15m backoff up to 10 attempts on non-2xx responses And recipients can deduplicate using event id and idempotency key; duplicate deliveries do not cause duplicate side effects And a dead-letter queue retains undelivered events for at least 7 days with admin visibility
Attendance Scan Integration & Auto-Status
"As an instructor, I want the system to auto-mark students based on when they scan so that I don’t spend time reconciling attendance during class."
Description

Tie the late window rules into all check-in mechanisms (QR/NFC scan, roster tap, manual mark). Persist immutable scan timestamps and derive attendance status transitions in real time. Disable check-in attempts after the scan window closes unless user has override permissions. Handle offline scanning with queued sync, conflict resolution, and correct backfill behavior against cutoffs. Prevent double-bookings and duplicate scans, and surface clear UI states to instructors (e.g., Late, Window Closed). Provide role-based overrides with reason capture.

Acceptance Criteria
Real-Time Status from Any Check-In Mechanism within Window
Given a class has scan open time T_open and close time T_close with late window rules configured And the participant is enrolled and eligible to attend And current server time is between T_open and T_close When the participant is checked in via QR scan, NFC tap, roster tap, or manual mark Then the system records one immutable check-in event with fields: eventId, classId, userId, method (QR/NFC/Roster/Manual), serverTimestamp (UTC ISO-8601), sourceDeviceId (if applicable), and createdBy And the attendance status transitions to Present within 2 seconds in instructor and participant views And the event is read-only via UI and API; any correction creates a new linked event without altering the original timestamp And an audit log entry captures actor, prior status, new status, and eventId
Late Status During Grace and Auto No‑Show After Cutoff
Given a class defines a late grace period G after T_close and has late/no-show outcomes configured And the participant has a valid booking and no prior attendance event When a check-in occurs after T_close and before T_close + G Then the status transitions to Late and configured outcomes (e.g., fee, pass decrement) execute within 5 seconds and are idempotent per user/class And a notification/receipt is sent and logged When the system time reaches T_close + G for any participant without a check-in event Then the status transitions to No-show and no-show outcomes execute within 5 seconds and are idempotent per user/class And all transitions and outcomes are recorded in the audit log
Check-In Disabled After Window Without Override
Given current server time is at or after T_close + G for the class When a participant or non-privileged staff attempts check-in via QR, NFC, roster tap, or manual mark Then the attempt is rejected with error code WINDOW_CLOSED And no attendance event is created or modified And the instructor UI visibly displays Window Closed and the participant UI shows that check-in is unavailable And the rejected attempt is logged with actor, time, and method
Role-Based Override with Reason Capture
Given a staff member with override permission is authenticated and scoped to the class And current server time is after T_close (with or without grace remaining) When the staff member initiates a check-in override for a participant Then the system requires a non-empty reason to proceed And upon confirmation, an override event is recorded with fields: overrideEventId, actorUserId, targetUserId, classId, reason, method, serverTimestamp, and priorStatus And attendance status transitions per configured override policy (e.g., Overridden Present or Overridden Late) and corresponding outcomes are applied or reversed accordingly within 5 seconds And the audit log records actor, reason, policy applied, and resulting status; events remain immutable
Offline Scanning, Sync, Conflict Resolution, and Cutoff Backfill
Given the check-in device is offline and has a last-known server clock offset captured within the past 24 hours When QR/NFC scans are performed, the client enqueues events with UUID, localTimestamp, method, userId, classId, and deviceId And upon reconnect, the client uploads queued events preserving order and metadata Then the server deduplicates by UUID, reconstructs serverTime from localTimestamp and saved offset, and evaluates each against T_open, T_close, and G And events resolving before T_close set status Present; between T_close and T_close + G set Late with outcomes; after T_close + G are rejected unless accompanied by a valid override event And multiple events for the same user/class resolve to the earliest valid event; later duplicates are ignored and logged And up to 100 queued events are processed within 10 seconds of reconnect and a sync completion indicator is shown to the user
Duplicate Scan and Double-Booking Prevention
Given a participant already has an attendance event (Present/Late/Overridden) for the class When any additional scan or check-in attempt is made for the same class Then the system returns idempotent success with code ALREADY_CHECKED_IN and does not create a new event And when the participant holds bookings for two classes whose scanning windows overlap by any amount Then checking in to one class blocks check-in to the other and returns error DOUBLE_BOOKED, without creating an event for the blocked class And all prevented attempts are logged with userId, classIds, time, and method
Instructor UI State Indicators and Timestamp Visibility
Given the class is active and late window rules are in effect When the scanning window opens, is in progress, enters grace, and closes Then the instructor roster displays state indicators: On-time, Late, Window Closed, No-show, and Overridden And when a participant’s status changes, the UI updates within 2 seconds and shows the immutable timestamp and method on tap/hover And indicators and tooltips meet accessibility guidelines and are consistent across web and mobile
Policy Surfacing & Notifications
"As a student, I want to see and be reminded of the late/no-show policy and deadlines so that I can avoid fees and understand outcomes."
Description

Surface late/absence policies transparently across the booking journey: on class pages, at checkout, and in confirmation/Reminder SMS/email with localized times and clear cutoffs. Send outcome notifications when a fee is charged, a credit is returned, or a waitlist seat is released. Respect quiet hours, communication preferences, and localization. Provide template controls, per-brand theming for white-label embeds, and preview of policy language. Include in-app banners for impending cutoffs (e.g., “Check in within 5 minutes to avoid late fee”).

Acceptance Criteria
Class Page Policy Display with Localized Cutoffs
Given a public class page with Late Window Rules configured and a viewer in any timezone When the page loads on desktop (≥1024px width) or mobile (≥375px width) Then the late/absence policy snippet is visible within the first viewport near the booking call-to-action And cutoff and grace-period times render in the viewer’s local timezone and locale format And a "View full policy" link opens a modal with the complete policy text And the policy values reflect the class’s active Late Window Rules And the policy block inherits the brand’s theme (logo, colors, fonts) for white-label embeds.
Checkout Policy Surfacing
Given a user proceeds to checkout for a class with Late Window Rules When the checkout screen renders Then a policy summary with localized cutoff timestamps appears in the order review section And the summary includes a persistent link to the full policy modal And the policy content matches the latest saved policy template for the brand And policy text remains visible or sticky during scroll on desktop and mobile layouts.
Confirmation and Reminder Messages Include Policy Cutoffs
Given a successful booking for a class with Late Window Rules When confirmation email and SMS are generated Then both messages include the check-in window close, late cutoff, and no-show cutoff as localized timestamps And both messages include a link to the full policy And the copy and styling use the brand’s selected templates and theme And the reminder message scheduled before class includes the same policy details And all displayed times are computed from the class schedule but rendered in the recipient’s local timezone and locale formatting.
Outcome Notifications Respect Quiet Hours and Preferences
Given a fee charge, credit return, or waitlist seat release is triggered by Late Window Rules When notifications are queued for the affected user Then the system selects channels per the user’s communication preferences (e.g., SMS off, Email on) And messages that would be sent inside configured quiet hours are deferred to the end of quiet hours in the recipient’s local timezone And if all outbound channels are disabled, an in-app notification is created and the event is logged And exactly one notification per outcome is sent And the notification body includes the outcome type, amount or credit delta, time applied, and a link to the policy.
Template Controls and Preview
Given an admin with permissions edits policy and notification templates for a brand When they save template changes Then templates support the variables: {class_name}, {venue_name}, {start_time_local}, {check_in_close_local}, {late_cutoff_local}, {no_show_cutoff_local}, {late_fee_amount}, {credit_delta}, {waitlist_release_time_local}, {policy_link}, {brand_name} And validation blocks save if a required template is missing any required variable And a live preview renders with sample data for email and SMS, showing localized timestamps and brand theming And previous versions are retained with timestamp and editor for rollback And changes apply only to messages generated after the save time.
In-App Impending Cutoff Banner
Given a booked student has not checked in and the check-in window will close within 5 minutes When the student opens the app or class details view Then an in-app banner displays "Check in within N minutes to avoid late fee" with a countdown timer And N updates at least once per minute until the window closes or the student checks in And tapping the banner opens the check-in flow And the banner disappears after check-in or after the cutoff passes And the banner respects quiet hours by suppressing push notifications but still shows when the app is opened.
Localization and DST Behavior
Given a brand default locale and a user preferred locale/timezone (including DST transitions) When policy text and timestamps are rendered on pages and in messages Then dates, times, currency, and numbers format according to the user’s locale And timezone abbreviations or UTC offsets are included where space allows And supported locales include en-US, en-GB, fr-FR, es-ES, de-DE, ja-JP with fallback to the brand default And timestamps remain correct across DST changes (no one-hour drift) And automated tests cover at least one class scheduled on a DST transition day.
Dispute Management & Audit Trail
"As support staff, I want a clear audit trail and override tools so that I can resolve late/no-show disputes quickly and transparently."
Description

Record a complete, immutable audit trail of rule evaluations, timestamps, outcomes, payment attempts, and overrides. Provide an admin workflow to review disputes, waive or refund fees, restore credits, and annotate with reason codes. Changes must update reports and maintain referential integrity. Include exportable logs, role-based access, and SLA metrics to reduce support time and resolve edge cases fairly.

Acceptance Criteria
Immutable Audit Trail for Late Window Rule Evaluations
Given a class session with Late Window Rules configured When an attendance scan occurs or the scan window closes and an evaluation is performed Then the system records an append-only audit event with fields: event_id, event_type (evaluation|outcome|payment_attempt|override), timestamp_utc (ISO 8601), rule_version_id, inputs (scheduled_start, scan_time, lateness_thresholds, user_id, booking_id), evaluation_result (on_time|late|no_show), outcomes_applied (credit_return|pass_decrement|fee|waitlist_release), and actor (system|user_id) And when an outcome triggers a payment attempt Then a payment_attempt sub-event is recorded with gateway, amount, currency, idempotency_key, status (initiated|succeeded|failed|reversed), provider_response_code, provider_txn_id, and correlation to the evaluation event_id And when an admin performs an override Then an override event is appended capturing admin_user_id, reason_code (from managed list), freeform_note, previous_outcome, new_outcome, and timestamp_utc; original entries remain unchanged And then audit events are tamper-evident (hash chain or write-once storage) and updates/deletes are disallowed; any correction is appended as a new event referencing prior_event_id And then each booking_id’s complete trail for the last 90 days is retrievable within 200 ms P95 and 500 ms P99
Dispute Review and Resolution Workflow
Given an admin with Dispute:Resolve permission views a disputed booking When opening the dispute detail page Then a unified timeline of audit events, outcomes, payment attempts, overrides, and communications is displayed in chronological order When the admin selects an action (waive fee|refund|restore credit|reverse pass decrement|adjust attendance status) Then the system requires selection of a reason_code from the managed list and allows an optional note (max 2,000 chars) And then the selected action executes atomically, creates a dispute_resolution event linking to the original evaluation/outcome event_id, and updates user balances/attendance accordingly And financial actions are idempotent using idempotency keys; duplicate submissions within 5 minutes do not double-charge or double-refund Then Finance, Attendance, and Payout reports reflect the adjustment within 5 minutes, showing an explicit adjustment line with reason_code and actor And if the action changes a customer’s financial or credit state, customer and instructor notifications are sent using the configured templates and recorded as notification_sent events
Role-Based Access Controls for Audit and Disputes
Given roles Owner, Manager, Support, and Instructor are defined When accessing audit trails and disputes Then permissions are enforced as follows: Owner/Manager (view/export/resolve), Support (view/resolve, no export), Instructor (view timeline for own classes, no financial amounts or PII) And unauthorized attempts return HTTP 403 and a security event access_denied is recorded with actor, resource, and timestamp And PII fields (email, phone, billing details) are masked unless the user has PII_VIEW permission; exports respect the same masking And permission changes take effect within 60 seconds and are themselves audited (permission_change event with actor, target_user, diff)
Exportable Audit Logs with Filtering and Redaction
Given an admin with Export permission applies filters (date range, org_id, class_id, user_id, booking_id, event_type, outcome, reason_code, actor) When requesting an export Then the export returns only matching records in CSV or JSON, includes schema_version, column headers, and UTC timestamps in ISO 8601 And exports up to 100,000 rows begin streaming within 60 seconds and complete within 10 minutes P95; a resume token supports continuation after interruption And sensitive fields (payment tokens, idempotency_key, provider_txn_id) are hashed or redacted per policy; PII masking follows role permissions And an export_event is logged with requester_id, filters_applied, format, row_count, started_at, completed_at, and status (succeeded|failed|canceled)
SLA Metrics and Operational Dashboard for Disputes
Given disputes are created via customer contact or auto-flags When viewing the disputes dashboard Then the system displays metrics: count_open, time_to_first_response (median/P95), time_to_resolution (median/P95), reopen_rate, backlog_age_buckets, and breakdowns by instructor/class and reason_code And SLA targets are configurable per organization (e.g., first response 12h, resolution 72h) And breaches are highlighted, generate an sla_breach event, and can notify via email/Slack if enabled And dashboard metrics refresh at least every 5 minutes and can be exported to CSV/JSON And metric aggregates reconcile to underlying events within 1% over a 30-day period
Data Integrity, Concurrency, and Idempotency Controls
Given multiple admins may act on the same booking concurrently When two conflicting dispute actions are submitted Then optimistic concurrency or compare-and-swap ensures only one succeeds; the other receives HTTP 409 with retry guidance, and both attempts are audited And all financial and state changes are atomic per booking_id + action_type and idempotent to prevent duplicate refunds, charges, or credit grants And referential integrity is enforced: all events must reference existing booking_id, user_id, class_id; deletes are prohibited—cancellations are soft via status fields; orphaned records are rejected And payment gateway retries use exponential backoff with idempotency keys; each attempt and final outcome are recorded; partial failures trigger auto-reconciliation jobs within 15 minutes And downstream reports (finance, attendance, payout) achieve consistency within 5 minutes P95; lag beyond this emits a consistency_delay alert event
Waitlist Auto-Release & Seat Reallocation
"As a studio manager, I want seats to be auto-released to the waitlist when someone is late so that class capacity is maximized without manual intervention."
Description

When a booked attendee fails to check in by the configured pre- or post-start threshold, automatically release their seat to the next eligible waitlisted user based on priority rules. Notify the new attendee, require rapid confirmation if configured, and handle payment/credit adjustments accordingly. Respect capacity constraints, per-product policies, and prevent race conditions during high-demand drops. Expose configuration for release timing, confirmation windows, and exemptions.

Acceptance Criteria
Auto-Release at Pre-Start Threshold
Given a class with capacity 20, a booked attendee A, a pre-start release threshold of 10 minutes, and confirmation not required And A has not checked in by 10 minutes before class start When the release job runs at the threshold Then A’s seat is released and A’s booking status updates to "released-late" And the next eligible waitlisted user W1 is auto-enrolled And class enrolled count remains <= capacity And A receives a release notification and W1 receives a booking confirmation via SMS/email per their preferences within 60 seconds And a single audit log entry is created with actor=system, reason=late-window, previousStatus=booked, newStatus=released-late, and timestamp
Post-Start Release with 2-Minute Confirmation Window
Given class start time has passed, a post-start release threshold of 5 minutes, and a confirmation window of 2 minutes And attendee A is booked and not checked in by start+5 minutes And W1 is the next eligible waitlisted user When the system sends an offer to W1 at start+5 minutes Then W1 has 2 minutes to accept via a unique confirmation link And upon acceptance within 2 minutes, W1 is enrolled and payment is captured or credit is applied per W1’s account And if W1 does not accept within 2 minutes, the offer expires and W2 is offered immediately, repeating until filled or waitlist exhausted And all expired offers are marked "timed-out" and are not re-sent And the offer notification reaches W1 via SMS/email per their preferences within 60 seconds
Priority Queue and Eligibility Enforcement
Given a waitlist with users W1, W2, W3 and configured priority rules: membership tier (Gold > Standard) then time joined And the class requires a valid pass/credit or on-file payment method and a signed waiver When a seat is released Then the system selects the highest-priority eligible user And ineligible users (no valid pass/credit, missing waiver, overlapping booking) are skipped with a reason recorded per user And the final selection is deterministic for the same inputs and reproducible via audit log entries
Return Credit on Pre-Start Auto-Release
Given the product policy ReturnCreditOnAutoRelease=true for this class And attendee A paid with a single-class credit When A’s seat is auto-released at the pre-start threshold Then A’s credit balance increases by 1 within 60 seconds And no late fee is charged to A And W1’s enrollment captures payment or decrements pass according to W1’s account And the ledger records a reversal for A and a charge for W1 linked to the class ID, with balanced totals
Forfeit and Late Fee on Post-Start No-Show Auto-Release
Given the product policy ForfeitOnNoShow=true and LateFee=5.00 USD And attendee A did not check in by the post-start threshold When A’s seat is auto-released after class start Then A’s pass/credit is decremented and a 5.00 USD late fee is charged And W1 is offered the seat per the confirmation flow And A receives a receipt including the fee line item within 5 minutes And ledger entries reflect the forfeit and fee with correct amounts and currency
Race-Safe Reallocation and Capacity Integrity
Given concurrent events: the release job runs at the threshold, A attempts check-in, and W1 attempts offer acceptance When operations execute under system locking and idempotency controls Then if A successfully checks in before the release transaction commits, the release is aborted and no offers are sent And at most one seat is allocated from the release event And enrolled count is never less than 0 nor greater than class capacity And repeated processing of the same job/webhook request ID does not create duplicate enrollments or notifications
Configuration, Exemptions, and Persistence
Given an admin sets pre-start threshold=10 minutes, post-start threshold=5 minutes, confirmation window=2 minutes, and exemption tag="VIP" for the class When the configuration is saved via UI or API Then the values persist and are returned by GET /classes/{id}/late-window And attendees with the "VIP" tag are not auto-released and are only marked "late" per policy And changes apply to future release evaluations and do not alter past bookings And the configuration is visible to instructors in the class policy summary

Dynamic QR Shield

The class QR rotates every few seconds and is bound to session, location, and time window to block screenshots and early scans. Reuse attempts surface a friendly help prompt for the attendee and an alert for staff. Prevents ghost check-ins while keeping honest guests fast.

Requirements

Rotating QR Token Service
"As an instructor, I want the check-in QR to refresh automatically every few seconds so that screenshots and stale codes can’t be reused while my class lines move quickly."
Description

Build a server-driven service that generates cryptographically signed, time-sliced QR payloads that rotate every N seconds (configurable) and encode session_id, venue_id, valid_from/valid_to, and rotation_index. Tokens must be compact, tamper-evident (e.g., HMAC or EdDSA), and resilient to clock skew (±X seconds) while enforcing strict expiry to make screenshots unusable. The QR content resolves to a short URL with the token; image/SVG rendering must complete under 100 ms to ensure smooth auto-refresh on signage and host devices. Implement key rotation, secrets management, and replay-hardening via per-slice entropy and strict TTLs. Provide SDK hooks for web and native hosts to auto-refresh, prefetch the next code, and handle low-connectivity gracefully. Expected outcome: frictionless, secure rotating codes that are fast to render and impractical to reuse outside their time slice.

Acceptance Criteria
Time-Sliced Rotation With Skew Tolerance
Given rotation_period N=5s and skew_tolerance X=30s are configured When the host requests rotating tokens for a session_id and venue_id Then each issued token has valid_from and valid_to exactly N seconds apart And the validator accepts scans only within [valid_from - X, valid_to + X] And tokens presented before valid_from - X return error code TOKEN_EARLY And tokens presented after valid_to + X return error code TOKEN_EXPIRED And rotation_index increments by 1 per slice within a session and resets for new sessions
Compact, Tamper-Evident Token
Given a token is generated for a session_id and venue_id Then the payload includes session_id, venue_id, valid_from, valid_to, rotation_index, and slice_nonce And the token is cryptographically signed (HMAC-SHA256 or EdDSA) and any payload modification fails verification And the encoded token length is <= 180 characters (base64url or URL-safe form) And no PII is present in the token payload
Strict Expiry and Replay Hardening
Given a token is scanned and validated successfully once When the same token is presented again (within or after its validity window) Then validation fails with error code TOKEN_REPLAY And the attendee-facing response shows a friendly help prompt with next steps And a staff-facing alert event replay_detected is emitted with session_id, venue_id, rotation_index, and timestamp And slice_nonce is unique per (session_id, venue_id, rotation_index) within a 24h window; issuance rejects any duplicate
Short URL Resolution and Context Validation
Given a QR is scanned by a client Then the QR content is an HTTPS short URL on the configured short domain that carries the token as a parameter And resolving the short URL returns a 302/307 redirect to the validation endpoint within 200 ms p95 And the validator parses the token and confirms session_id and venue_id match the check-in context; mismatches return WRONG_SESSION or WRONG_VENUE And malformed tokens return HTTP 400 with a human-readable message and no sensitive details
QR Image/SVG Rendering Performance
Given any valid token When rendering the QR in PNG and SVG formats via the SDK on reference devices/browsers Then time from refresh tick to new QR rendered is <= 100 ms at p95 and <= 50 ms at p50 And rendering does not drop UI frames (no freezes > 16 ms consecutive over 3 frames) during auto-refresh And generated assets meet minimum contrast and module size for reliable scanning at typical viewing distance (per library defaults)
SDK Auto-Refresh, Prefetch, and Offline Handling
Given the SDK is initialized with rotation_period N and skew_tolerance X When the current slice approaches its boundary Then the SDK prefetches the next token at least 500 ms before valid_to And at the boundary, the SDK swaps in the prefetched token without visual flicker And if network is unavailable, the SDK uses the prefetched token; if none is available and the previous token is past valid_to + X, it shows an offline state and does not display the expired code And the SDK emits onRefresh, onPrefetchSuccess, onPrefetchError, and onOffline events and retries with exponential backoff
Key Rotation and Secrets Management
Given signing keys are managed in KMS/HSM with rotation enabled When a rotation occurs Then new tokens include a key identifier (kid) and are signed with the new active key; verifiers accept both old and new keys during the configured overlap window And tokens signed with a retired key are accepted only until the end of their slice plus skew_tolerance And no raw secrets appear in logs, crash reports, or client artifacts; configuration and JWKS endpoints update within 60 seconds and respect cache headers
Time & Session Binding Validation
"As an attendee, I want my scan to instantly confirm the correct class check-in so that I don’t wait in line or worry about mistakes."
Description

Implement backend validation that verifies token signature, time window, session/venue bindings, and class schedule windows (open/close) before recording a check-in. Enforce idempotency per attendee and session, returning consistent success even on repeated valid scans while blocking ghost check-ins without an authenticated or verified attendee identity. Provide precise error codes for expired, wrong session, outside window, or malformed token to support tailored UX. Capture device, IP, and coarse telemetry for anomaly scoring without storing sensitive PII beyond policy. Ensure sub-200 ms end-to-end validation under expected load to keep the flow instant. Expected outcome: only legitimate, in-window scans for the correct class are accepted and recorded reliably.

Acceptance Criteria
Valid In-Window Scan for Correct Session
Given a QR token signed with the platform’s active key containing sessionId and venueId, and an authenticated, verified attendee who has not checked in for this session And the current time is within the configured check-in window for the session When the client calls POST /v1/checkins/validate with the token and attendee context Then the service verifies signature, session/venue bindings, and window, and creates exactly one check-in linked to attendeeId+sessionId+venueId with a unique idempotency key (attendeeId, sessionId) And responds 200 with { code: "OK", sessionId, attendeeId, checkedIn: true, idempotent: false } And captures deviceId (ephemeral), sourceIp, userAgent, and coarseLocation telemetry per policy And writes an audit log with correlationId And no sensitive PII (name, email, phone) is persisted in check-in or telemetry
Idempotent Repeat Scans Return Consistent Success
Given an attendee already has a successful check-in record for a session When the attendee rescans a valid token for the same session within the allowed window, including rapid repeats and concurrent requests Then the service returns 200 with { code: "OK", sessionId, attendeeId, checkedIn: true, idempotent: true } for each repeat And no additional check-in records are created (count for attendeeId+sessionId remains 1) And concurrent duplicate requests produce at most one write operation and zero duplicate records
Time Window and Token Expiry Enforcement
Given a validly structured token When the scan occurs before the session check-in window opens Then respond 409 with { errorCode: "E_OUTSIDE_WINDOW", reason: "EARLY" } and do not record a check-in Given a validly structured token When the scan occurs after the session check-in window closes Then respond 409 with { errorCode: "E_OUTSIDE_WINDOW", reason: "LATE" } and do not record a check-in Given a token whose exp is earlier than now minus the allowed clock skew When validation is requested Then respond 401 with { errorCode: "E_TOKEN_EXPIRED" } and do not record a check-in And the validator honors a maximum clock skew tolerance of 30 seconds
Session and Venue Binding Enforcement
Given a token that is syntactically valid and signed correctly When the token’s sessionId or venueId does not match the targeted session and location for this scan Then return 409 with { errorCode: "E_WRONG_SESSION" } and do not record a check-in And record an audit event with reason "WRONG_SESSION_OR_VENUE"
Malformed or Tampered Token Handling
Given a token that is malformed (missing required claims, wrong format) or fails cryptographic verification (bad signature, unsupported alg, unknown key id) When the token is submitted to validation Then return 400 with { errorCode: "E_TOKEN_MALFORMED" } and do not perform any database writes And emit a security log with reason "TOKEN_MALFORMED" and a correlationId
Unauthenticated or Unverified Attendee Blocked
Given there is no authenticated user context or the attendee identity is not verified per policy When a scan attempt is submitted even with a structurally valid token Then return 401 with { errorCode: "E_ATTENDEE_UNVERIFIED" } and do not record a check-in And include helpPromptKey in the response for client-side guidance And log the attempt for anomaly scoring without storing sensitive PII
Performance and Telemetry Under Expected Load
Given the load test profile QR-Checkin-Expected (70% valid, 30% invalid requests; ramp 5m, steady 10m, ramp-down 5m; 300 RPS sustained; 500 concurrent users) When validation requests are executed through the public API edge Then p95 end-to-end validation latency per request is <= 200 ms and p50 <= 100 ms And for valid scans, success rate is >= 99.9%; for invalid scans, correct errorCode emission rate is >= 99.9% And telemetry is emitted for >= 99.5% of requests including deviceId, sourceIp, userAgent, coarseLocation, with automated checks confirming no PII fields are present
Location & Proximity Enforcement
"As a studio owner, I want check-ins to be accepted only when attendees are on-site so that people can’t spoof attendance from elsewhere."
Description

Add configurable proximity checks that bind check-in validity to the class location via geofence radius, approved Wi‑Fi SSIDs, or optional BLE beacon presence. Respect user privacy by using coarse location with explicit consent and provide a code-entry fallback if location permissions are denied. Allow per-class configuration of enforcement level (strict, balanced, lenient) and thresholds for GPS accuracy/age. Surface clear responses when scans occur outside the permitted zone and log details for review. Expected outcome: remote or off-site scans are rejected while on-site guests pass seamlessly.

Acceptance Criteria
Strict Mode: Geofence plus Network/BLE Required
Given a class with enforcement level = Strict and thresholds configured (geofence radius R=100m, GPS accuracy A<=50m, GPS age T<=20s, approved Wi‑Fi SSIDs list, BLE beacons with RSSI>=-75dBm observed <=15s), When an attendee scans the session QR, Then the check-in is accepted only if the device is within R with accuracy<=A and age<=T AND (connected to an approved SSID OR a configured BLE beacon is detected as specified), And the decision is returned within 2 seconds at the 95th percentile, And if location permission is denied, geofence cannot be evaluated and the system requires Wi‑Fi OR BLE presence to accept; otherwise the user is prompted to use code fallback.
Balanced Mode: Any Single Proximity Signal Suffices
Given a class with enforcement level = Balanced and the same thresholds (R=100m, A<=75m, T<=30s, approved SSIDs, BLE RSSI>=-80dBm <=20s), When an attendee scans the session QR, Then the check-in is accepted if any one signal passes (geofence OR approved SSID OR BLE beacon per thresholds), And if GPS accuracy> A or age> T or permission is denied, the geofence signal is treated as failed without blocking acceptance via SSID or BLE, And the system returns a decision within 2 seconds at P95.
Lenient Mode: Geofence-Only with Wider Radius
Given a class with enforcement level = Lenient and thresholds configured (geofence radius R=250m, GPS accuracy A<=150m, GPS age T<=60s), When an attendee scans the session QR, Then the check-in is accepted if the device is within R with accuracy<=A and age<=T, And Wi‑Fi/BLE are not required, And if location permission is denied, the user is immediately offered code fallback to complete check-in.
Consent and Coarse Location Privacy
Given a first-time scanner for a class requiring geofence, When the app requests location access, Then the prompt explicitly states the purpose (on-site verification only) and requests coarse location only, And upon grant, only coarse location (rounded to ~100m precision, e.g., 3-decimal lat/lon) is processed and stored, And no background location is collected or stored, And if access is denied, the app does not repeatedly prompt within the same session and routes to code fallback, And audit logs store only coarse location and are retained for 30 days maximum.
Code-Entry Fallback when Location Permission is Denied
Given a user denies location permission for a class requiring proximity enforcement, When the user selects code fallback, Then the system accepts a valid 6-digit one-time code minted from the staff console within the last 2 minutes and tied to the class session, And the code is single-use and rate-limited to 5 attempts per minute per user/device, And the fallback path is disabled if location permission is granted and the user is out-of-zone (to prevent remote check-ins).
Out-of-Zone Rejection, Messaging, and Audit Logging
Given a scan occurs with no passing proximity signals for the configured enforcement level, When the attendee scans the session QR, Then the check-in is rejected with reason code PROXIMITY_NOT_MET and a user message "You're not at the class location" plus a "Having trouble?" help link, And an audit record is written containing timestamp, userId, classId, enforcement level, decision, reason code, coarse lat/lon, distance to geofence center if available, GPS accuracy and age, Wi‑Fi match boolean, BLE match boolean, And staff can view these attempts in the class attendance log filtered by outcome = Rejected (Proximity).
Reuse Detection & User Messaging
"As an attendee, I want clear, helpful messages when a QR doesn’t work so that I know exactly how to proceed without frustration."
Description

Detect screenshot/reuse attempts by identifying expired tokens, wrong-session tokens, or scans outside the allowed window and route users to friendly, brandable prompts with next steps. Provide tailored flows: suggest correct class page, show countdown until check-in opens, offer waitlist/standby, or direct to front desk for help. Localize copy, meet accessibility standards, and ensure messages render in under 200 ms. Track reason codes and conversion outcomes to inform tuning of windows and policies. Expected outcome: honest guests get clear guidance while bad-faith reuse is discouraged without creating friction.

Acceptance Criteria
Early Scan Shows Countdown
Given a valid class QR token is scanned before the configured check-in window opens for its bound session When the scan is processed by the Dynamic QR Shield Then no check-in record is created and the scan is blocked And a countdown prompt displays the remaining time until check-in opens in mm:ss and updates every second And the primary action to check in remains disabled until the window opens, with aria-disabled=true and appropriate tooltip text And if the class is at capacity, a Waitlist/Standby action is shown; otherwise it is hidden And the event is logged with reason_code=EARLY_WINDOW
Expired Token Help Prompt
Given a QR token is scanned after its validity window has closed or its signature has expired When the scan is processed Then the attendee is not checked in and a friendly help prompt explains the token is expired with the original class date/time And, if the referenced session is still bookable on the schedule, a primary action links to that session’s booking/check-in page; otherwise a secondary action offers directions to the front desk/help And the event is logged with reason_code=EXPIRED_TOKEN
Wrong Session or Location Suggestion
Given a scan contains a valid token bound to a different session or location than the current context When the scan is processed Then check-in to the current session is blocked And the prompt identifies the correct class (title, start time, location) and provides a one-tap redirect to that class page And, if allowed for the current session, a secondary action offers Join Waitlist/Standby here And the event is logged with reason_code set to WRONG_SESSION or WRONG_LOCATION accordingly
Replay/Reuse Attempt Handling & Staff Alert
Given a token that has already been used or invalidated by rotation is scanned again When the reuse scan is processed Then check-in is blocked and a friendly reuse prompt is shown to the attendee with next steps And attempt_count for this device/account is incremented and logged with reason_code=REPLAY_DETECTED And if attempt_count >= 3 within 2 minutes for the same session, a staff alert is sent to the host dashboard/push within 5 seconds including session_id, location_id, masked attendee identifier (if known), and timestamp
Localized, Brandable, Accessible Prompts
Given a tenant with configured brand theme (logo, colors) and supported locales When any user prompt (early, expired, wrong session/location, replay) is displayed Then all visible copy is sourced from localization keys, renders in the user’s preferred locale, and falls back to the tenant default without missing strings And the prompt applies the tenant logo and primary/secondary colors per theme tokens And the prompt meets WCAG 2.2 AA: keyboard navigable, focus visible, labels announced by screen readers, aria-live=polite for status updates, contrast ratio >= 4.5:1, and supports 200% text scaling without clipping or loss of functionality
Sub-200 ms Message Render
Given a scan that requires showing a user prompt instead of completing check-in When the scan event is received on a mid-tier device under a 4G network profile Then the initial prompt container becomes visible within 200 ms of the scan event at the 95th percentile over the last 7 days And remote resources (copy, theme) are preloaded or cached so network fetches do not block the first paint And a metric user_message_ttfp_ms is recorded for each event
Telemetry: Reason Codes & Conversion Outcomes
Given any blocked scan results in a user prompt When the prompt is shown and when the user takes an action Then a structured analytics event is emitted with tenant_id, session_id, location_id, user_id (if known), reason_code in [EARLY_WINDOW, EXPIRED_TOKEN, WRONG_SESSION, WRONG_LOCATION, REPLAY_DETECTED], attempt_count, locale, device_type, and action in [redirect_to_correct_class, joined_waitlist, contacted_front_desk, dismissed, eventual_checkin] And eventual_checkin is attributed if a successful check-in occurs for the same user/session within 24 hours And aggregated counts and conversion rates by reason_code are available via API or dashboard for a selectable date range
Staff Alerts & Override Console
"As front-desk staff, I want to be notified of suspicious scans and have a quick override option so that I can keep lines moving and handle exceptions confidently."
Description

Provide real-time alerts to staff when suspicious scan patterns occur (e.g., repeated invalid tokens, off-site scans, burst traffic) with context like class, time, and anonymized device indicators. Build a lightweight console in the staff app/web to approve one-tap overrides with reason capture and optional ID note, respecting role-based permissions and auditability. Support configurable alert thresholds, quiet hours, and delivery channels (push, SMS). Rate-limit alerts to prevent noise and ensure they reflect actionable issues. Expected outcome: staff can quickly resolve edge cases and maintain flow while the system deters abuse.

Acceptance Criteria
Real-Time Suspicious Scan Alert Generation
Given a class session is active and Dynamic QR check-ins are being processed And suspicion thresholds are configured When the system detects any of: - repeated invalid tokens from the same device fingerprint or IP range exceeding the configured threshold within the configured window - an off-site scan beyond the configured geofence radius for the class location - burst scans exceeding the configured count within the configured burst window Then an alert is created within 3 seconds and includes: alert_id, org_id, class_id, session_id, location_id, event_type, count, first_seen_ts, last_seen_ts, severity, anonymized_device_indicators, and detector_version And the alert is routed to the delivery pipeline for channel handling
Alert Rate-Limiting and De-Duplication
Given an alert with the same org_id, class_id, session_id, and event_type exists within a 120-second suppression window When additional matching events occur during that window Then do not emit more than one notification per recipient per window And increment the alert's aggregated count and update last_seen_ts And present a single aggregated alert card in the console And after the suppression window, emit a new alert if events continue
Configurable Suspicion Thresholds Per Org/Class
Given an org admin edits alert thresholds at org or class level When values are saved Then validation enforces ranges: burst_count 2-500, burst_window_seconds 5-600, invalid_token_repeat_count 2-50, geofence_radius_m 10-1000 And settings are persisted with versioning and audit log And per-class overrides take precedence over org defaults And detectors apply new settings within 60 seconds of save And default values are applied when a setting is unset
Quiet Hours and Channel Routing with Fallback
Given staff members have quiet hours schedules and channel preferences (push, SMS) configured When an alert is generated during a member's quiet hours Then push notifications to that member are suppressed And SMS is sent only if severity >= High and the member is marked on-call And otherwise the alert is queued for delivery at quiet-hours end When an alert is generated outside quiet hours Then send push immediately And if the alert is unacknowledged for 30 seconds and severity >= Medium, send SMS fallback And all delivery attempts are logged with outcome (delivered, failed, suppressed, queued)
Role-Based Access to Alerts & Overrides
Given roles and scopes are defined as: Owner/Manager (full org), Instructor (assigned classes), Staff (assigned locations), Viewer (read-only) When a user opens the Alerts & Overrides console Then the user only sees alerts within their scoped classes/locations And only Owner/Manager can approve overrides across the org; Instructor can approve only for assigned classes if granted the 'override' permission; Viewer cannot approve And unauthorized attempts return HTTP 403 with error code PERMISSION_DENIED and are audit-logged And permission changes take effect within 30 seconds across web and mobile
One-Tap Override with Reason and Optional ID Note
Given an authorized user selects an alert or attendee in the console When the user taps Approve Override Then the UI requires selection of a reason code from a configurable list And allows an optional ID note (text and/or photo attachment) And upon submission, the system creates an override record with: override_id, approver_user_id, class_session_id, timestamp, reason_code, note, attachment_ref (optional), validity_window_s=600 And the attendee's check-in is marked valid for the current session only and expires when the validity window elapses And the scanning device receives approval status within 2 seconds And the override can be revoked by authorized roles until it is consumed or expired, with all changes audit-logged
Audit Trail and Reporting
Given alerts are generated and overrides are actioned When these events occur Then an immutable, tamper-evident audit record is written including: event_type, actor_id (or system), org_id, class_id, session_id, timestamps, delivery outcomes, anonymized_device_indicators, before_state, after_state, reason_code (if applicable) And org admins can filter audit records by date range, event_type, class/session, actor, and severity in the console And exports to CSV for up to 50,000 records over a selected date range complete within 30 seconds And audit data access is permission-checked and respects data minimization And retention is configurable per org with a 365-day default
Offline & Fallback Check-in
"As an instructor, I want a reliable fallback when the internet drops so that attendees can still check in and we can start class on time."
Description

Enable check-in continuity during connectivity issues with a rolling short code or QR derived from a time-based seed that can be queued and verified when service resumes. Provide an on-device cache of near-term token slices and a 4–6 digit rotating staff PIN for manual entry with limited validity to reduce abuse. Queue check-ins locally with conflict resolution and automatic sync, showing clear statuses (queued, synced, failed). Document operational procedures for instructors and front desk. Expected outcome: classes can start on time even when networks are flaky, without opening avenues for ghost check-ins.

Acceptance Criteria
Offline rolling QR/short code check-in
Given the class session is within its configured check-in window and the staff device is offline When the app displays a rotating QR/short code derived from the time-based seed for that session and location Then the code slice rotates at the configured interval and is bound to the session, location, and time window And a scan or entry validates locally against the current or grace slices and queues a check-in with status "queued" in under 1 second And the same token slice cannot be reused for the same attendee within the window And the check-in is timestamped locally and marked for later server verification
On-device cache of near-term token slices
Given the instructor opens the class in the app while online prior to check-in When the app prepares for potential offline mode Then it preloads at least 20 future token slices and 2 prior slices for drift tolerance for the specific session and location And it stores the cache encrypted using the platform keystore And it purges the cache at class end or 24 hours after class start, whichever comes first And if preload fails, the app surfaces an inline warning and offers a retry
Rotating staff PIN for manual fallback
Given the staff device is offline and a participant cannot scan When a staff member selects Manual Fallback and enters the rotating staff PIN Then the app accepts only a 4–6 digit PIN per organization settings (default 6) And validates the PIN against the current 60-second slice and scope-limits it to the active session and location And after 3 consecutive invalid PIN attempts, Manual Fallback is temporarily locked for 2 minutes and an alert is shown And successful entries queue a check-in with status "queued" and an audit entry including staff user, device, and local timestamp
Local queueing, auto-sync, and status display
Given one or more check-ins are queued locally while offline When network connectivity resumes (Wi‑Fi or cellular) and the app is foregrounded Then the app begins sync within 10 seconds and retries with exponential backoff on failure And each record transitions from "queued" to "synced" or "failed", with a visible reason code on failure And the class screen displays real-time counts for queued, synced, and failed And staff can manually retry failed items individually or in bulk
Conflict detection and resolution on sync
Given queued check-ins include potential duplicates or occur after capacity was reached while offline When the app syncs queued records with the server Then duplicate check-ins for the same attendee and session are rejected with status "failed – duplicate" and do not increment attendance And over-capacity check-ins are rejected with status "failed – capacity" and do not overbook the class And any reused or out-of-window tokens trigger a friendly on-device prompt at next scan and generate a staff alert in the app And staff can apply an override with a required note, which is audited
Security safeguards and misuse limits offline
Given offline tokens and PINs are used during an outage When the app generates/validates token slices and records check-ins locally Then token slices are verified offline using a cached public key signature and are cryptographically bound to session, location, and time window And device clock drift up to ±2 minutes is tolerated via grace slices; beyond that, a valid staff PIN is required to proceed And manual fallback entries are rate-limited to 1 acceptance per 5 seconds and a warning surfaces if more than 10 are attempted in a minute And locally stored queues and token caches are encrypted at rest and contain no full payment data or sensitive PII
Operational procedures accessible offline
Given instructors or front desk staff need guidance during an outage When they tap Help from the class screen while offline Then an offline-ready guide titled "Offline & Fallback Check-in" opens within 2 taps and under 2 seconds And it includes step-by-step instructions, visual indicator legends (queued/synced/failed), and conflict resolution steps And the guide shows a last-updated date and version number And when back online, staff can export a one-page PDF SOP from the same screen
Admin Controls, Branding & Reporting
"As an owner, I want to configure the QR rules and review results so that I can optimize security without hurting the guest experience."
Description

Add dashboard controls to enable Dynamic QR Shield per class/location, configure rotation interval, validity window, geofence radius, and allowed proximity methods. Provide brand customization of QR frame, error/help messages, and supported languages, with a preview mode. Include reports on scan volumes, acceptance rates, invalid/reuse reasons, and impact on no-shows, with CSV export and retention controls. Support A/B testing of rotation intervals and windows to balance speed and security. Expected outcome: operators can tune the shield to their context, maintain brand consistency, and measure outcomes.

Acceptance Criteria
Enable Dynamic QR Shield per Class and Location
Given I am an admin on ClassTap with access to Scheduling settings When I open a specific Class instance or Location settings Then I can toggle Dynamic QR Shield on or off for that scope And the saved state is persisted and reflected in the API and UI within 2 seconds of save And a class-level setting overrides the location default And changing the location default updates only classes without an explicit class-level setting
Configure Rotation Interval, Validity Window, Geofence, and Proximity Methods
Given Dynamic QR Shield is enabled for the selected scope When I set a rotation interval between 3 and 30 seconds Then values outside this range are rejected with an inline validation message and save is disabled When I set a scan validity window between 5 and 120 seconds Then values outside this range are rejected with an inline validation message and save is disabled When I set a geofence radius between 0 and 150 meters (0 disables geofence) Then the selected radius is persisted and the effective geofence is shown on a map preview When I choose allowed proximity methods (GPS and Bluetooth) Then only the selected methods are enforced during scans and reflected in the API configuration
Brand Customization and Localization of QR Frame and Messages
Given I open the QR Shield Branding settings When I upload a frame (SVG or PNG up to 1 MB) and choose a primary color Then the preview updates to show the frame and color exactly as saved When I edit the attendee error and help messages with placeholders {class_name} and {support_contact} Then the system validates max length 240 characters and preserves placeholders When I select supported languages and provide translations for each message Then the scan UI displays the message in the device locale if available, else falls back to the default language
Preview Mode Reflects Configuration Without Live Check-in
Given I have configured branding and timing settings When I click Preview Then a rotating QR preview is shown using the configured rotation interval and frame And scanning the preview shows a “Preview Only” message and does not register a check-in And any unsaved changes are not reflected until I click Save; saved changes appear in the preview within 1 second
Reports for Scan Volumes, Acceptance Rates, and Invalid/Reuse Reasons
Given I open Reports and select a date range, class, and/or location filters When I run the report Then I see total scans, accepted scans, acceptance rate, and a breakdown of invalid scans by reason (early scan, outside geofence, expired QR, reuse attempt) And the report can be exported to CSV containing one row per scan attempt with timestamp (ISO 8601), class_id, location_id, outcome, and reason And the CSV export respects applied filters and completes within 60 seconds for up to 100,000 rows And an admin can set a retention period for scan logs between 30 and 365 days; events older than the selected period are permanently deleted and excluded from reports and exports
Measure Impact on No-Shows
Given I open Reports and enable the No-show Impact view When I compare a period before and after enabling Dynamic QR Shield or between classes with and without the shield Then the report displays no-show rate per cohort and the change in no-show rate in percentage points And the view supports the same filters as other reports and can be exported to CSV
A/B Testing of Rotation Intervals and Validity Windows
Given I create an experiment with two variants that differ in rotation interval and/or validity window When I set the scope (classes or locations) and allocation (e.g., 50/50) Then attendees in the scope are assigned a variant deterministically per class session And the experiment dashboard shows per-variant metrics: acceptance rate, average time to successful check-in, and no-show rate And I can stop the experiment and apply the chosen variant to the defined scope; subsequent sessions use the selected settings

Doorline Waitlist

A separate door poster QR lets walk-ins join the waitlist instantly with a lightweight profile and payment hold. When a no-show is confirmed, the next in line gets a pay-to-claim text and is auto-added on payment—filling last-minute seats without front-desk juggling.

Requirements

QR Doorline Waitlist Enrollment
"As a walk-in attendee, I want to scan a QR at the door to join the waitlist in seconds so that I can claim a seat if one opens without needing front-desk assistance."
Description

Generate per-class, white-labeled dynamic QR codes for door posters that deep-link to a mobile-optimized enrollment screen. The flow captures minimal required data (name, mobile number, email optional, consent/waiver) and obtains explicit consent for transactional SMS. It displays live class status, estimated wait time/queue position, and studio branding. The enrollment prevents double-booking by detecting overlapping bookings and existing roster status, deduplicates by phone/email, and supports captcha/rate limiting. On submit, the user is added to the waitlist with a lightweight profile and associated payment method placeholder, and receives a confirmation text. Integrates with ClassTap scheduling, user accounts, and branding theming, with localized copy, timezone handling, and accessibility compliance for on-site use.

Acceptance Criteria
Scan QR to Open Branded Class Enrollment
Given a published class with a generated door-poster QR code When a user scans the QR within the QR token validity window Then the link deep-links to the class-specific mobile enrollment page under the studio’s domain/subdomain And the page renders with the studio’s logo, colors, and typography and no ClassTap consumer branding And the deep link contains no PII and uses HTTPS with a signed, time-bound token valid until class end + 30 minutes And scanning an expired/invalid token shows an error page with a short URL for manual entry and a link to view other classes And repeated scans of the same valid token on multiple devices load the same enrollment screen without creating duplicate entries
Enter Minimal Details, Waiver, and SMS Consent
Given the class enrollment page is open When the user provides name (required), mobile number in E.164 format (required), and email (optional) And the user checks the required waiver/terms acknowledgment And the user explicitly checks the required SMS consent checkbox with visible link to SMS terms & fees Then the Submit button becomes enabled And on submit, invalid or missing required inputs are flagged inline with specific messages without full page reload And the system records waiver and SMS consent artifacts (timestamp, IP, user agent, consent text version) with the profile
Prevent Double-Booking and Duplicate Waitlist Entries
Given a user enters a phone and/or email that matches an existing user When they attempt to join the waitlist for the class Then the system merges to the existing profile using phone as primary dedupe and email as secondary And if the user is already enrolled or already on the waitlist for the class, the submission is blocked with a clear, localized message And if the user has an overlapping active booking within the class time window (including studio-defined buffer), the submission is blocked with guidance to manage bookings And the system prevents creation of multiple waitlist entries for the same user/phone/email for this class
Apply CAPTCHA and Rate Limiting on Abuse
Given multiple submissions originate from the same IP/device or phone number When thresholds are exceeded (e.g., >3 attempts in 5 minutes) or bot signals are detected Then a human verification challenge (CAPTCHA) is required and must be passed to proceed And hard rate limits are enforced (e.g., 10 submissions per hour per phone; 60 per hour per IP) with HTTP 429 and a friendly, localized message And successful, legitimate completion clears the CAPTCHA requirement after a cooldown window And all challenges, passes, failures, and 429 events are logged with non-PII identifiers for audit
Create Waitlist Entry with Payment Placeholder and Confirmation SMS
Given a studio requires a payment method placeholder for doorline waitlist When the user provides valid card details via a PCI-compliant embedded form Then a payment method token is created with the payment processor and attached to the lightweight profile without charging And if tokenization fails, the user is shown an actionable error and can retry without losing prior inputs And upon successful submission, a waitlist entry is created in ClassTap scheduling with an assigned queue position And a confirmation SMS is sent within 10 seconds containing studio name, class name/date/time (studio timezone), current queue position, and a manage link And if the studio does not require a payment placeholder, the flow skips card collection and still creates the waitlist entry and sends the confirmation SMS
Show Live Class Status, Queue Position, and ETA with Localization
Given the enrollment page is loaded Then it displays the current class status (e.g., Full, In Progress, Cancelled) sourced from scheduling within 2 seconds And upon successful join, it shows the user’s queue position and an estimated wait time And class times and ETA are displayed in the studio’s local timezone with explicit TZ label And content is localized to the device language with fallback to the studio default And status, queue position, and ETA auto-refresh at least every 15 seconds without a full page reload And if the class is cancelled or the waitlist cap is reached, submission is disabled and an explanatory message is shown
Apply Studio Branding and Meet Accessibility/Performance Targets
Given the enrollment experience is rendered on a mobile device Then the page applies the studio’s theme (logo, colors, typography) and supports high-contrast mode, without exposing ClassTap consumer branding And the UI meets WCAG 2.2 AA: keyboard operability, visible focus, ARIA labels, form field associations, and minimum 4.5:1 color contrast And interactive targets are at least 44x44 CSS pixels and support zoom up to 200% without layout breakage And Largest Contentful Paint is ≤2.5s on a 3G Fast profile; Time to Interactive ≤5s And the printed poster includes a readable short URL as fallback alongside the QR code And the page functions on the latest two major versions of iOS and Android (Safari/Chrome)
Tokenized Payment Hold (Pre-Authorization)
"As a studio, I want a payment hold taken when a walk-in joins the waitlist so that only serious prospects remain and payment is frictionless when a seat opens."
Description

Collect a payment method during QR enrollment using PCI-compliant tokenization with support for major cards and Apple/Google Pay. Place a configurable pre-authorization hold (class price or deposit) with currency-aware amounts and SCA support where applicable. Holds are time-bound and automatically released on expiration, cancellation, or when the user is skipped. On successful claim, convert the hold to a capture/charge; on failure, retry per gateway rules and gracefully notify the user. Store non-PCI tokens only, log authorization IDs for reconciliation, and expose hold/charge state in the admin console. Handle edge cases (insufficient funds, expired cards, SCA challenges) and provide clear user messaging. Integrates with existing ClassTap payment provider abstraction and tax/fee configuration.

Acceptance Criteria
QR Enrollment Tokenized Hold Creation
Given a walk-in scans the door poster QR and completes required profile fields When they add a payment method via major card, Apple Pay, or Google Pay Then a PCI-compliant token is created and stored (non-PCI token only; no PAN/CVV/expiry stored) and linked to the waitlist entry And when the pre-authorization is initiated, it is requested through the payment provider abstraction and an authorization_id is returned and persisted And the user sees a confirmation of hold initiation with amount and currency And if the hold attempt is declined, the user sees a clear, actionable error and can try another method without losing their current claim timer
Currency-Aware Configurable Hold Amount
Given the class has a configured hold basis (full price or deposit) and tax/fee rules in a specific currency When the pre-authorization is created Then the hold amount equals the configured basis plus applicable taxes/fees And the calculation uses correct currency exponent and rounding rules, with the stored minor units matching the gateway authorization within 1 minor unit And the user-facing amount displays with the correct currency symbol and locale formatting
Strong Customer Authentication and Wallet Flows
Given the merchant region or card requires SCA When the user initiates the pre-authorization Then a 3DS/SCA challenge is performed and must complete within 5 minutes And on success the hold is authorized; on failure or timeout the hold is not placed and the user is clearly informed with next steps And when using Apple Pay or Google Pay, SCA is satisfied within the wallet flow where supported; fallback to 3DS is used for standard cards
Time-Bound and Event-Driven Hold Release
Given a pre-authorization hold exists When the configured hold TTL elapses, the user cancels enrollment, or the user is skipped in the waitlist Then the system voids/releases the authorization via the gateway and marks the hold status as Released, recording release timestamp and reference And no capture is attempted after release, and the user is notified (where applicable) that no charge was made And hold release operations are idempotent such that repeated releases do not alter the final state or create errors
Claim Conversion to Capture and Auto-Enrollment
Given the user receives a pay-to-claim notification and opens the link within the claim window When they confirm to claim the spot Then the system captures the existing authorization for the finalized amount, supporting partial/adjusted capture per gateway rules And on capture success the booking is confirmed, the user is automatically added to the class roster, and a receipt is sent And capture requests are idempotent using a unique idempotency key to prevent double charging
Capture Failure Handling, Retries, and User Messaging
Given a capture attempt fails due to insufficient funds, expired card, SCA re-auth required, or transient gateway error When the gateway’s retry policy allows retries Then the system retries according to provider rules with exponential backoff up to the configured limit And if retries ultimately fail or the claim window expires, the user is notified with a clear message and a link to update payment; their claim is canceled, the hold is released, and the seat is offered to the next waitlisted user And all failure codes and retry attempts are logged with timestamps for audit
Admin Console Visibility and Reconciliation
Given an admin views the class or waitlist entry in the admin console When inspecting the payment details Then the admin can see the payment lifecycle state (Authorized, Captured, Released, Failed), amounts, currency, and timestamps And authorization_id, capture_id/charge_id, and last_error_code/message are visible and exportable for reconciliation And admins can filter by state and date, and states update in near real-time (under 30 seconds) after gateway webhooks are processed
No-Show Detection and Seat Release Rules
"As an instructor, I want the system to automatically detect no-shows after a cutoff and free seats to the waitlist so that last-minute spots can be filled without manual juggling."
Description

Provide configurable no-show logic that frees seats to the doorline waitlist. Support multiple triggers: instructor marks attendance in-app, automated check-in window cutoff, and late grace period rules. When a registered attendee is not checked in by the cutoff, mark as no-show and release their seat(s) to the waitlist while respecting membership policies and cancellation terms. Prevent race conditions by locking capacity updates and honoring holds/refunds for the original attendee per policy. Surface real-time capacity changes to the waitlist queue and analytics. Includes safeguards to reverse a release if a late arrival is manually marked present within a short rollback window.

Acceptance Criteria
Instructor Manual No‑Show Release
Given a class has at least one eligible user on the doorline waitlist and attendee A with booking B is not checked in When the instructor marks A as No‑Show in the staff app Then A’s seat(s) are released to availability immediately And the first eligible waitlister receives a pay‑to‑claim SMS with a unique link and an H‑minute hold And the offer price, fees, and taxes match the class pricing and the attendee’s policy eligibility And A’s booking reflects the configured policy outcome (e.g., credit deducted or fee charged; no refund if beyond cancellation window) And the staff roster, consumer booking page, and doorline queue reflect the new availability within 5 seconds And analytics logs seat_released with class_id, booking_id, attendee_id, trigger=manual_no_show within 10 seconds
Automated Check‑In Cutoff No‑Show Release
Given a class has a check‑in window end time T and a grace period G minutes configured And attendee A with booking B has not checked in by T + G When the system evaluates attendance at T + G Then A is marked No‑Show and their seat(s) are released to availability And the next eligible waitlister receives a pay‑to‑claim SMS with an H‑minute hold And if no eligible waitlister exists, the seat becomes publicly bookable for doorline/walk‑ins And no duplicate releases or notifications occur if A was already marked No‑Show or checked in And analytics logs seat_released with trigger=auto_cutoff within 10 seconds And all user‑facing capacity indicators update within 5 seconds
Late Arrival Rollback Window
Given attendee A’s seat(s) were released at time t0 due to a No‑Show And a rollback window R minutes is configured When staff marks A as Present at time t such that t ≤ t0 + R And no waitlist offer for those seat(s) has been paid Then the system cancels any active offers, restores A’s seat(s), reverses fees/credit deductions per policy, and marks A as Checked‑In And all affected UIs reflect the restoration within 5 seconds And analytics logs seat_reclaim_rollback linked to the original seat_released event within 10 seconds When a waitlist offer has already been paid for any of the released seat(s) Then rollback is rejected with a clear error to staff and no changes are made to the claimant’s booking or payment
Atomic Capacity Locking and Race‑Condition Safety
Given manual no‑show marking, automated cutoff, and doorline claim requests may occur concurrently When release and claim operations execute for the same seat(s) Then the system performs atomic availability updates so available seats never become negative or exceed class capacity And at most one claim succeeds per released seat; all other conflicting operations are rejected with no payment captured And repeated identical requests do not create duplicate releases or claims (idempotent behavior) And audit logs show a single committed state transition per seat with timestamps and actor/trigger
Policy and Membership Compliance on No‑Show
Given cancellation and membership policies P define explicit no‑show outcomes by booking type (membership/unlimited, class pack, drop‑in) When a no‑show is recorded by any trigger Then the system applies the policy‑defined outcome for that booking type (e.g., strike increment, fee F charged, credit deduction, or waiver if eligible) And any refunds/fee charges/credit adjustments are posted to the ledger within 5 minutes and reflected on the booking And the attendee receives an email/SMS receipt/notification detailing the adjustment within 2 minutes And the policy applied is visible to staff on the booking timeline with reason and trigger
Multi‑Seat (Party) Booking No‑Show Handling
Given booking B reserves N seats and by cutoff only M of those seats are checked in where 0 ≤ M < N When no‑show processing runs Then N − M seats are released to availability And waitlist offers allow claimants to select up to K seats where 1 ≤ K ≤ N − M And if partial claims occur, remaining seats continue down the queue until all N − M seats are claimed or the queue is exhausted And roster views show M checked‑in seats under booking B and remove unclaimed party seats from capacity counts
Real‑Time Waitlist and Analytics Updates
Given a doorline waitlist Q is active for class C When any seat is released, offered, expired, claimed, or reclaimed via rollback Then the doorline queue and consumer booking page update position numbers and available seats within 2 seconds And the staff roster updates within 5 seconds And analytics emits events seat_released, seat_offer_sent, seat_offer_expired, seat_claimed, seat_reclaim_rollback with class_id, booking_id, attendee_id, trigger, seat_count, and timestamps And the dashboard reflects updated fill‑rate and no‑show KPIs within 60 seconds
Pay-to-Claim Notifications with Countdown
"As a waitlisted attendee, I want a pay-to-claim message with a short timer so that I can quickly secure an open seat before others."
Description

When a seat becomes available, notify the next eligible person in the queue via SMS (and email fallback) with a secure, single-tap claim link. Enforce a configurable claim window (e.g., 5–10 minutes) with visible countdown and auto-expiry. If the window lapses or payment fails, automatically advance to the next person, with batching support when multiple seats free up. Messages use studio branding and templates, respect communication consent, and include opt-out keywords. Implement delivery status tracking, throttling, and retries. The claim link opens a streamlined checkout that confirms price, taxes/fees, and applies the stored payment hold for instant capture. All transitions are atomic to avoid double assignment.

Acceptance Criteria
SMS First, Email Fallback With Consent and Branding
Given a user is next eligible and has SMS consent true, When a seat becomes available, Then send an SMS using the studio’s active branding template within 10 seconds that includes a single-tap claim link and opt-out instructions. Given the SMS provider returns a delivery failure or the user lacks SMS consent but has email consent, When the notification is triggered, Then send a branded email with the same claim link within 30 seconds. Given any inbound STOP/UNSUBSCRIBE reply to the SMS or email, When received, Then update the user’s communication preferences immediately and suppress further messages. Given rate limits configured per studio, When sending notifications, Then throttle to no more than 60 messages per minute per studio and queue excess with FIFO order. Given a notification is sent, When delivery receipts/webhooks arrive, Then persist message status (queued, sent, delivered, failed) with timestamps and provider IDs. Given a transient send failure occurs, When eligible for retry, Then retry up to 2 times with exponential backoff before marking the notification as failed.
Configurable Claim Window With Visible Countdown and Auto-Expiry
Given a studio-configured claim window between 5 and 10 minutes, When the claim link page loads, Then display a visible countdown timer matching the configured duration. Given the countdown reaches zero, When the user attempts to claim, Then block the claim, mark the link expired, and show an “Offer expired” message. Given an unexpired link, When the user taps the claim link, Then validate the token is unexpired and single-use before proceeding to checkout. Given a claim window lapses, When expired, Then automatically advance the queue and send the next notification within 10 seconds.
Single-Tap Claim Checkout With Payment Hold Capture
Given a valid claim link and an existing payment hold on file, When the user taps “Claim seat”, Then capture the hold for the full amount (class price + taxes/fees) and confirm the booking without entering payment details. Given a valid claim link and a stale or missing hold, When the user taps “Claim seat”, Then prompt for payment, authorize, and capture instantly upon confirmation without losing seat priority during the window. Given the checkout displays pricing, When rendered, Then show itemized price, taxes/fees, and total before capture. Given a successful capture, When confirmed, Then send a booking confirmation and mark the waitlist entry as converted; Given a failed capture, Then do not assign the seat and proceed to auto-advance logic.
Auto-Advance On Expiry Or Payment Failure
Given a notified user does not complete within the claim window, When the window expires, Then automatically notify the next eligible user and disable the previous link. Given a payment authorization or capture fails, When failure occurs, Then immediately revoke the seat offer and notify the next eligible user within 10 seconds. Given an auto-advance occurs, When processed, Then record an audit event with cause (expired, payment_failed), timestamps, and affected user IDs.
Batch Notifications When Multiple Seats Free Up
Given N seats become available simultaneously, When notifying the queue, Then send claim notifications to the top N eligible users concurrently, each with their own claim window. Given more users attempt to claim than seats available, When claims arrive, Then assign seats to the first N successful payers by receipt timestamp and present a “Sold out” message to others without charging them. Given a user receives multiple seat offers for the same class, When claiming, Then restrict the user to claiming at most one seat. Given seats remain after the first batch’s windows expire, When re-batching, Then notify the next eligible users until all seats are either filled or the queue is exhausted.
Atomic Assignment Under Concurrency
Given two or more users follow claim links for the same seat at nearly the same time, When they attempt to complete checkout, Then ensure only one atomic assignment succeeds and all other attempts are declined with a clear reason. Given a claim link is used, When processed, Then enforce idempotency so repeated taps or network retries do not create duplicate bookings or charges. Given concurrent notifications and claims, When persisting state, Then lock seat inventory during assignment and commit payment capture and booking in a single transaction or compensating transactions that prevent double-assignment.
Secure Single-Use Claim Link and Token Expiry
Given a generated claim link, When created, Then bind the token to the waitlist entry, class instance, and user, and set expiry equal to the claim window. Given a claim token is redeemed once, When another attempt uses the same token, Then reject with “Link already used” and do not process payment. Given a claim link is shared or opened on another device, When user identity cannot be verified, Then require lightweight verification (e.g., last 4 of phone or email OTP) before allowing claim within the window.
Auto-Enrollment and Confirmation
"As a scheduler, I want successful payment to instantly enroll the attendee and send confirmations so that records stay accurate and the attendee is ready to attend."
Description

Upon successful claim and payment capture, automatically add the attendee to the class roster, decrement capacity, and remove them from other conflicting waitlists. Send immediate confirmation via SMS/email with class details, location, and check-in instructions; update calendar invites and reminder schedules. Reflect the change in the instructor dashboard and attendee profile in real time. Release or void any unused holds in the queue and log all state changes for audit. Ensure idempotency so repeated link clicks or double-submits do not create duplicates. Integrates with existing ClassTap roster management, reminders, and attendance systems.

Acceptance Criteria
Claimed Seat Auto-Enrollment and Capacity Update
Given an attendee on the Doorline waitlist receives a valid pay-to-claim link for Class X with at least 1 seat available And payment authorization is successful and funds are captured When the attendee completes the claim and payment Then the attendee is added to the Class X roster exactly once And Class X remaining capacity is decremented by 1 atomically And capacity never goes below 0 And the roster integration marks the attendee as Enrolled for that occurrence And the enrollment completes within 5 seconds of payment capture
Removal from Conflicting Waitlists
Given the attendee is enrolled into Class X from the Doorline waitlist And the attendee is on waitlists for other classes that overlap in time with Class X by any amount When enrollment is confirmed Then the attendee is removed from all time-conflicting waitlists within 10 seconds And queue positions are compacted for each affected waitlist And the attendee remains on non-conflicting waitlists unchanged And an audit log entry is recorded for each removal
Immediate Confirmation and Scheduling Updates
Given enrollment into Class X is confirmed When confirmation is triggered Then an SMS and an email are sent within 10 seconds to the attendee’s verified contacts And both messages include class title, start and end time with timezone, location address/map link, and check-in instructions And a calendar invite (ICS) is created or updated without duplication with the confirmed details And reminder jobs are scheduled or updated at T-24h, T-2h, and T-30m (or the next upcoming window if start is sooner) And duplicate confirmations are not sent more than once for the same enrollment
Idempotent Claim and Payment Capture
Given a pay-to-claim link for the attendee and Class X When the attendee clicks the link multiple times or submits the payment form more than once within 5 minutes Then only a single successful charge is captured And only one roster record exists for the attendee in Class X And subsequent attempts return an idempotent success response indicating the seat is already claimed And no additional confirmation emails/SMS are sent beyond the initial And the behavior is consistent across web and mobile surfaces
Release/Void of Unused Holds in Queue
Given multiple attendees have active payment holds for Class X via Doorline When one attendee successfully enrolls and capacity reaches 0 or their claim window expires Then all remaining unused holds for Class X that are not converting to enrollments are released or voided within 2 minutes And no released holds result in captured charges And affected attendees are notified of hold release if they were presented a pay-to-claim prompt And an audit entry records each hold release with payment reference
Audit Log Completeness and Traceability
Given any state change related to Doorline claim, enrollment, capacity, notifications, calendar updates, reminders, or holds When the change is committed Then an immutable audit log entry is written with event_id, ISO8601 UTC timestamp, actor/user_id or system, class_id, previous_state, new_state, and payment_intent_id when applicable And entries are queryable by class_id and user_id via admin API within 2 seconds And audit logs are retained for at least 24 months
Real-Time Dashboard and Profile Reflection
Given an attendee is enrolled into Class X from Doorline When the enrollment transaction commits Then the instructor dashboard roster list and capacity counters update within 2 seconds without manual refresh And the attendee profile shows Class X under Upcoming with correct date/time and location within 2 seconds And the attendee appears on the attendance/check-in list for Class X immediately And the change is consistent across web and mobile apps
Admin Controls and Queue Management
"As a studio owner, I want configurable rules and live queue controls so that the doorline waitlist aligns with my policies and on-the-ground operations."
Description

Provide studio-level and class-level settings to configure the doorline waitlist: enable/disable per class, max queue size, claim window duration, pre-auth amount, fees, tax handling, no-show cutoff/grace rules, and notification templates. Include a live queue view with actions to pause/resume, reorder, skip, manually invite, or block abusive users. Allow overrides to comp a seat or bypass payment hold for special cases. Expose status of each waitlister (joined, notified, claiming, paid, expired), delivery statuses, and payment states. Include permission controls for staff roles and a full audit trail of actions and seat assignments.

Acceptance Criteria
Class-Level Waitlist Toggle and Studio Defaults
Given a studio default for Doorline Waitlist is set to Enabled or Disabled When a staff member with Manage Classes permission creates or edits a class Then they can explicitly set the class waitlist to Enabled or Disabled, overriding the studio default for that class only And the setting persists and is visible on the class settings view Given a class has waitlist Disabled When a walk‑in scans the door poster QR Then the join flow is blocked with the configured disabled message, no waitlist record is created, no payment hold is attempted, and an audit event is recorded Given a class has waitlist Enabled When a walk‑in scans the door poster QR Then the join flow begins using the class’s waitlist settings and an audit event is recorded
Max Queue Size Enforcement
Given max queue size N is configured for a class When the number of active waitlisters (statuses: joined, notified, claiming) reaches N Then additional QR join attempts receive a “Waitlist full” message, no waitlist record is created, no payment hold is attempted, and an audit event is recorded Given a waitlister transitions to paid, expired, or blocked When the active queue count drops below N Then subsequent QR join attempts may proceed normally and are counted toward N
Claim Window, No‑Show Cutoff, and Auto‑Invite
Given a no‑show cutoff and optional grace period are configured When a seat is freed by staff marking an attendee no‑show at or after cutoff (including grace) or a booked student cancels per cutoff rules Then the system automatically selects the next eligible waitlister by current queue order When a waitlister is selected Then a pay‑to‑claim notification is sent using the active template (variables: class_name, start_time, seat_count, claim_window, claim_url) And delivery status is recorded (queued, sent, failed) And the waitlister status changes to notified When the waitlister opens the claim link Then a countdown equal to the configured claim window is shown and status becomes claiming If payment is completed within the window Then the seat is assigned, the waitlister status becomes paid, they are auto‑added to the roster, and the queue advances If the timer expires without payment Then the status becomes expired, the next eligible waitlister is invited automatically, and the expired user may rejoin if capacity exists Then all timestamps, cutoffs, and timers honor the class venue’s timezone
Payment Pre‑Authorization, Fees, and Tax Handling
Given a pre‑authorization amount is configured When a walk‑in joins the waitlist Then a payment hold for the configured amount is attempted And on success the waitlister status is joined with payment_state=hold_placed And on failure, join is blocked with an error and no waitlist record persists When a waitlister claims a seat Then the final charge (ticket price ± discounts + configured fees + applicable taxes per studio tax settings) is captured, converting the hold when possible And on capture success payment_state=captured and the roster is updated And on failure payment_state=capture_failed and the seat is not assigned When a waitlister expires, is skipped, or is blocked Then any active hold is released within the configured release SLA and payment_state=hold_released is recorded Then receipts and ledger entries reflect itemized fees and taxes per configuration (tax‑inclusive vs tax‑exclusive) And all payment transitions are recorded in the audit trail
Live Queue Moderation and Auditability
Given a staff user with Queue Manage permission views the live queue When they pause the queue Then new QR joins are blocked with a “Queue paused” message and an audit entry is recorded And when resumed, joins are allowed When the staff reorders waitlisters Then the new order is persisted, visible to all staff sessions, and used for the next auto‑invite When the staff skips a waitlister Then that waitlister moves to expired (or skipped) state, any active hold is released, and the next waitlister is invited automatically When the staff manually invites a specific waitlister Then that user receives the pay‑to‑claim notification immediately and status transitions to notified When the staff blocks a user Then the user is removed from the active queue, cannot rejoin for the class, future QR joins are denied with a “Blocked” message, and a reason must be recorded Then every action writes an audit record with actor, timestamp, action, target, reason (if applicable), and before/after values And concurrent actions show a conflict message and do not overwrite newer changes
Comp Seat and Hold Bypass Overrides
Given a staff user with Comp/Override permission selects a waitlister When they choose “Comp seat” or “Bypass hold” Then the user is assigned to the class without payment capture, status becomes paid with charge_type=comp or hold_bypassed, and no fees/taxes are applied And the override requires a reason note and is recorded in the audit trail And a comp receipt is sent if the studio setting to notify comps is enabled
Role‑Based Permissions for Queue Management
Given staff roles are configured When a user without View Queue permission accesses the queue Then access is denied When a user has View Queue but not Manage Queue Then they can view waitlister statuses, delivery statuses, and payment states but cannot perform actions; controls are disabled server‑side and in UI When a user has Manage Queue or Comp/Override permissions Then they can perform only the actions tied to their permissions, actions require authenticated user identity, and all actions are logged
Waitlist Performance Analytics and Audit Logging
"As a studio owner, I want analytics on waitlist conversions and seats saved so that I can measure impact and optimize settings."
Description

Deliver reports and dashboards showing join-to-claim conversion, seats saved from no-shows, time-to-fill, revenue recaptured, average claim window utilization, notification delivery rates, and failure reasons. Provide exportable CSV and API access for BI tools. Maintain immutable audit logs for compliance and support, capturing user actions, policy changes, payment events, and message sends with timestamps and correlating IDs. Use these insights to A/B test claim windows and fees, informing studio optimizations.

Acceptance Criteria
Dashboard Metric Completeness & Accuracy
- Dashboard surfaces: join-to-claim conversion (%), seats saved from no-shows (count), time-to-fill (median and p90 minutes), revenue recaptured ($), average claim window utilization (%), notification delivery rate (%), and notification failure rate (%), each with a tooltip showing exact formula and inclusions/exclusions. - Metrics exclude canceled classes and staff comps; include only Doorline Waitlist claims tied to a successful payment capture; define a no-show as not checked-in by class cutoff policy. - For a given studio, date range, and filter set, API metric responses match dashboard values within ±0.1 percentage points or ±1 unit where applicable. - Each metric includes a time series view and a totals card; totals equal the sum/aggregation of the series within rounding tolerance (<= 0.1%). - Notification failure reasons surface top 5 provider error codes with counts; clicking a code filters affected events. - Revenue recaptured is computed as sum(payment_captured_amount) for waitlist claims replacing no-shows; currency handling respects studio currency with ISO code; rounding uses bankers’ rounding.
Data Freshness, Filters, and Timezone Handling
- New events (join, notify, claim, payment, check-in) appear in analytics within 5 minutes of occurrence (p95) and 10 minutes (p99). - Filters include: date range (absolute and presets), class, instructor, location, class type, channel (Doorline vs. other), and experiment variant; applying any filter updates visuals within 2 seconds (p95) on broadband. - All timestamps are displayed in studio local timezone with an option to toggle to UTC; exports and API default to UTC ISO 8601 with offset (e.g., 2025-09-15T14:05:07Z). - Timezone changes do not alter aggregate totals for the same absolute date-time window. - Pagination and virtualized loading maintain consistent totals across pages; changing filters resets pagination and selection state.
CSV Export Schema & Performance
- Users can export both aggregated metrics and underlying event-level data as CSV with a stable header schema; file is RFC 4180 compliant, UTF-8, with header row. - Event export includes: event_id, event_type, studio_id, class_id, waitlist_entry_id, user_id, message_id, payment_id, correlation_id, timestamp_utc, attributes (flattened), and derived flags (no_show_replacement, claimed, delivered, failed_reason_code). - Aggregated export includes each metric per day per dimension (studio, class_id, instructor_id, channel) with consistent column names and data types. - Synchronous exports support up to 100k rows under 30 seconds (p95); larger requests run asynchronously and complete within 10 minutes for up to 5 million rows, providing an expiring (24h) download link and email notification. - Numeric fields use dot decimal, no thousands separators; currency amounts exported in minor units and currency_code column present. - Exported counts and sums match dashboard/API values for the same filters within ±0.1% or ±1 unit.
Analytics API for BI Tools
- Provide versioned REST endpoints (/v1/analytics/metrics, /v1/analytics/events) with OAuth2 client credentials; scope controls read access per studio. - Endpoints support filters: date_from/date_to, studio_id, class_id, instructor_id, location_id, channel, experiment_id/variant, pagination (cursor), and sort by timestamp. - 95th percentile latency <= 800 ms for metrics and <= 1200 ms for events at 1k RPS; uptime SLO 99.9% monthly with status page exposing incidents. - Responses include request_id and correlate to audit correlation_id; strong consistency with dashboard such that values match within tolerances specified for exports. - Schema is documented via OpenAPI 3.1 and includes field-level definitions and examples; breaking changes require a new version; non-breaking changes follow additive rules. - Rate limiting is documented (e.g., 600 requests/min per client); clients receive 429 with Retry-After and can resume via cursor without duplication.
Immutable Audit Log Capture & Integrity
- The system records an audit event for: user actions (create/update/delete), policy changes (claim window, fees), payment events (hold, capture, refund, failure), and messaging events (send, delivery, failure) with exact timestamp (UTC, ms precision). - Each event contains: event_id (UUIDv4), event_type, actor_id/type, target_id/type, class_id, waitlist_entry_id, payment_id, message_id, correlation_id, previous_values (for updates), new_values, and request_id. - Storage is append-only; events are chained via SHA-256 hash of previous_event_hash + event_payload; a daily root hash is published internally and verifiable via an integrity endpoint. - Sensitive data is redacted or tokenized (e.g., phone masked, card PAN never logged, only last4 and network); secrets and access tokens are never persisted. - Event write acknowledgment within 1 second (p95) of business event and retried idempotently; duplicate submissions do not create duplicate audit records. - No update/delete operations are permitted on stored audit events; attempted mutations are blocked and logged with 403 in admin UI and 409 via API.
Audit Log Query, Access Control, and Export
- Authorized roles (Owner, Admin, Compliance) can query audit logs by time range, actor, event_type, class_id, waitlist_entry_id, payment_id, message_id, correlation_id, and status (success/failure). - Query returns results with stable sorting (timestamp desc, tie-breaker event_id); pagination via cursor with consistent results under concurrent writes. - Export of filtered audit logs available as CSV and JSONL; large exports run asynchronously with progress and a 7-day retention of downloadable artifacts. - Access is enforced by RBAC and tenant isolation; cross-tenant data is not returned; all audit queries and exports themselves generate audit events including requester and filter summary. - Retention is configurable per tenant up to legal maximum; default retention 7 years; legal holds prevent deletion and are themselves audited. - P95 query latency <= 2 seconds for 1M-row window; timeouts return actionable errors with suggested narrower filters.
A/B Testing of Claim Windows and Fees
- Admins can create experiments targeting claim_window_minutes and claim_fee parameters with variants (e.g., A: 10 min/$0, B: 7 min/$1) and define unit of randomization (customer_id or class_id) and allocation (default 50/50). - Randomization is uniform and deterministic per unit; sample ratio mismatch alerts trigger when any variant deviates >3 percentage points for >1 hour at >100 assignments. - The platform computes primary metrics per variant: join-to-claim conversion, time-to-fill, revenue recaptured, and notification-to-claim conversion; secondary: churn proxy (opt-outs), refund rate. - Dashboard shows uplift, 95% confidence intervals, p-values (two-sided), and minimum sample size thresholds; results are marked “insufficient data” until thresholds are met. - Experiments support start/stop, pause, scheduled end, and holdout; changes are audited; assignment and exposure events are logged with correlation_id. - API and CSV expose experiment metadata and per-variant metrics; archived experiments remain queryable and immutable.

Rule Canvas

A visual, drag‑and‑drop builder for prerequisite logic—combine blocks like Prior Class Completion, Certification Valid Within X Months, Age/Grade, Waiver on File, and Instructor Approval. Includes test mode and inline previews so admins can see exactly who qualifies before publishing. Cuts manual checks, prevents misbookings, and keeps rules consistent across classes and locations without writing a line of code.

Requirements

Visual Rule Canvas
"As an admin, I want to visually build eligibility rules without code so that I can create and maintain consistent prerequisites quickly and confidently."
Description

Provide a drag‑and‑drop, node‑based editor that lets admins assemble eligibility logic using blocks and AND/OR/NOT joiners. Include snap‑to‑grid, zoom/pan, grouping, inline block configuration, keyboard shortcuts (undo/redo/copy/paste), auto‑layout for readability, and an adaptive canvas for desktop and tablet admin use. Serialize rules to a portable JSON DSL compatible with the booking eligibility engine. Show live evaluation badges on each block when in test mode. Provide contextual help/tooltips, full keyboard accessibility, and localization‑ready copy. Autosave drafts and handle basic concurrent edit conflict resolution.

Acceptance Criteria
Drag-and-Drop Node Assembly with Boolean Joiners
Given the Rule Canvas is open on a supported device, When the admin drags any block from the palette onto the canvas, Then the block appears and snaps to the nearest grid intersection. Given two blocks on the canvas, When the admin connects them via an AND, OR, or NOT joiner, Then a valid connection line is created and the composite node reflects the chosen operator. Given an invalid connection attempt (e.g., output-to-output, type mismatch), When the admin tries to connect, Then the connection is prevented and an inline error tooltip explains why. Given at least one block and joiner are present, When the admin deletes a node or connection, Then the canvas updates and the rule remains syntactically valid or shows a validation warning if not.
Inline Block Configuration and Live Preview
Given a block is selected, When the admin opens its inline configuration, Then editable fields appear with validation (required, type, range) and localized labels/placeholders. Given a valid change is made (e.g., Certification Valid Within 6 months), When the admin blurs or confirms the field, Then the change is applied and reflected immediately in the block summary and JSON preview. Given an invalid input, When the admin attempts to apply, Then the field shows an error message, prevents saving, and retains the prior valid value. Given contextual help icons are present, When the admin hovers or focuses them, Then tooltips show concise, localized guidance linked to documentation.
Zoom, Pan, and Adaptive Canvas (Desktop and Tablet)
Given the canvas is loaded, When the admin uses mouse wheel/trackpad or pinch gesture, Then zoom adjusts between 25% and 200% with content remaining crisp and centered on the focal point. Given the canvas is zoomed, When the admin drags with the middle mouse or two-finger pan on tablet, Then the viewport pans smoothly without losing node positions. Given device size ≥1024px (desktop) or 768–1024px (tablet), When the admin interacts, Then hit targets are at least 44x44 px, controls adapt to touch on tablet, and no horizontal scroll is required outside the canvas.
Keyboard Shortcuts, Multi-Select, and Grouping (Accessibility)
Given focus is on the canvas, When the admin presses Cmd/Ctrl+Z or Cmd/Ctrl+Shift+Z (or Ctrl+Y), Then undo/redo reverts/applies the last action including moves, connects, edits, and deletes. Given one or more nodes are selected, When the admin presses Cmd/Ctrl+C then Cmd/Ctrl+V, Then duplicates are created with a 10px offset and preserved connections within the selection. Given multiple nodes, When the admin Shift+clicks or drags a marquee, Then multi-select is created; When pressing G, Then a named group is created; When pressing Shift+G, Then the group is disbanded. Given keyboard-only navigation, When the admin tabs/arrow-navigates nodes and menus, Then all functions are operable with visible focus, ARIA roles/labels are present, and no keyboard trap occurs.
Serialization to Portable JSON DSL and Round-Trip Load
Given a valid rule graph, When the admin clicks Save or exports, Then a JSON DSL is produced conforming to schema version X.Y with stable IDs, types, and parameters. Given the produced JSON DSL, When it is imported back into the canvas, Then the graph reconstructs with identical structure, labels, and configuration values. Given the JSON DSL is sent to the eligibility engine, When evaluated against the same sample user data, Then the pass/fail result matches the canvas evaluation. Given an unknown or older schema version is loaded, When imported, Then a non-destructive migration runs and a warning is displayed if any defaults are applied.
Test Mode with Live Evaluation Badges
Given Test Mode is enabled and sample user data is provided, When evaluation runs, Then each block shows a badge (Pass/Fail/Unknown) within 500 ms of changes, and aggregate nodes show counts. Given a block depends on external data not present, When evaluated, Then it shows Unknown with a tooltip listing missing fields. Given a discrepancy between canvas evaluation and backend engine, When detected, Then a warning banner appears with a link to view the diff and re-run evaluation.
Autosave Drafts and Concurrent Edit Conflict Resolution
Given the admin edits a rule, When changes occur or every 5 seconds of inactivity, Then an autosave writes a draft with a timestamp and an “All changes saved” status indicator. Given the network is offline, When an autosave is due, Then changes are queued locally and synced automatically upon reconnection with user notification. Given two admins edit the same rule, When a save conflict is detected, Then the later editor is presented with a merge dialog showing theirs vs remote changes, with options: Merge, Overwrite, or Discard, and an activity log entry is created.
Prerequisite Block Catalog and Data Connectors
"As an admin, I want ready‑made, configurable blocks that pull from our records so that eligibility rules reflect real data without custom development."
Description

Offer a curated palette of configurable blocks: Prior Class Completion, Certification Valid Within X Months, Age/Grade, Waiver on File (with version), Instructor Approval, Membership/Pass Ownership, Time Since Last Attendance, Custom Attribute Check, Location Restriction, and Date Window. Each block exposes parameters with input validation and helpful defaults. Connect each block to authoritative ClassTap data sources (attendance history, certifications with expiry, user profile DoB/grade, digital waivers, approvals, passes/memberships). Support external data via webhook/API and CSV import with field mapping and normalization. Implement data freshness indicators, cache TTLs, and retry/backoff policies. Enforce tenant scoping and field‑level permissions so only authorized data is surfaced in the canvas and evaluations.

Acceptance Criteria
Catalog Availability and Block Parameters
Given an authorized admin opens the Rule Canvas Block Catalog When the catalog loads Then the following blocks are available: Prior Class Completion; Certification Valid Within X Months; Age/Grade; Waiver on File (with version); Instructor Approval; Membership/Pass Ownership; Time Since Last Attendance; Custom Attribute Check; Location Restriction; Date Window And each block displays a parameter panel with labeled inputs, helper text, and default values And saving a block is disabled until all required parameters pass validation When an invalid parameter is entered (e.g., negative months, malformed date, missing required field) Then an inline error identifies the field and reason, and the invalid value is not persisted When the admin clicks Reset on a block Then all parameters revert to their documented defaults
Authoritative Data Resolution Across Core Blocks
Given tenant T and a user U in tenant T When Prior Class Completion is configured for Course ID C and evaluated for U Then it returns true if attendance history contains a completion record for C for U, otherwise false When Certification Valid Within X Months is configured with X=6 and class start date D and evaluated for U Then it returns true if U has a certification record whose expiry date is on or after D and whose issue date is within 6 months prior to D, otherwise false When Age/Grade is evaluated for U for a class starting at D Then age is computed from U's DoB as of D and grade is read from U's profile; comparison operators (=, >=, <=, between) produce correct boolean results When Waiver on File is configured for version V and evaluated for U Then it returns true if the digital waivers store shows a signed waiver for version V for U, otherwise false When Instructor Approval is evaluated for U Then it returns true only if the approvals store has status Approved for U for the target class or program, otherwise false When Membership/Pass Ownership is evaluated for U Then it returns true if U holds an active pass/membership valid for the class at booking time and with required credits (if applicable), otherwise false When Time Since Last Attendance is configured for threshold T_days and evaluated for U Then it returns true only if days since U's last attendance in the target class/program/location is >= T_days
External Data via Webhook/API and CSV with Mapping and Normalization
Given an external data connector block is configured with a webhook URL, auth secret, and response schema When the admin runs Test Connection Then a 2xx response with a payload matching the declared schema is required before the configuration can be saved When the webhook responds with non-2xx or times out Then the error is surfaced with status code and message, and the configuration cannot be saved Given a CSV import for an external data-backed block When the admin uploads a CSV and maps required fields to system fields Then the system validates required mappings, normalizes data types (dates, booleans, numbers), trims whitespace, and reports row-level errors with row numbers and reasons And only valid rows are imported; invalid rows are excluded and available in a downloadable error report And the field mapping can be saved and reused for future imports within tenant T When duplicate keys are detected in the CSV or across prior imports Then later rows overwrite earlier rows only if the overwrite option is enabled; otherwise duplicates are rejected with errors
Data Freshness Indicators, Caching TTL, and Retry/Backoff
Given any data-connected block with cache TTL configured When the block loads Then the UI displays a Last updated timestamp and freshness state (Fresh when age <= TTL, Stale when age > TTL, Error on last fetch failure) And a Refresh action is available to bypass cache and refetch now When TTL is edited to a new integer value between 1 and 1440 minutes (default 15) Then validation enforces the range and updates future cache expiry behavior; if current cache age > new TTL, the next evaluation triggers a refetch When a data fetch fails Then the system retries with exponential backoff and jitter up to N attempts (configurable 1–5, default 3), and surfaces the final error state with the time of last attempt And no partial data is used for evaluation on error unless a prior fresh cache exists within TTL, in which case the cache is used and the UI indicates Degraded with timestamp When a 304 Not Modified is returned from the source Then cached data is retained, Last checked is updated, and freshness state remains unchanged
Tenant Scoping and Field-Level Permissions Enforcement
Given admin A belongs to tenant T with role R When A opens the Block Catalog and any data field picker Then only fields permitted to role R are visible and selectable; restricted fields do not appear in search or suggestions When evaluating any block for a user or class Then all data queries are scoped to tenant T and no cross-tenant records are returned When A attempts to add or evaluate a block requiring a field A lacks permission to access Then the action is blocked with a message indicating the missing permission, and no data values are leaked When external data is configured (webhook or CSV) by A Then the data is stored and accessible only within tenant T and is not visible to users of other tenants
Custom Attribute, Location Restriction, and Date Window Behavior
Given a Custom Attribute Check configured with key=k, operator==, value=v When evaluated for a user with profile attribute k present Then it returns true if the normalized value equals v (case and type aware), otherwise false When operator is in and value is a list [v1, v2] Then it returns true only if the attribute value matches one of the listed values Given a Location Restriction configured to include locations [L1, L2] When evaluated for a class at location L1 Then it returns true; when at any other location, it returns false; when mode is exclude, logic inverts accordingly Given a Date Window with start=S, end=E, timezone=Z When evaluated for a class starting at datetime D Then it returns true if S <= D <= E interpreted in timezone Z; if E is empty, the window is open-ended; invalid ranges (E < S) are blocked by validation
Eligibility Test Mode and Inline Preview
"As an admin, I want to simulate who qualifies before publishing so that I can catch mistakes and reduce support issues."
Description

Provide a non‑destructive test mode where admins can pick a specific attendee, enter an email/phone, or upload a sample roster to simulate rule evaluation. Display inline pass/fail per block with reason codes and missing‑data notes, plus an overall eligibility result. Allow date/time simulation (e.g., “as of next Monday”) to validate time‑bound rules like certification windows and age thresholds. Offer coverage insights for targeted classes (e.g., percentage of current roster that qualifies) and downloadable test reports for training and audit.

Acceptance Criteria
Single Attendee Simulation via Email or Phone
Given an Admin with "Manage Rules" permission and an existing rule canvas When they enter a known attendee's email or phone and click Run Test Then the system evaluates the attendee against the current rule canvas and renders each rule block with a Pass or Fail badge and a machine-readable reason code And any required data not found on the attendee profile is flagged on the affected block with a "Missing Data: <field>" note and the block result is set to Unknown And an overall eligibility result is displayed as Pass, Fail, or Unknown with a timestamp and the rule set version identifier And no records are created or modified, and no messages (SMS/email) are sent
Roster Upload Simulation with Row-Level Results
Given an Admin uploads a CSV roster that matches the downloadable template When the file is parsed Then rows with required identifiers (email or phone) are accepted and invalid rows are rejected with line-specific error messages When the test is executed Then each row displays overall result (Pass/Fail/Unknown) and per-block results with reason codes and missing-data notes And a summary totals bar shows counts and percentages by overall result And the system performs no writes to production data and sends no notifications
Date/Time Simulation for Time-Bound Rules
Given an as-of date/time selector defaulting to Now When the Admin chooses a future or past date/time and runs the test Then all time-based computations (age thresholds, certification windows, waiver expiry) use the selected as-of reference And any block whose outcome changes relative to Now displays a "Changed due to As-Of" indicator with a before/after note And the as-of value appears in the overall result header and in downloaded reports
Coverage Insights for Selected Class
Given an Admin selects a class and a rule canvas When they run Coverage Then the system computes the percentage and count of the current class roster that would Pass, Fail, or be Unknown under the selected rule canvas And a breakdown shows top failing blocks by count with aggregated reason codes And for rosters up to 2,000 attendees, the coverage computation returns within 10 seconds at P95
Downloadable Test Reports for Training and Audit
Given a completed single-attendee or roster test When the Admin clicks Download Then CSV and PDF are generated containing: test scope, as-of timestamp, rule set version, tester identity, overall result(s), per-block results with reason codes, and missing-data notes And file names follow the convention RuleTest_<RuleId>_<YYYYMMDD-HHMMSS>.<ext> And the downloaded data matches the on-screen results 1:1
Inline Preview Usability and Accessibility
Given per-block results are rendered inline When navigating via keyboard Then all block details (status, reason code, missing-data note) are reachable and readable using standard tab order and focus indicators And the UI meets WCAG 2.1 AA contrast for Pass/Fail/Unknown indicators and provides ARIA labels for screen readers And the inline preview layout works without horizontal scrolling at 320px width and scales up to desktop
Non-Destructive Mode and Access Control
Given Test Mode When executed by a user without "Manage Rules" permission Then access is denied with an explanatory message and no evaluation runs When executed by an authorized Admin Then no enrollments, waivers, certifications, approvals, or payments are created, updated, or deleted; and no outbound SMS/email/webhooks are triggered And each test execution is logged with user, timestamp, scenario (single/roster/coverage), as-of value, rule set version, and row count (if applicable)
Checkout and Waitlist Enforcement & Messaging
"As a customer booking a class, I want clear guidance when I don’t meet prerequisites so that I know what to do next and avoid failed checkouts."
Description

Integrate compiled rules into checkout and waitlist flows to evaluate eligibility in real time for single and group bookings. Present clear, branded messaging when requirements are unmet, with actionable next steps (book prerequisite, sign waiver, request instructor approval). Support soft‑gate (warn) and hard‑gate (block) modes configurable per rule. Re‑evaluate on waitlist promotion, reschedules, instructor overrides, and data updates. Log all decisions with reason codes for support and analytics. Provide API hooks/webhooks for instructor approval workflows and external systems.

Acceptance Criteria
Checkout: Real-time Eligibility Evaluation (Single Attendee)
- Given compiled rules are attached to a class, when a single attendee reaches the payment step with required profile data, then all applicable rules are evaluated within 800ms p95 and 1500ms p99. - If any hard-gate rule fails, then the Pay/Confirm button is disabled, an error banner lists each failed rule with a human-readable label, and the booking cannot be submitted. - If any soft-gate rule fails and no hard-gate fails, then a warning banner is shown, the user can proceed only after checking an acknowledgment box, and the acknowledgment is stored on the booking record. - When the attendee completes a self-serve action (e.g., signing a waiver, updating profile, uploading certification) via provided CTAs, then the rules re-evaluate immediately and the UI updates without a page reload. - Every evaluation creates a decision log entry with overall outcome (pass/warn/block) and reason codes per rule.
Checkout: Group Booking Mixed Eligibility Handling
- Given a group booking of up to 10 attendees, when proceeding to payment, then the system evaluates rules per attendee within 1.5s p95 total and 3s p99 total. - The UI displays per-attendee eligibility status (pass/warn/block) with labels of failed rules. - If one or more attendees fail a hard-gate, then the user is prevented from submitting the booking until each failing attendee is removed or resolves the issues; eligible attendees can still be booked after confirmation. - Pricing, taxes, and seat counts recalculate immediately when attendees are removed or become eligible. - Soft-gate warnings are captured per attendee as acknowledgments and stored on the booking.
Waitlist: Eligibility on Join and Promotion
- When a user attempts to join a waitlist, then rules are evaluated and applied according to each rule’s gate mode for waitlist; hard-gate failures block joining with reasons, soft-gate failures allow joining with a warning and “Needs Action” tag. - On promotion to an available spot (automatic or manual), rules are re-evaluated; if any hard-gate fails, promotion is not confirmed, the user and admin are notified with required next steps, and the waitlist position is preserved. - If only soft-gate rules fail on promotion, the promotion proceeds, a warning is sent, and acknowledgment is recorded on the booking. - If the user completes required actions while on the waitlist, the system detects the change and automatically re-evaluates within 10 seconds; if all hard-gates pass, the user is eligible for promotion.
Re-evaluation on Reschedules, Overrides, and Data Updates
- When a booking is rescheduled to a different session, then rules are re-evaluated before confirmation; hard-gate failures block the reschedule, soft-gate failures allow it with recorded acknowledgment. - When an instructor override is applied (via UI or API), then the specified attendee, class/session, and rule IDs are marked as overridden with author, reason, and optional expiry; subsequent evaluations treat those rules as passed until expiry while other rules evaluate normally. - When relevant data changes (waiver signed, DOB updated, certification uploaded, external system update), then the affected attendee’s eligibility is re-evaluated within 5 seconds; any change in decision triggers appropriate notifications and UI updates. - Duplicate external events do not create duplicate state changes or logs (idempotent processing keyed by event ID).
Branded Messaging with Actionable Next Steps
- Error/warning UI elements use the organization’s theme colors and logo. - For each unmet rule, the message shows a human-readable label and one actionable CTA: Book prerequisite, Sign waiver, Request instructor approval, Upload certification, or Update profile. - CTAs deep-link to the relevant flows pre-filled with context (attendee, class, rule) and return the user to checkout after completion. - After completing a CTA, the system re-evaluates eligibility immediately and updates the messaging state without page reload.
Decision Logging and Analytics
- For every evaluation event (checkout, waitlist join, promotion, reschedule, override, data update), the system records an immutable log entry containing: timestamp (UTC ISO-8601), actor (user/admin/system), organization ID, class/session ID, booking ID, attendee ID(s), evaluated rule IDs and versions, gate mode per rule, outcome per rule (pass/warn/block), overall decision, reason codes, source trigger, correlation ID, and the message template keys shown to the user. - Logs are queryable by organization, class/session, booking, attendee, date range, and outcome; exportable as CSV; and retrievable via API with pagination. - Logs are retained for at least 365 days and are tamper-evident.
Instructor Approval Integrations (API/Webhooks)
- A REST endpoint exists to request instructor approval; when invoked from checkout or via API, an approval record is created with status=pending and a unique approval_id, and a webhook approval.requested is emitted. - On receiving approval.approved or approval.denied webhooks (HMAC-signed, retried up to 10 times with exponential backoff, 5s timeout, idempotency key enforced), the system updates the approval record, re-evaluates eligibility immediately, and unblocks or blocks the flow accordingly. - The UI reflects approval status in real time (pending/approved/denied) and displays or hides the Request Approval CTA as appropriate. - All approval events and resulting eligibility decisions are logged with reason codes and correlation IDs.
Assignment, Inheritance, and Targeting
"As an ops manager, I want to apply rules across classes and locations with exceptions so that I keep policies consistent without duplicating work."
Description

Allow rules to be assigned to classes, series, categories, and locations with inheritance and explicit overrides. Support org‑level defaults and per‑class exceptions. Provide an impact preview showing all classes affected before publishing, along with an effective date range scheduler. Ensure the booking engine resolves and applies the correct rule set per class/location at evaluation time, including fallbacks when no rule is assigned.

Acceptance Criteria
Org default with per-class override
Given an organization-level default rule R_default is published and effective now And class "Yoga 101" has an explicit assigned rule R_class effective now And class "Pilates 101" has no explicit rule When the booking engine evaluates prerequisites for "Yoga 101" Then R_class is the applied rule set And R_default is not applied to "Yoga 101" When the booking engine evaluates prerequisites for "Pilates 101" Then R_default is the applied rule set
Series-level rule inheritance with explicit class override
Given series "Summer Bootcamp" has an assigned rule R_series effective now And class "Bootcamp Week 1" belongs to the series and has no explicit rule And class "Bootcamp Week 2" belongs to the series and has an explicit rule R_class2 effective now When the booking engine evaluates prerequisites for "Bootcamp Week 1" Then R_series is the applied rule set When the booking engine evaluates prerequisites for "Bootcamp Week 2" Then R_class2 is the applied rule set And R_series is not applied to "Bootcamp Week 2"
Location-level rule applied with precedence and explicit override
Given location "Downtown" has an assigned rule R_loc effective now And class "Spin 7AM" occurs at location "Downtown" and has no explicit rule When the booking engine evaluates prerequisites for "Spin 7AM" Then R_loc is the applied rule set When an explicit rule R_class is assigned to class "Spin 7AM" and is effective now And the booking engine re-evaluates prerequisites Then R_class is the applied rule set And R_loc is not applied
Impact preview lists affected classes before publishing
Given an admin targets a new rule R_new to category "Aquatics" and location "East Pool" And sets an effective date range from 2025-10-01 00:00 to 2026-03-31 23:59 in org timezone And some classes in category "Aquatics" at location "East Pool" have explicit overrides When the admin opens the impact preview before publishing R_new Then the preview displays the total count of classes that will be affected And lists each affected class with identifier, title, series (if any), and location And marks classes excluded due to explicit overrides with the reason "Explicit override" And reflects the effective date range in the header, including timezone And the Publish action is disabled until the preview is acknowledged
Effective date scheduling activates and deactivates rules correctly
Given rule R_sched is assigned to series "Fall Yoga" with a start time 2025-10-01 08:00 and end time 2025-12-15 22:00 in America/New_York When the booking engine evaluates a class in "Fall Yoga" at 2025-10-01 07:59 America/New_York Then no series rule is applied from R_sched When it evaluates at 2025-10-01 08:00 America/New_York Then R_sched is applied When it evaluates at 2025-12-15 22:01 America/New_York Then R_sched is not applied And the scheduling UI displays and stores the timezone used
Runtime rule resolution with fallback when no rule is assigned
Given class "Open Gym" has no explicit class rule And belongs to no series with a rule And is not in a category with a rule And is at a location with no rule And the organization has no default rule When the booking engine evaluates prerequisites for "Open Gym" Then no prerequisite rule is applied and booking proceeds Given the organization later sets a default rule R_default effective now When the booking engine evaluates prerequisites for "Open Gym" again Then R_default is the applied rule set
Deterministic precedence across targets
Given the following effective rules exist simultaneously: org default R_org, location rule R_loc for "Downtown", category rule R_cat for "Cardio", series rule R_series for "Cardio Blast", and class rule R_class for class "Cardio Blast 6PM" And class "Cardio Blast 6PM" belongs to series "Cardio Blast", is in category "Cardio", and occurs at location "Downtown" When the booking engine evaluates prerequisites for "Cardio Blast 6PM" Then the precedence order applied is: Class > Series > Category > Location > Org Default And R_class is the applied rule set When the class explicit rule is removed Then R_series becomes the applied rule set When the series rule is removed Then R_cat becomes the applied rule set When the category rule is removed Then R_loc becomes the applied rule set When the location rule is removed Then R_org becomes the applied rule set When an explicit class override "No Rule" is set on the class Then no prerequisite rule is applied regardless of higher-level rules
Templates, Versioning, and Publishing Workflow
"As a program director, I want versioned, reusable rule templates so that I can standardize policies and safely iterate."
Description

Enable saving rules as reusable templates with tags and search. Provide semantic versioning, change summaries, and side‑by‑side diffs of logic changes. Support draft, review, and publish states with optional approver gates and scheduled publishing. Allow rollback to prior versions and automatic deprecation of superseded rules. Notify impacted staff of pending changes and expose a class‑level changelog.

Acceptance Criteria
Save Rule as Template with Tags and Search
Given an admin has a completed rule canvas open When they choose "Save as Template", enter a unique Name and Description, and assign one or more tags Then a new template is created with a unique ID, creator, and timestamp And the template appears in the Templates library with its tags and previewable logic And searching by full or partial name, description keywords, or tag returns the template within the top 10 results And filtering by a selected tag includes the template and excludes non‑tagged templates
Semantic Versioning and Change Summary Enforcement
Given an existing template at version 1.3.2 When an admin edits the template and selects "Publish Changes" Then the system requires: a version bump type (MAJOR, MINOR, or PATCH) and a non‑empty change summary of at least 10 characters And the new version strictly follows x.y.z and increments according to the selected bump type And version numbers are unique per template and cannot be lower than the current version And the change summary and author are stored and visible in the version history
Side-by-Side Logic Diff Visualization
Given two versions of a template are selected for comparison When the admin opens the diff view Then added, removed, and modified rule blocks are visually highlighted and aligned side‑by‑side And clicking a changed node scrolls and highlights its counterpart in the opposite pane And unchanged sections can be collapsed/expanded to focus on changes And a toggle allows switching between visual graph diff and raw JSON/YAML diff And the diff content matches the exact persisted logic of the selected versions
Draft, Review, Approver Gate, and Publish Workflow
Given the organization has an approver gate enabled for publishing When an editor saves a draft and requests review Then the draft status changes to "Review Requested" and approvers receive a review notification And editors cannot publish while approval is pending When an approver approves the draft Then the draft status changes to "Ready to Publish" and publish becomes available When the version is published Then it becomes immutable with a publish timestamp, and the action is audit‑logged with actor and time And only users with Approver role can approve; only authorized roles can create/edit drafts
Scheduled Publishing with Timezone Accuracy
Given an approved draft version exists When an approver schedules publishing for a future datetime with a specific IANA timezone Then the schedule shows both local time and UTC and is stored reliably And the version is automatically published at the exact scheduled instant, even across service restarts And the published version is labeled "Published (Scheduled)" with the effective time recorded And a scheduled publish can be canceled or rescheduled prior to execution, with changes audit‑logged And attempting to create a second schedule for the same template prompts to replace the existing one
Rollback to Prior Version and Auto‑Deprecation
Given a template has a currently published version 1.4.0 and a prior version 1.3.2 When an admin selects "Rollback" to 1.3.2 and confirms with a reason Then a new version is created (e.g., 1.5.0) whose logic matches 1.3.2 and it becomes the active Published version And the previously active 1.4.0 is marked Deprecated with timestamp and reason And all changes are recorded in version history and visible in the audit log And any classes referencing the template now resolve to the newly published version without breaking existing bookings
Notifications and Class‑Level Changelog Exposure
Given a publish (immediate or scheduled) or rollback affects templates used by one or more classes When the action is confirmed Then impacted staff (class owners, assigned instructors, and org admins) receive a notification in‑app and via email including version, change summary, effective time, and a link to the diff And recipients are deduplicated so no user receives duplicate notifications for the same event And the class‑level changelog displays an entry with version, action (publish, schedule, rollback, deprecate), actor, timestamp, and summary And staff can mark the changelog entry as acknowledged; acknowledgments are stored and reportable
Audit Trail, Permissions, and Access Control
"As an owner, I want a clear audit trail and permissions around rule changes so that we remain compliant and can investigate issues."
Description

Record who created, edited, reviewed, and published each rule with timestamps and reason notes. Enforce role‑based access controls for creators, editors, reviewers, and viewers, including restrictions on sensitive blocks (e.g., age/grade). Provide exportable audit logs and integrate with existing organization audit reporting. Align with data protection policies and regional consent requirements for eligibility checks.

Acceptance Criteria
Audit Events for Rule Lifecycle
Given a user with the Creator role saves a new rule draft When the save completes Then an immutable audit record is stored with fields: event_type=create, rule_id, version_id, actor_id, actor_role, timestamp (UTC ISO 8601), reason_note (nullable), source_ip Given a user with the Editor role updates any block or parameter in an existing rule When they save the changes Then an audit record is stored with event_type=edit and a changed_fields array listing field_name, old_value, new_value for each modified field Given a user with the Reviewer role records a review outcome for a rule When they submit the review Then an audit record is stored with event_type=review and review_outcome in {approved, changes_requested} and optional reason_note Given a user with the Publisher role publishes a rule version When they confirm with a mandatory reason note of at least 5 characters Then an audit record is stored with event_type=publish, published_version_id, reason_note, and timestamp Given any audit record exists When viewed in the audit log UI or via API Then the record content is read-only (no update/delete endpoints), is paginated, and is retrievable within 2 seconds for page sizes up to 50
Role-Based Access Matrix Enforced
Given defined roles {Creator, Editor, Reviewer, Publisher, Viewer} When users attempt actions Then permissions are enforced as: Creator={create, view drafts they created}, Editor={edit drafts in their scope}, Reviewer={set review status, view all drafts}, Publisher={publish approved rules}, Viewer={view published rules}; and no role may perform actions outside its set Given a user without the required permission attempts a restricted action (e.g., publish by Creator) When the action is triggered Then the system returns 403 Forbidden, shows a standard error message, and logs an access_denied audit event with attempted_action, actor_id, and timestamp Given SSO/IdP role mappings are updated by an org admin When the mapping is saved Then the new permissions take effect by next login or within 5 minutes for active sessions, and a role_mapping_updated audit event is recorded
Sensitive Blocks Restrictions and Masking
Given sensitive blocks {Age/Grade, Certification Validity, Legal Waiver} When a user lacking 'Sensitive Block: Edit' permission browses the Rule Canvas Then those blocks are hidden from the block palette and any attempt to create/update them via API returns 403 with no sensitive payload in the response Given a user lacking 'Sensitive Block: View Data' permission opens a rule containing sensitive blocks When the rule loads Then sensitive values are masked in the UI and excluded from API responses unless include_sensitive=true and the request has sufficient scope; explanations in previews omit PII Given test mode or inline preview is executed by an unauthorized user When evaluations run Then predicate results are computed without revealing PII and only non-identifying outcomes (pass/fail and rationale without raw values) are displayed/logged
Audit Log Export with Filters
Given an org admin opens Audit Export When they apply filters by date range, user, rule_id, event_type, and location Then the preview updates with a total count and sample rows Given the admin starts an export When processing completes (<=60 seconds for up to 100k rows) Then downloadable CSV and JSON files are generated with headers/keys: event_id, event_type, rule_id, version_id, actor_id, actor_role, timestamp_utc, source_ip, reason_note, changed_fields_json, and a SHA-256 checksum is provided Given export policies require PII protection When the file is generated Then IPs are partially masked for non-Super Admins, reason_note is redacted per configured patterns, and an export_created audit event with checksum and file_expiry (+7 days) is logged Given no records match filters When export runs Then an empty file with headers is produced and counts show 0
External Audit Integration Delivery
Given an organization has configured an external audit endpoint with URL and API key When a new audit record is created Then the system POSTs the record within 2 minutes, signs it with HMAC-SHA256 (header: X-Signature), and retries on failure with exponential backoff for up to 24 hours Given repeated delivery failures exceed retry policy When the endpoint continues to return non-2xx Then the integration is marked degraded, org admins are notified via email, a manual requeue option is available, and all attempts are logged with last_error Given the external endpoint returns 200 OK When delivery succeeds Then the audit record delivery status is marked delivered and visible in integration logs filterable by date and status
Regional Consent Enforcement for Eligibility Checks
Given a rule includes a consent-gated sensitive block and the org's region requires consent When a user attempts to publish the rule for that region Then a linked consent policy must be selected; otherwise publishing is blocked with a validation error and no publish audit event is created Given an end-user booking flow evaluates eligibility using a consent-gated rule When no valid consent exists for the subject Then evaluation is halted, a consent_missing audit event with pseudonymized subject_id is recorded, and booking is prevented Given valid consent exists with appropriate scope and expiry When the eligibility evaluation runs Then the consent_id and consent_timestamp are attached to the evaluation audit record and surfaced in exports/integrations
Permissions Changes and Scope Audited
Given an admin grants or revokes a role or permission for a user When the change is saved Then an audit record with event_type=permission_change is stored including actor_id, target_user_id, added_permissions, removed_permissions, scope (org/location), and timestamp Given a user's permissions are reduced during an active session When they attempt a now-prohibited action Then the action is blocked, a re-auth prompt is shown, and an access_denied audit event is recorded Given an organization has multiple locations When a user attempts to act on a rule outside their assigned scope Then the action is blocked with 403 and the audit log records attempted_scope, resource_id, and actor_scope

Cert Snap

Fast proof capture that lets attendees snap or upload certificates on mobile. Auto‑reads name, issue/expiry dates, and provider, then auto‑approves when matched to rules or routes to quick review when something’s off. Stores once and reuses across bookings, with renewal reminders baked into SMS/email. Reduces back‑and‑forth and keeps rosters compliant without slowing checkout.

Requirements

Mobile Certificate Capture & Upload
"As an attendee, I want to quickly snap or upload my certificate on my phone during checkout so that I can complete my booking without back‑and‑forth."
Description

Enable attendees to capture a certificate via mobile camera or upload images/PDFs during booking or from their account, with real‑time edge detection, crop/rotate, multi‑page support, file size/type validation, upload progress, and fallback for low connectivity. Integrates into the checkout flow without adding friction by allowing configurable gating (required before payment or allowed after provisional booking) per class. Ensures secure client‑side compression and transport over TLS, and tags each upload with booking, user, and class identifiers for downstream processing.

Acceptance Criteria
Mobile Camera Capture with Edge Detection and Editing
Given an attendee on a mobile device at the certificate step during checkout When they choose Capture with Camera Then the live camera view displays real‑time edge detection and alignment guides And the user can manually capture or auto‑capture triggers when the document is stable And after capture, a crop box is auto‑applied to detected edges and the user can adjust crop and rotate in 90° increments And saving the edit produces an image in a supported type (JPEG/PNG/HEIC) and proceeds to upload
File Upload of Images/PDFs with Validation and Multi‑page Support
Given allowed file types are configured as jpg, jpeg, png, heic, pdf and max_upload_size is set to 20 MB When the attendee selects files from device storage during checkout or in Account > Certificates Then files with unsupported types are blocked with the message "Unsupported file type" And files exceeding 20 MB are blocked with the message "File exceeds 20 MB limit" And multi‑page PDFs are accepted and preserved as multiple pages And multiple images selected in one action are accepted and stored as a multi‑page certificate in the order selected And successful selections advance to preview prior to upload
Upload Progress, Resumable Transfers, and Low‑Connectivity Fallback
Given the attendee starts an upload of a certificate When the upload is in progress Then a per‑file progress indicator shows percentage and bytes transferred And if connectivity is lost for up to 15 minutes, the upload auto‑resumes from the last completed chunk without user action And if the app is closed or offline persists beyond 15 minutes, the upload is queued locally and resumes on next launch with the same account And the attendee can tap Save and finish later to defer the upload and retain the selected files securely on device until completion or cancellation
Checkout Gating Per Class: Required Before Payment vs Post‑Booking Allowed
Given Class A is configured as Certificate required before payment When an attendee proceeds to checkout for Class A Then the Pay/Confirm action remains disabled until a certificate has been captured/uploaded and passed basic file validation And attempting to skip shows guidance to add a certificate Given Class B is configured as Certificate can be provided after provisional booking When an attendee proceeds to checkout for Class B without adding a certificate Then the booking completes as Provisional with certificate status Pending and payment succeeds And the attendee sees a clear prompt to upload now or later from Account > Bookings
Secure Client‑side Compression and TLS Transport
Given a certificate image is captured or selected When the file is prepared for upload Then client‑side compression is applied prior to network transfer without altering legibility And uploads are sent only over HTTPS using TLS 1.2 or higher And if a secure transport cannot be established, the upload is blocked and the user is prompted to retry on a secure connection And no certificate data is transmitted in query strings; file data is sent via POST body with authorization headers
Metadata Tagging for Downstream Processing
Given an attendee uploads a certificate during checkout for Class C When the upload request is sent Then the payload includes booking_id (or provisional_booking_id), user_id, and class_id fields And upon booking completion, any provisional_booking_id is replaced with the final booking_id without losing the certificate linkage And the stored record is immutable for these identifiers and is retrievable via API using any of the three IDs
Account Flow: Add or Replace Certificate After Booking
Given an attendee has an existing booking with certificate status Pending When they navigate to Account > Bookings > [Booking] > Certificates and select Add/Replace Then they can capture via camera or upload files with the same validation, editing, and progress behaviors as in checkout And upon successful upload, the booking’s certificate status updates to Submitted and is visible immediately on the booking detail screen
Auto‑Extraction (OCR) of Credential Fields
"As an instructor, I want the system to auto‑read key fields from uploaded certificates so that I don’t have to manually type or verify them."
Description

Automatically read name, provider, issue date, and expiry date from uploaded certificates using an OCR pipeline with template‑agnostic parsing, date normalization (locale/timezone aware), and confidence scoring per field. Supports multiple file formats, glare/noise handling, and retries. Returns structured data with field confidences and a redacted thumbnail for review. Includes fuzzy logic to match common provider names and normalizes provider aliases. Falls back to manual review when fields are missing, low‑confidence, or contradictory.

Acceptance Criteria
OCR Extraction on Clear Certificate Image
Given a supported single-image certificate upload with legible text When the OCR pipeline processes the file Then the system returns structured JSON containing fields: name, provider_raw, provider_normalized, issue_date, expiry_date, and confidences{name,provider,issue_date,expiry_date} And issue_date and expiry_date are returned in ISO 8601 date format YYYY-MM-DD And a redacted thumbnail image is generated and its URL is included in the response And extraction succeeds without relying on template-specific rules
Multi-Format Upload Support (JPG/PNG/PDF/HEIC)
Given an upload in any of the formats: JPG, JPEG, PNG, PDF (single or multi-page), or HEIC When the OCR pipeline executes Then the file is accepted and processed without conversion errors And for multi-page PDFs, pages are evaluated sequentially until required fields are extracted or pages are exhausted, and the page_index used is returned And if a file is corrupted or unreadable, an explicit error code is returned and the case is routed to manual review
Locale- and Timezone-Aware Date Normalization
Given certificate date strings that may be ambiguous (e.g., 01/02/2025) and a known tenant locale/timezone When date parsing and normalization run Then issue_date and expiry_date are interpreted according to the tenant locale (e.g., en-GB → DD/MM/YYYY, en-US → MM/DD/YYYY) And month names, ordinal suffixes, and common separators are correctly parsed And normalized dates are returned as YYYY-MM-DD, with the tenant timezone applied when a time or zone is present; otherwise defaults to tenant timezone And invalid or impossible dates result in needs_review with reason=invalid_date
Provider Fuzzy Match and Alias Normalization
Given a provider text extracted from the certificate When fuzzy matching and alias normalization run against the provider catalog Then provider_normalized is set to the catalog’s canonical name when match_score ≥ 0.85 or an alias match is found And provider_match_score is included in the response And if match_score < 0.85 and no alias match exists, provider_normalized is null and the case is routed to needs_review with reason=provider_unmatched while preserving provider_raw
Per-Field Confidence Scoring and Threshold Routing
Given extracted field candidates with confidence scores When routing logic evaluates results Then auto_approve=true only if all mandatory fields (name, provider, issue_date, expiry_date) are present, each confidence ≥ 0.80, and dates are non-contradictory (expiry_date ≥ issue_date) And if any mandatory field is missing or confidence < 0.80, status=needs_review with reason=low_confidence or missing_field And confidence scores are floats in [0.0, 1.0] with three-decimal precision included per field
Glare/Noise Handling with Auto-Retry
Given an uploaded image exhibiting glare, noise, skew, or low contrast When the first OCR pass yields any mandatory field with confidence < 0.50 Then the pipeline applies image enhancements (deskew, denoise, deglare/contrast boost, adaptive binarization) and retries up to 2 additional OCR passes And the best per-field result across passes is used for routing And total processing time including retries does not exceed 5 seconds for a 2 MB image on a standard worker And if after retries any mandatory field remains < 0.80 confidence, the case routes to needs_review with reason=low_confidence
Fallback to Manual Review with Redacted Thumbnail
Given missing fields, low confidence, unsupported format, invalid dates, or contradictions (e.g., expiry_date < issue_date or expiry_date already expired beyond allowed grace) When the pipeline finalizes the extraction outcome Then a manual review task is created with payload including structured data, per-field confidences, redacted thumbnail URL, and reasons {missing_field | low_confidence | unsupported_format | invalid_date | contradiction} And the API response includes review_id and extraction_status=needs_review without blocking checkout
Rules Engine & Auto‑Approval
"As a studio owner, I want certificates to auto‑approve when they meet my rules so that rosters stay compliant without slowing checkout."
Description

Provide a configurable rules engine at organization, location, program, and class levels to define accepted providers, certificate types, minimum validity windows, and name‑match thresholds. On successful match, auto‑approve the credential and mark the booking compliant; otherwise, route to review with explicit failure reasons. Supports effective‑date versioning of rules, per‑class overrides, and actions (block checkout, allow provisional booking, waitlist only). Emits events/webhooks for approvals, rejections, and expirations for downstream integrations.

Acceptance Criteria
Auto-approval on exact rules match at organization level
Given an organization-level ruleset allows provider "Red Cross", certificate type "CPR-AED", minimum validity window 30 days relative to class start, and name-match threshold 0.90 And an attendee named "Jordan Smith" uploads a certificate extracted as provider "Red Cross", type "CPR-AED", attendee name "Jordan A. Smith", expiry ≥ 30 days after the class start, and issue date < expiry When the attendee submits the certificate during checkout for a class starting at 2025-10-15T10:00:00Z Then the engine evaluates the effective ruleset for the class scope chain and applies the organization-level rule And the computed name match score is ≥ 0.90 And the credential is auto-approved And the booking is marked Compliant within 2 seconds of submission And an audit record is stored with rule_id, evaluated_scope="org", match_scores, input fields, and decision timestamp
Scope precedence with per-class override
Given rules exist at organization, location, program, and class scopes And the class defines an override that sets minimum validity window to 7 days and adds provider "Acme Training" to the allowed list When an attendee submits a certificate from provider "Acme Training" that expires 8 days after the class start Then the class-level override takes precedence over broader scopes And the credential is auto-approved And the audit record indicates evaluated_scope="class" and override_applied=true And no broader-scope rule blocks the approval
Effective-dated rules versioning with deterministic evaluation
Given a program-level ruleset v1 effective_until 2025-09-30 and v2 effective_from 2025-10-01 And a class starts on 2025-10-05 and inherits program rules When an attendee submits a certificate for that class on 2025-09-20 Then the engine evaluates against v2 because evaluation is based on class start time And the decision references the specific rule_version_id used And re-running the evaluation with the same inputs yields the same decision
Routing to manual review with explicit failure reasons
Given location-level rules require provider in ["Red Cross"], certificate type in ["CPR-AED"], minimum validity window 30 days, and name-match threshold 0.92 When an attendee submits a certificate with provider "Blue Cross", type "CPR-AED", expiry 20 days after class start, and name match score 0.89 Then the engine routes the credential to Manual Review And booking compliance is set to Non-Compliant (Pending Review) And the response includes failure reasons with codes and details: provider_not_allowed, validity_window_too_short (required 30d, found 20d), name_match_below_threshold (required 0.92, found 0.89) And an audit record persists the failed checks, rule_id, and extracted/expected values
Rule-configured actions on failure: block, provisional, waitlist only
Given a class-level rule action is "Block Checkout" on failure When evaluation fails Then checkout is prevented with an error summarizing unmet requirements and no seat is reserved Given the action is "Allow Provisional Booking" When evaluation fails Then booking completes with status=Provisional and compliance=Pending and a reminder task is scheduled before class start Given the action is "Waitlist Only" When evaluation fails Then the attendee can join the waitlist but cannot confirm a seat
Webhook events for approvals, rejections, and expirations
Given a webhook endpoint is configured and reachable When a credential is auto-approved Then an event "credential.approved" is emitted within 5 seconds with payload fields: event_id, event_type, occurred_at, booking_id, attendee_id, credential_id, rule_id, decision="approved", match_scores, signature, and idempotency_key When a credential is rejected after review Then an event "credential.rejected" is emitted with reasons[] and the same required meta fields When a stored credential is 14 days from expiry and linked to at least one upcoming compliant booking Then an event "credential.expiring" is emitted once per credential per day maximum with retries (≥3 over 10 minutes) until a 2xx response is received
Re-evaluation on rules change and on credential update
Given a booking is Provisional due to rules failure and a new rules version becomes effective for that class When the effective ruleset changes for the class start date Then the engine re-evaluates affected pending/provisional bookings within 60 seconds and updates compliance and emits the appropriate event Given an attendee uploads a replacement certificate for a failed one When the new credential meets applicable rules Then the booking is updated to Compliant, prior failure reasons remain in audit history, and an approval event is emitted
Reviewer Queue & Quick Triage
"As an admin, I want a fast review queue for exceptions so that I can resolve edge cases in seconds and keep classes moving."
Description

Deliver a lightweight admin queue to triage flagged certificates with side‑by‑side original and extracted data, inline field edits, quick approve/reject with canned reasons, request resubmission, and bulk actions. Shows confidence indicators, rule failures, and history for the user’s prior credentials. Provides keyboard shortcuts, SLA timers, and notification triggers to minimize turnaround time. All actions are recorded in an immutable audit log with actor, timestamp, changes, and booking linkage.

Acceptance Criteria
Queue Intake, SLA Timers, and Ordering
Given a certificate is flagged by Cert Snap, When it is ingested, Then it appears in the Reviewer Queue within 5 seconds. Given multiple items in the queue, When the queue loads, Then items are ordered by SLA due time ascending and each item displays a mm:ss countdown. Given an item is <= 15 minutes from SLA breach, When the threshold is crossed, Then a "Due Soon" badge is shown and an email notification is sent to the assigned reviewer within 1 minute. Given filters and sorting controls, When the reviewer applies "Overdue" or "Assigned to me", Then the queue updates to show only matching items without page refresh. Given an item is opened by Reviewer A, When Reviewer B attempts to open it, Then the item shows an "In Review" lock with Reviewer A’s name and prevents conflicting actions for B.
Side-by-Side View with Confidence, Rule Failures, and History
Given a reviewer opens a queue item, When the detail view renders, Then the original document (image/PDF) is shown on the left and extracted fields (name, provider, issue date, expiry date) on the right. Given extracted fields are displayed, When confidence per field is below 0.85, Then that field is highlighted and labeled with its exact confidence percentage. Given validation rules are evaluated, When any rule fails, Then the failed rules list displays rule name and human-readable reason. Given the attendee has prior credentials, When the detail view loads, Then a history panel shows prior submissions for the same user with type, status, and dates. Given a 10 MB document, When the detail view opens on a broadband connection, Then the side-by-side view fully loads within 2 seconds.
Inline Field Edits and Real-Time Revalidation
Given a reviewer edits an extracted field inline, When Save is clicked or Ctrl+S is pressed, Then the new value persists, is marked as "edited", and validation re-runs updating pass/fail indicators immediately. Given a date field is edited, When an invalid date or format is entered, Then Save is disabled and an inline error explains the required format (YYYY-MM-DD). Given a field has been edited, When the reviewer hovers the edit badge, Then a tooltip shows the original OCR value. Given unsaved edits exist, When Cancel is clicked or Esc is pressed, Then all edits are reverted to the last saved values.
Quick Approve/Reject with Canned Reasons and Shortcuts
Given a reviewer is on an item, When Approve is triggered via button or keyboard shortcut A, Then the certificate status becomes Approved, the associated booking’s credential requirement is marked satisfied, and the item is removed from the queue. Given a reviewer chooses Reject, When initiated via button or keyboard shortcut R, Then a modal requires selecting a canned reason and allows an optional note before submission. Given a decision (Approve or Reject) is submitted, Then an email and SMS are sent to the attendee with the decision and next steps within 1 minute, and the booking detail reflects the new status. Given keyboard shortcuts are enabled, When J or K is pressed, Then the next/previous item opens; when E is pressed, Then focus enters the first highlighted field for editing and a toast confirms the action.
Request Resubmission Workflow and Reminders
Given a reviewer selects Request Resubmission via button or shortcut U, Then the certificate status changes to Resubmission Requested, the SLA timer pauses, and the attendee receives an email and SMS with a secure upload link and instructions. Given no new upload occurs, When 24 hours elapse, Then an automatic reminder is sent every 24 hours up to 3 times or until a new upload is received. Given the attendee uploads a new document from the link, When ingestion completes, Then the prior attempt is retained in history, auto-approval rules are re-evaluated, and the item either auto-approves or returns to the queue assigned to the last reviewer.
Bulk Actions with Safeguards and Performance
Given multiple items are selectable, When the reviewer multi-selects via Shift+Arrow and Space or by checkboxes, Then bulk action buttons (Approve, Reject, Assign) become enabled and show the count selected. Given any selected item has a critical rule failure requiring manual review, When a bulk Approve is attempted, Then the action is blocked with an explanation and a list of offending items. Given a bulk Reject is confirmed, When a canned reason is chosen, Then it applies to all selected items with an option to override per item before final submission. Given a bulk action on up to 200 items, When executed, Then at least 95% of successful item updates complete within 10 seconds and a results report lists successes and failures without rolling back successful items.
Immutable Audit Log with Actor, Timestamp, Changes, and Booking Linkage
Given any queue action (view, edit, approve, reject, request resubmission, bulk action, assignment), When it occurs, Then an audit entry is recorded with actor ID, UTC timestamp, action type, certificate ID, booking ID, and before/after values for changed fields. Given audit entries exist, When viewed in the admin, Then entries are read-only and display in chronological order with precise timestamps to the second. Given an attempt to modify audit data, When a user tries to edit or delete an audit entry via UI or API, Then the UI disallows the action and the API returns 405 Method Not Allowed. Given a booking is opened, When viewing its audit trail, Then all related certificate actions are visible with deep links to the certificate and reviewer who acted.
Credential Vault & Reuse Across Bookings
"As a returning attendee, I want my previously approved credentials to auto‑apply to new bookings so that I don’t have to re‑upload them."
Description

Store approved certificates in a secure, encrypted credential vault linked to the attendee’s profile, with deduplication, versioning, and provider/type metadata. Automatically reuse valid credentials across future bookings that require the same certificate, attaching them silently during checkout to avoid re‑upload. Allow attendees to view, replace, or remove credentials from My Account, and allow admins to revoke or force renewal. Enforce least‑privilege access controls and data retention policies aligned with org settings.

Acceptance Criteria
Encrypted Vault & Least‑Privilege Access
Given a credential is stored in the vault, Then the file and metadata are encrypted at rest and in transit and keys are managed by the platform KMS. Given a user without the required role attempts to access another attendee’s credential, When the request is made, Then access is denied with 403 and no sensitive fields are returned. Given a user with the correct role accesses a credential, Then only provider, type, issue date, expiry date, status, and version are returned unless an explicit download is requested. Given any credential read/write/delete action occurs, Then an immutable audit log entry is recorded with actorId, attendeeId, action, timestamp, and IP. Given a cross‑org request to any credential, Then access is denied and the event is logged as a security violation.
Deduplication & Versioning on Add/Replace
Given an attendee uploads a certificate with identical content hash to their active credential of the same provider and type, Then the system does not create a new version and returns a message indicating it is already stored. Given an attendee uploads a certificate with a different content hash for the same provider and type, When the upload is approved, Then a new version number is created (previousVersion+1), the new version becomes Active, and the prior version is retained as Archived. Given concurrent uploads of the same document, When processed, Then only one new version is created and referenced as Active. Given a credential version is created, Then metadata is captured and stored: provider, type, issue date, expiry date, issuing country/region (if provided), and reviewerId/auto‑approved flag.
Auto Reuse During Checkout
Given an attendee has an Active, non‑expired credential whose provider and type satisfy the class requirement rules, When they reach checkout for that class, Then the credential is auto‑attached within 300 ms and no re‑upload prompt is shown. Given multiple matching Active credentials exist, Then the most recent non‑expired version is auto‑attached. Given no matching Active credential exists, Then the attendee is prompted to upload and the checkout cannot complete until a valid credential is attached. Given an attached credential is later revoked before class start, Then the booking is flagged non‑compliant and the attendee is notified to replace the credential.
Expiry Validation & Renewal Reminders
Given an Active credential with expiry date E and org reminder policy of 30/7/1 days, Then SMS/email reminders are queued at E‑30, E‑7, and E‑1 days with opt‑out respected. Given a credential is expired at checkout time, Then it is not auto‑attached and the attendee must upload a replacement. Given an org‑defined grace period G exists, Then auto‑attach is allowed only if now < E + G and the booking date is within G, otherwise block and prompt for renewal. Given a credential is replaced, Then any pending reminders for the old credential are suppressed and a confirmation is sent.
Attendee Self‑Service: View, Replace, Remove
Given an attendee opens My Account > Credentials, Then a list shows each credential with provider, type, issue date, expiry date, status (Active/Revoked/Expired), and current version. Given an attendee selects Replace on an Active credential, When a new document is approved, Then a new version is created and set to Active; the previous Active becomes Archived. Given an attendee selects Remove on a credential not referenced by any upcoming booking, Then the credential status becomes Deleted and it is excluded from future auto‑reuse. Given an attendee attempts to Remove a credential referenced by an upcoming booking, Then the action is blocked with a message listing the affected bookings. Given any change is made, Then the action appears in the attendee’s activity log within 1 minute.
Admin Revoke & Force Renewal Controls
Given an admin with Credential:Manage permission opens an attendee’s credential, When Revoke is applied with a reason, Then the credential status becomes Revoked, it is excluded from auto‑reuse, and an audit entry is recorded. Given upcoming bookings used the revoked credential, Then those bookings are marked non‑compliant and attendee/admin notifications are sent within 5 minutes. Given an admin sets Force Renewal by date D, Then the credential cannot be auto‑attached for checkouts after D unless a newer version exists. Given an admin clears Force Renewal, Then normal auto‑reuse resumes if the credential is Active and non‑expired.
Data Retention & Purge Policy
Given org retention policy R days for expired/revoked/deleted credentials, Then the system permanently deletes the credential file and metadata after R days and records a purge audit log. Given a credential is purged, Then it no longer appears in UI, search, or API responses, and any storage objects are removed from primary buckets. Given legal hold is enabled for an attendee, Then retention‑based purge is skipped until the hold is lifted. Given a purge job fails, Then it is retried with exponential backoff and alerts are raised after 3 consecutive failures.
Renewal Reminders via SMS/Email
"As a program coordinator, I want automated renewal reminders via SMS/email so that attendees keep their certifications current."
Description

Schedule automated renewal reminders based on certificate expiry with configurable lead times (e.g., 60/30/7 days) and final grace window, delivered via existing SMS/email channels using brand templates. Messages include a secure deep link to update the credential on mobile with one tap. Support per‑org opt‑out, throttle limits, localization, and time zone alignment. Track delivery, opens, and completion, and automatically update booking eligibility based on renewal status.

Acceptance Criteria
Lead-Time Scheduling and Time Zone Alignment
Given an org with default time zone America/New_York and a learner profile time zone Europe/Berlin with a certificate expiring 2025-12-31 10:00 in the learner’s time zone, When lead times are set to 60, 30, and 7 days, Then reminders are scheduled for 2025-11-01 10:00, 2025-12-01 10:00, and 2025-12-24 10:00 in Europe/Berlin. Given a grace window of 5 days after expiry and no renewal, When the scheduler runs, Then exactly one final reminder is sent at 2026-01-05 10:00 in the learner’s time zone. Given a target send time that crosses daylight saving transitions, When computing the schedule, Then the send occurs at the configured local time (e.g., 10:00) on each target date. Given a reminder already sent for a specific lead-time milestone, When the scheduler re-runs, Then no duplicate reminder for that milestone is queued or sent. Given the learner has no time zone set, When computing send times, Then the org’s default time zone is used.
Branded SMS/Email with Secure Deep Link
Given the org has active branded templates for SMS and email and the learner has both channels enabled, When a reminder is sent, Then the message uses the org’s template and includes populated tokens for learner name, certificate name, expiry date, and org name. Given a reminder is sent, When the learner receives it, Then it contains a secure deep link with a single-use token bound to org, learner, certificate type, and send id, and the token expires after 24 hours or upon successful submission, whichever occurs first. Given the deep link token is expired or already used, When accessed, Then the user is shown an error and offered a way to request a fresh link without exposing any personal data. Given the learner opens the link on a mobile device, When tapped, Then the credential update flow loads with mobile camera/upload options available and pre-associates the renewal to the correct certificate type.
Per-Org Opt-Out and Send Throttling
Given the org-level setting "Certificate renewal reminders" is disabled, When the scheduler runs, Then no new reminders are queued or sent for that org and any pending queued reminders are canceled. Given per-channel throttling is configured at 30 messages per minute per org, When due reminders exceed the limit, Then messages are queued and dispatched without exceeding 30/min/channel and remaining messages are sent in subsequent minutes. Given the provider returns a rate-limit response, When retrying, Then the system applies exponential backoff and respects provider retry-after headers, with a maximum of 3 retry attempts per message. Given the org has disabled SMS in channel preferences but enabled email, When reminders are sent, Then only email is used and no SMS is attempted.
Localization of Reminder Content
Given the org has templates for en-US and es-ES and the learner’s locale is es-ES, When sending a reminder, Then the es-ES template is used and the expiry date/time is formatted in the learner’s locale (e.g., DD/MM/YYYY HH:mm) and translated strings are rendered. Given the learner’s locale is not supported, When sending, Then the org’s default locale template is used. Given localized templates include dynamic tokens, When rendering, Then all tokens are resolved and there are no placeholder artifacts in the delivered message.
Event Tracking: Delivery, Opens, and Completion
Given a reminder is sent, When provider events are received, Then the system records per message: channel, provider message id, send timestamp, delivery status (queued, sent, delivered, bounced/failed), and any provider error code. Given the learner opens an email (pixel) or clicks the reminder link (any channel), When the event is captured, Then an "open" event with timestamp and source channel is stored and deduplicated per send id. Given the learner completes a renewal via the deep link, When upload/submit succeeds, Then a "renewal_completed" event is recorded and associated to the originating send id and learner. Given org analytics are queried via API, When filtered by date range and certificate type, Then delivery, open, and completion counts are returned and can be exported as CSV.
Auto-Update Booking Eligibility on Renewal Status
Given a learner has upcoming bookings requiring Certificate X and the certificate is expired but within grace window, When a valid renewal is auto-approved or manually approved, Then the learner’s eligibility is set to eligible within 5 minutes and applied to all upcoming bookings for Certificate X. Given the grace window has elapsed without renewal, When the learner attempts a new booking that requires Certificate X, Then the booking is blocked with a clear error indicating certificate expiry and a link to update is presented. Given existing future bookings for classes after the expiry date with no renewal, When the grace window elapses, Then those bookings are marked ineligible and surfaced in the org’s roster/compliance view for action. Given the learner renews via the reminder link and passes auto-approval rules, When eligibility updates, Then waitlist promotions and check-in for affected classes proceed without manual intervention.
Compliance Indicators in Rosters & Reporting
"As a front‑desk staff, I want clear compliance indicators in rosters and reports so that I can enforce policies and communicate status to attendees."
Description

Surface real‑time compliance status in class rosters with clear badges (Compliant, Pending Review, Expiring Soon, Expired, Rejected) and filters. Allow optional enforcement to block check‑in or payment capture for non‑compliant bookings per class policy. Provide exports and an API endpoint to pull compliance summaries, along with an audit trail of approvals/rejections and expiry changes. Include dashboards to monitor compliance rates and average review times.

Acceptance Criteria
Real-Time Compliance Badges on Class Roster
Given a staff user is viewing a class roster And each booking has an evaluated certificate status from Cert Snap When the roster loads Then each booking row displays exactly one badge from {Compliant, Pending Review, Expiring Soon, Expired, Rejected} matching the underlying status And when an approval, rejection, new certificate read, or system-driven expiry event occurs Then the affected booking’s badge reflects the new status within 10 seconds without requiring a page refresh And Expiring Soon is assigned when the certificate expiry falls within the expiring-soon window configured for the class policy
Roster Filters by Compliance Status
Given a staff user is viewing a class roster with a compliance status filter control When the user selects one or more statuses from {Compliant, Pending Review, Expiring Soon, Expired, Rejected} Then only bookings with the selected statuses are shown in the roster And when the user clears the selection Then all bookings are shown again And the applied filter persists when navigating away and back to the roster within the same session
Enforcement Blocks Check-in and Payment for Non-compliance
Given a class has Enforce Compliance at Check-in enabled in its policy When a staff user attempts to check-in a booking with status Expired or Rejected (or Pending Review if Block Pending Review is enabled) Then the check-in action is blocked and an explanatory message states the blocking status and required action Given a class has Enforce Compliance at Payment Capture enabled in its policy When the system attempts to capture payment for a booking with status Expired or Rejected (or Pending Review if Block Pending Review is enabled) Then the capture is prevented and no charge is created, and the booking shows a non-compliance error state in the payment log
Compliance Summary Export
Given an instructor or admin with export permissions is on a roster or report view When they request an export with the current filters and date range Then a CSV or XLSX file is generated containing at minimum: class_id, class_name, class_start_at, booking_id, attendee_id, attendee_name, compliance_status, certificate_provider, certificate_issue_date, certificate_expiry_date, last_reviewed_at, reviewer_id, review_decision, review_reason, last_status_change_at And the export reflects the same records and statuses visible in the UI under the applied filters And all timestamps are in the account time zone and ISO 8601 format And exports up to 50,000 rows complete within 30 seconds
Compliance Summary API Endpoint
Given an authenticated API client with a valid token When the client requests the compliance summary endpoint with optional filters (date_range, class_id, instructor_id, location_id) Then the API responds with HTTP 200 and JSON containing counts per status {Compliant, Pending Review, Expiring Soon, Expired, Rejected} and total_bookings for the requested scope And when the client provides invalid parameters Then the API responds with HTTP 400 and a descriptive error message And unauthenticated requests receive HTTP 401 And the endpoint returns results within 2 seconds for queries covering up to 10,000 bookings
Audit Trail of Approvals/Rejections and Expiry Changes
Given a booking has a certificate reviewed or its expiry updated (manually or system-driven) When the status changes (e.g., Pending Review -> Compliant, Compliant -> Expired, Compliant -> Rejected) Then an immutable audit record is written capturing: event_type, previous_status, new_status, previous_expiry_date, new_expiry_date, actor_id (or system), actor_role, reason/note (if provided), occurred_at And the booking detail view shows the chronological audit trail And the audit trail entries are available in exports and via API detail endpoints
Compliance Dashboards and Metrics
Given an admin opens the Compliance dashboard and selects a date range and scope (all classes or a specific class) When metrics are calculated Then the dashboard shows compliance rate defined as (bookings in Compliant status at class start time) / (total bookings requiring certification in range) And the dashboard shows average review time measured from certificate submission to first approval or rejection within the selected range And charts and KPIs reflect applied filters and refresh at least every 15 minutes And dashboard data can be exported as CSV

Fit Check

A self‑serve eligibility checker shown on class pages and paywalls. Clearly explains what’s needed to book (e.g., “Complete Intro Yoga” or “Upload CPR card”) and offers one‑tap actions: upload proof, add prerequisite to cart, or request review. Removes guesswork, decreases abandonment, and builds trust for first‑time attendees.

Requirements

Eligibility Rules Engine
"As a studio owner, I want to configure eligibility rules per class using simple options and logic so that only qualified students can book without manual screening."
Description

Configurable engine to define and evaluate class-level eligibility criteria with support for prerequisite classes, membership tiers, signed waivers, age limits, certifications with expiry, and custom questionnaire gates. Provides AND/OR logic groups, effective/expiry dates, per-schedule overrides, multi-tenant templating, and localization. Evaluates in real time on class pages and paywalls, returning standardized status codes (eligible, action_required, under_review, expired) and human-readable reasons. Exposes an internal API for UI components and webhooks to trigger when eligibility changes. Includes caching for performance, rate limiting, and resilience against partial outages. All rule definitions and evaluations are versioned for traceability and safe rollouts.

Acceptance Criteria
AND/OR Logic Evaluation with Standardized Status Codes
Given a class rule tree with top-level AND of [prereq_completed(Intro Yoga) AND (membership_tier in {Gold,Platinum} OR waiver_signed=true)], When a user has completed Intro Yoga, membership_tier=Gold, and no waiver on file, Then evaluation returns status=eligible and reasons=[], actions=[]. Given the same rule tree, When a user has not completed Intro Yoga but meets (membership_tier in {Gold,Platinum} OR waiver_signed=true), Then evaluation returns status=action_required and reasons includes {code:"missing_prerequisite", prereq_id, prereq_name}, and actions includes {type:"add_to_cart", resource:"class", id:prereq_id}. Given an OR group with N child rules, When at least one child evaluates true, Then the OR group evaluates true and no failing reasons from other children appear in reasons. Given an AND group with N child rules, When any child evaluates false, Then the AND group evaluates false and reasons aggregates only unique failing reasons from the false children in deterministic order by rule declaration. Given evaluation completes, Then reasons use a controlled vocabulary of codes {missing_prerequisite, missing_membership, missing_waiver, age_restriction, cert_expired, questionnaire_required} with human_readable texts provided separately.
Expiry- and Date-Bound Rules (Certifications, Waivers, Age)
Given a certification rule with cert_expiry_date < now, When evaluating, Then status=expired and reasons includes {code:"cert_expired", cert_id, cert_name, expiry_date} and actions includes {type:"upload", resource:"certification", id:cert_id}. Given a certification rule with cert_expiry_date >= now, When evaluating, Then the certification rule passes and contributes no failing reason. Given a waiver requirement with validity_days, When a user’s waiver_signed_at within validity window, Then the waiver rule passes; otherwise status=action_required and actions includes {type:"open_waiver", resource:"waiver", id:waiver_id}. Given a class with min_age=16, When user.current_age < 16, Then status=action_required and reasons includes {code:"age_restriction", min_age:16, current_age}, and actions=[] and booking_blocked=true. Given any rule with effective_start and effective_end dates, When now is outside [effective_start, effective_end], Then that rule is ignored for evaluation (neither pass nor fail) and does not contribute a reason.
Per-Schedule Overrides and Multi-Tenant Templates Precedence
Given tenant T defines template TemplateA with rules R1 and R2, and class C adopts TemplateA, and schedule S of C overrides R2=disabled, When evaluating for schedule S, Then only R1 is enforced and R2 contributes no reason. Given rule sources at three levels [template, class, schedule], When conflicting settings exist, Then precedence is schedule > class > template and the effective rule set reflects that order. Given two tenants T1 and T2, When evaluating eligibility for a user in T1, Then rules from T2 are not accessible and do not affect the outcome. Given an override at schedule level changes from disabled to enabled, When next evaluation occurs, Then the change is reflected immediately (within 1 minute) and captured in evaluation metadata as {rule_source:"schedule"}.
Localization of Human-Readable Reasons and Actions
Given a request with locale=fr-FR, When evaluating and returning human_readable texts, Then all reason.text and action.label fields are localized to fr-FR while reason.code remains stable in English. Given a request with locale that lacks a specific translation (e.g., es-MX missing a key), When evaluating, Then the engine falls back to tenant_default_locale and then to en-US if still missing. Given multiple locale hints (query param, user profile, Accept-Language, tenant default), When conflicting, Then precedence is query param > user profile > Accept-Language > tenant default and the selected locale is included in the response as response_locale. Given reasons include dynamic tokens (e.g., {prereq_name}, {expiry_date}), When localized, Then tokens are interpolated and formatted per locale conventions (dates, numbers) before returning.
Internal API Contract, Caching, Rate Limiting, and Resilience
Given POST /internal/eligibility/evaluate with {tenant_id, user_id, class_id, schedule_id, locale}, When successful, Then response JSON includes {status in [eligible, action_required, under_review, expired], reasons[], actions[], evaluation_id(UUIDv4), rule_version(semver), evaluated_at(ISO8601), cache_ttl_ms(int)}. Given identical inputs within cache_ttl_ms and no underlying data changes, When re-evaluated, Then the engine serves from cache with the same evaluation_id and a remaining_ttl_ms field. Given sustained traffic, When requests exceed 100 requests/min per tenant, Then API responds 429 with Retry-After header and no evaluation performed. Given warm cache, When evaluating, Then p95 latency <= 150ms; Given cold start or cache miss, Then p95 latency <= 400ms. Given a dependent service (e.g., membership) is unavailable or exceeds 300ms timeout, When evaluating, Then status=under_review and reasons includes {code:"dependency_unavailable", dependency:"membership", timeout_ms}, and no 5xx is returned. Given the API is called without valid service credentials, When evaluating, Then 401 is returned and no eligibility data is leaked.
Webhooks for Eligibility Change and Idempotent Delivery
Given a user’s eligibility for (tenant_id, class_id, schedule_id) changes (e.g., action_required -> eligible), When detected, Then the engine emits webhook event eligibility.updated with payload {tenant_id, user_id, class_id, schedule_id, before:{status,reasons}, after:{status,reasons}, evaluation_id, rule_version, occurred_at(ISO8601)}. Given webhook delivery, When sent, Then each event is signed with HMAC-SHA256 using the tenant’s secret, includes X-Signature and X-Event-Id headers, and an idempotency_key in the payload. Given a subscriber returns non-2xx, When retrying, Then retries use exponential backoff starting at 10s up to 24h with at-least-once delivery guarantees and a maximum of 100 attempts. Given rapid successive state flaps, When changes occur within 60 seconds, Then events are debounced and only the final state is delivered, with a changes[] summary in the payload. Given a subscriber requests replay for a time window, When authorized, Then events are replayable by event_id range with original ordering preserved.
Versioning and Traceability of Rules and Evaluations
Given any change to rule definitions, When published, Then a new immutable rule_version is created and tagged with author, changelog, created_at. Given an evaluation runs, When response is returned, Then it includes evaluation_id and rule_version and the system stores an evaluation trace containing inputs, rule tree snapshot, and outputs retrievable by evaluation_id for 90 days. Given a staged rollout is configured (e.g., 10% of users), When evaluating, Then assignment is consistent per user_id (sticky) and both old and new rule_version metrics are recorded. Given a rollback is initiated, When executed, Then traffic switches to the prior stable rule_version within 1 minute and subsequent evaluations reference the rolled-back version. Given audit requirements, When querying by user_id or class_id, Then the system can produce a chronological list of evaluations with status and rule_version for compliance.
Document Upload & Verification
"As a student, I want to upload proof with one tap from my phone so that I can satisfy requirements quickly and continue booking."
Description

One-tap capture and upload of proof artifacts (e.g., CPR card, ID, completion certificate) from mobile and desktop with support for photos and PDFs, client-side compression, and instant previews. Performs file validation, virus scanning, and metadata extraction (OCR for name and expiry) to auto-populate fields and pre-qualify submissions. Stores documents securely with encryption at rest and access controls, links them to the user profile, and flags duplicates or expired items. Routes submissions to an approval workflow with statuses (submitted, under_review, approved, rejected) and triggers notifications. Provides accessibility-compliant UI, retry logic for poor connectivity, and localized error messages.

Acceptance Criteria
Mobile one‑tap capture with compression and preview
Given a signed-in user on a class page When they tap "Upload proof" and capture a photo Then camera permission is requested and, if denied, a file-picker fallback is presented And the captured image is client-side compressed to ≤ 2 MB with longest edge ≥ 1500 px before upload And HEIC images are automatically converted to JPEG And an on-device preview renders within 2 seconds of capture And the user can retake or confirm before the upload begins
Upload validation, malware scanning, and retry resilience
Given a user selects a file When the file type is JPG, PNG, or PDF Then the system accepts it; otherwise it blocks with a localized error listing allowed types And if an image exceeds 10 MB pre-compression or 2 MB post-compression, or a PDF exceeds 10 MB Then the upload is blocked with a localized size error And every file is scanned for malware before being persisted; if malware is detected Then the upload is rejected, the temporary file is discarded, and no record is linked to the profile And uploads are chunked with up to 3 automatic retries using exponential backoff starting at 1s And interrupted uploads can resume within 15 minutes without data loss And a progress indicator updates at least once per second; on fatal failure, a localized error is shown and form data is preserved
Secure storage, linkage, and access control
Given an upload passes validation When it is stored Then the document is encrypted at rest (AES-256) and transmitted over TLS 1.2+ And the stored object is linked to the uploader's user profile and the relevant prerequisite type And access is restricted to the uploader and authorized roles (Owner, Reviewer); all others receive HTTP 403 And access URLs are signed and expire within 15 minutes And an immutable audit log records create/read/update/delete events with timestamp, actor, and action
OCR metadata extraction and auto‑population
Given a legible photo or PDF is uploaded When OCR runs Then the system attempts to extract full name and expiry date And if extraction confidence ≥ 0.85 Then the name and expiry fields on the submission are auto-populated and displayed for confirmation And if confidence < 0.85 or a field is missing Then the user is prompted to enter or edit the field before submission And if expiry > today and the extracted name matches the user profile with similarity ≥ 0.90 Then the submission is marked pre_qualified=true And OCR completes within 5 seconds for files ≤ 2 MB
Duplicate and expired document detection
Given a user attempts to upload a document of a prerequisite type When the system computes a content hash and compares against existing documents of the same type for that user Then exact duplicates are blocked with a localized duplicate message And if the uploaded document's expiry date is before today (user's timezone) Then the submission is saved but flagged not qualifying with a clear, localized message to upload a valid document And if an unexpired approved document of the same type already exists Then the user is informed and must explicitly confirm to proceed with a replacement
Approval workflow and user notifications
Given a submission is created Then its initial status is submitted When a reviewer opens the submission Then the status can be set to under_review by authorized roles only When a reviewer approves or rejects Then the status changes to approved or rejected respectively, with a required reason on rejection And the user receives SMS and email notifications in their saved locale within 1 minute of the status change And Fit Check reflects the approved state within 5 seconds of approval And all transitions are recorded in the audit log with actor and timestamp
Accessibility and localization compliance
Given a user navigates the upload UI When using a keyboard only Then all interactive elements are reachable in a logical tab order with visible focus and operable controls And all inputs, buttons, and status messages have screen-reader labels or roles; upload progress announces via aria-live And color contrast for text and interactive elements is ≥ 4.5:1 And error messages and placeholders are localized based on the user's selected locale with English fallback And after a validation error, focus moves to the first errored field and the error is announced
Eligibility Status UI
"As a prospective attendee, I want a clear indicator of whether I’m eligible and what steps remain so that I can decide and act without leaving the page."
Description

Self-serve checker component embedded on class pages and paywalls that displays the user’s current eligibility state and exact next steps. Surfaces clear badges and copy for Eligible, Action Required, Under Review, and Expired, with contextual CTAs: upload proof, add prerequisite to cart, request review, or contact support. Supports guest users with a lightweight sign-in gate, progressive disclosure to reduce clutter, and full keyboard/screen-reader accessibility. Localizable content and studio-branding controls ensure white-label consistency. Emits analytics events for impressions, clicks, and conversion, and gracefully degrades if the rules API is unavailable.

Acceptance Criteria
Eligibility State Display and Progressive Disclosure
Given a signed-in user views a class page or paywall When the eligibility rules API responds Then the component renders a badge labeled exactly one of: "Eligible", "Action Required", "Under Review", "Expired" Given the state is Eligible When the component is visible Then the copy displays a confirmation that the user can book and no required actions Given the state is Action Required When the details panel is expanded Then the copy lists each unmet requirement with clear next-step hints Given the state is Under Review Then the copy indicates review is in progress and how the user will be notified Given the state is Expired Then the copy indicates what expired and the expiration date Given the component loads Then details are collapsed by default and a "Show details" control is visible Given a user toggles "Show details" Then the panel expands/collapses and the expanded/collapsed state persists while on the page
Contextual CTAs per Eligibility State
Given state is Action Required and the unmet requirement is a course/class prerequisite When the user clicks "Add prerequisite to cart" Then the specific prerequisite is added to the cart exactly once, a confirmation toast appears, and the CTA changes to "View cart" Given state is Action Required and the unmet requirement is a document/certification When the user clicks "Upload proof" Then the proof uploader opens with the correct requirement preselected Given state is Expired with a re-upload path When the user clicks "Re-upload proof" Then the uploader opens and replaces the existing file upon submission Given the user cannot self-resolve the requirement When viewing available actions Then a "Contact support" link is visible and opens the configured support channel prefilled with class and studio context Given any CTA click Then the button shows a loading indicator, the action is idempotent, and failures display an inline error without reloading the page
Guest Sign-in Gate and Return-to-Context
Given a guest user views the component Then the header indicates "Sign in to check eligibility" and CTAs are gated Given a guest user clicks any gated CTA When sign-in completes successfully Then the user is returned to the same class page anchored to the eligibility component and the original action resumes (e.g., uploader opens, item added to cart) Given sign-in is cancelled or fails Then no changes are applied and the guest view remains Given a signed-in user refreshes the page Then the component reflects their eligibility without requiring re-authentication
Proof Upload and Review Flow
Given Action Required for a document/certification When the user uploads a file of type PDF, JPG, or PNG up to 10 MB Then client-side validation passes and an upload progress indicator is shown Given the upload completes Then a preview (thumbnail or filename) is shown and a "Submit for review" action becomes enabled Given the user clicks "Submit for review" Then the eligibility state transitions to Under Review, the UI updates, and a confirmation message is displayed Given the file fails validation or upload Then a specific error message is displayed and the user can retry Given an uploaded proof reaches its expiry date Then the next eligibility evaluation sets state to Expired and shows a "Re-upload proof" action Given the backend later approves the proof When the user revisits or refreshes the page Then the state displays Eligible
Accessibility: Keyboard and Screen Reader
Given a keyboard-only user When tabbing through the page Then every interactive element in the component is reachable in a logical order and has a visible focus indicator Given a keyboard-only user When pressing Space or Enter on any CTA or expander Then the intended action triggers Given a screen reader user Then the badge announces the current state and next step via accessible name/description, and state changes are announced via an aria-live region Given any theme/branding Then text and interactive elements meet WCAG 2.1 AA contrast (4.5:1 for normal text, 3:1 for UI components) Given any interactive element Then it has an accessible role, name, and state, and there are no keyboard traps
Localization and White-Label Branding
Given the site locale is changed Then all component strings, dates, and numbers render in that locale and layout supports both LTR and RTL Given a translation key is missing Then the English fallback is shown and a missing-translation analytics event is logged Given a studio applies branding (colors, button shapes, typography) Then the component adopts those styles while preserving accessibility requirements and does not display ClassTap branding Given long translations or expanded copy Then the layout responsively wraps without overlapping or truncating critical information
Analytics and Graceful Degradation
Given the component becomes visible Then exactly one eligibility_impression event is emitted per page view with properties: user_id or anon_id, studio_id, class_id, eligibility_state (if known), locale, and timestamp Given a user clicks any CTA Then an eligibility_cta_click event is emitted with cta_type, eligibility_state, class_id, studio_id, and a deduplicated click_id Given a user becomes eligible and initiates booking from the component Then an eligibility_conversion event is emitted with class_id and eligibility_state=Eligible Given the rules API errors or exceeds a 1.5s timeout Then the component shows a neutral "Eligibility temporarily unavailable" message with "Try again" and "Contact support" actions, suppresses state-specific CTAs, and continues to render without blocking the rest of the page Given the user clicks "Try again" Then the component retries the eligibility check and replaces the fallback with the result on success
One-Tap Add Prerequisite to Cart
"As a new user, I want to add the prerequisite class to my cart with one tap so that I can resolve eligibility and finish booking."
Description

Contextual CTA that, when a required intro course or package is missing, adds the correct prerequisite offering to the cart with one tap and preserves the user’s intended class context. Preselects the earliest available session that satisfies conflicts and location preferences, supports bundles and discounts, and warns on schedule clashes. Maintains deep links and cart state across sessions and devices, and records attribution to the originating class for analytics. Respects inventory and waitlist rules, and blocks checkout if prerequisites cannot be met by the target class date.

Acceptance Criteria
One-Tap Add from Class Page When Prerequisite Is Missing
Given a user views a class page and the Fit Check determines a required prerequisite is missing And the class has a mapped prerequisite SKU (course or package) When the user taps the contextual "Add prerequisite" CTA Then the correct prerequisite SKU is added to the cart as a single line item And the originating class ID, date/time, and location are stored with the cart context And the user remains on the class page and sees a success confirmation within 500 ms And the CTA updates to indicate the item was added with an option to View Cart And if the user already satisfies the prerequisite, the CTA is not rendered
Auto-Select Earliest Eligible Prerequisite Session
Given the prerequisite requires enrollment in a scheduled session And the target class has a specific date/time and location When the user taps the "Add prerequisite" CTA Then the system preselects the earliest available prerequisite session that occurs before the target class start time And the selected session does not overlap with any of the user's existing bookings And the selected session matches the target class location when specified, otherwise matches the user's saved location preference And if multiple sessions qualify, the one with the earliest start time is chosen; ties are resolved by shortest distance to the target class location And the selected session's date, start time, and location are shown in the cart
Handle Schedule Clashes and Block When No Eligible Session Exists
Given some prerequisite sessions conflict with the user's existing bookings When the user taps the "Add prerequisite" CTA Then conflicting sessions are excluded from consideration And if at least one conflict-free session exists before the target class, that session is auto-selected and added And if no conflict-free session exists before the target class date, no session is added and a blocking message explains the prerequisite cannot be met And checkout for the target class is disabled until the user selects a different class date or an eligible session becomes available
Support Bundles, Packages, and Discounts for Prerequisites
Given the prerequisite can be satisfied by multiple offerings (e.g., single intro course or intro package) When the user taps the "Add prerequisite" CTA Then the system evaluates eligibility and selects the least total-cost option considering active discounts, memberships, and promo codes And the correct item(s) are added to the cart with discounts applied and price reflected before taxes/fees And if overlapping items already exist in the cart, duplicates are prevented and quantities are consolidated And taxes and fees are recalculated consistently with pricing rules
Respect Inventory and Waitlist Rules for Prerequisite Sessions
Given the targeted prerequisite session has 0 available seats And waitlist is enabled for that session When the user taps the "Add prerequisite" CTA Then the user is added to the waitlist instead of adding a paid seat to the cart And the cart displays a non-purchasable waitlist indicator for the prerequisite and prevents checkout for the target class until the prerequisite is confirmed And if waitlist is disabled, the system shows "Full — choose another session" and no item is added And all inventory reservations and timeouts are honored
Persist Cart and Deep-Link Class Context Across Sessions and Devices
Given the user arrives via a deep link to a class page and taps the "Add prerequisite" CTA When the user signs in on another device or returns later Then the cart retains the prerequisite item and its selected session (if any) until purchase or explicit removal And the cart retains origin metadata (originating class ID, date/time, location, UTM/campaign) and associates it to checkout And returning from the cart via deep link navigates back to the originating class context
Record Attribution and Events for One-Tap Add to Cart
Given the user taps the "Add prerequisite" CTA Then an analytics event "prereq_add_clicked" is emitted with: origin_class_id, origin_class_datetime, prerequisite_sku, selected_session_id (if applicable), user_id or anon_id, location_id, UTM parameters, timestamp, idempotency_key And upon successful add or waitlist join, a corresponding event is emitted ("cart_item_added" or "waitlist_joined") with the same origin metadata And events are deduplicated per idempotency_key per 5 minutes And at least 95% of CTA taps have corresponding events within 60 seconds in production monitoring
Instructor Review Console & Notifications
"As an instructor, I want a simple console to review and approve eligibility documents so that qualified students can book quickly."
Description

Back-office console for instructors and admins to review submitted documents and eligibility requests, approve or reject with comments, request resubmission, and set auto-approval rules by issuer or template. Provides filtering, bulk actions, SLAs with reminders, and escalation paths. Integrates SMS/email to notify students on status changes using studio-branded templates with rate limiting and unsubscribe compliance. Includes role-based access control, mobile-responsive UI, detailed activity logs, and exportable reports for audits and operations.

Acceptance Criteria
Single Submission Review Decision
Given a pending eligibility submission is opened in the Review Console, When the reviewer selects Approve and enters an optional comment and reason code, Then the submission status updates to Approved, the associated prerequisite is marked Satisfied for the student, and the class paywall reflects eligibility within 2 seconds. Given a pending eligibility submission is opened, When the reviewer selects Reject and provides a mandatory reason code and a comment of at least 10 characters, Then the submission status updates to Rejected and the class paywall reflects ineligibility within 2 seconds. Given two reviewers act concurrently on the same submission, When one decision is saved, Then subsequent attempts are blocked with an Already Decided message and no duplicate decisions are recorded. Given a decision (approve or reject) is saved, Then an activity log entry records actor ID, role, timestamp, decision, reason code, free-text comment, and a hash of attached documents.
Request Resubmission Workflow
Given a reviewer deems a submission insufficient, When they select Request Resubmission and provide checklist items, a due date, and a message, Then the submission status updates to Resubmission Requested and the student receives a resubmission link. Given a student uploads new documents via the resubmission link, When the upload completes, Then the previous attempt is archived, the new attempt becomes the active version, and the SLA timer resets. Given the due date passes without a new upload, Then an automatic reminder is sent to the student and the item is flagged Past Due in the console. Given a resubmission is received, When a reviewer makes a decision, Then only the latest version can be approved/rejected, and earlier versions remain read-only.
Auto-Approval Rules by Issuer/Template
Given an admin with the appropriate role configures a rule (e.g., Issuer = American Red Cross AND Template = CPR-AED Adult AND Expiry Date > Today), When a matching submission is received, Then it is auto-approved within 60 seconds and marked as Auto-Approved in the activity log. Given auto-approval rules exist, When a reviewer chooses Manual Override on an auto-approved item, Then the system records the override with actor, reason, and timestamp, and updates the student’s eligibility accordingly. Given a new or edited rule, When Test Mode is run against the last 100 submissions, Then the system displays how many would have auto-approved/rejected without changing production data. Rules are versioned, disabled/enabled, and only users with Admin role can create, edit, or delete them.
Queue Management: Filtering, Sorting, and Bulk Actions
Given the Review Console is loaded, When filters are applied by status, prerequisite, issuer, template, date range, reviewer, SLA state, or flagged state, Then the list updates within 1 second and shows the count of matching items. Given a set of 1–200 selected submissions, When the reviewer performs a bulk Approve/Reject/Request Resubmission action and confirms, Then eligible items are processed and ineligible items are skipped with a per-item error reason and no partial data corruption occurs. Given a saved view is created with filters and sort order, When the user selects that view, Then the console restores the same configuration and results. Bulk actions include a dry-run summary indicating how many items will be affected and any conflicts before confirmation.
SLA Timers, Automated Reminders, and Escalation
Given a studio-defined SLA (e.g., 24 business hours and business hours calendar), When a submission enters Pending Review, Then an SLA countdown is shown and pauses outside defined business hours. Given a submission is 2 hours from SLA breach, Then the assigned reviewer receives a reminder notification and the item is labeled At Risk. Given an SLA breach occurs, Then the item is escalated to the designated escalation group, an escalation notification is sent, and the breach is recorded for reporting. SLA metrics are captured per studio and prerequisite: average time to decision, % within SLA, and # of escalations.
Student Notifications: Branded Templates, Rate Limiting, Unsubscribe
Given a submission’s status changes (Approved, Rejected, Resubmission Requested), When notifications are triggered, Then the student receives studio-branded email and/or SMS within 60 seconds, with dynamic fields (student name, class/prerequisite, next steps) rendered correctly. Rate limits are enforced per recipient: max 1 SMS per minute and 5 emails per hour; duplicates for the same event are deduplicated within a 10-minute window. Given a student has opted out of SMS or email, Then the system does not send on that channel, includes alternative channel if available, and records the suppression in the activity log. SMS supports standard opt-out keywords (STOP/UNSTOP/HELP); all messages include required compliance footer and an unsubscribe/manage-preferences link in email.
RBAC, Mobile-Responsive UI, and Audit Logging/Exports
RBAC: Only users with Reviewer or Admin roles can take decision actions; Viewers have read-only access; cross-studio access is denied with 403 responses and hidden UI controls. Mobile: The Review Console is usable at 320–768 px widths; critical actions (approve/reject/request resubmission/filter) are reachable without horizontal scroll; touch targets are ≥44 px; list virtualizes to keep Time to Interactive < 3s on a mid-tier mobile device. Audit: Every decision, rule change, notification send/suppress, and export is logged with event type, user ID, role, timestamp, IP, object IDs, and before/after deltas; logs are immutable and tamper-evident via checksum. Exports: Users with Admin role can export audit and operational reports (CSV/JSON) for a selected date range including counts by status, average decision time, SLA breach rate, and decisions by reviewer/prerequisite.
Auditability & Policy Transparency
"As a cautious first-time attendee, I want transparent requirements and decision reasons so that I trust the booking process and feel safe sharing documents."
Description

End-to-end audit trail capturing rule versions, evaluation inputs/outputs, reviewer decisions, timestamps, and notifications sent. Exposes a user-facing policy panel on class pages that explains prerequisites, acceptable proofs, review SLAs, expiry handling, and appeals, all customizable per studio. Provides downloadable logs for compliance, retention schedules, and consent tracking to build trust with first-time attendees and support dispute resolution.

Acceptance Criteria
Policy Panel Displays Studio-Specific Prerequisites
- Given a class has studio-configured policy items (prerequisites, acceptable proofs, review SLAs, expiry handling, appeals), when a prospective attendee opens the class page, then a Policy panel is visible above the Fit Check actions and lists all configured items with clear labels. - Given the studio has custom branding enabled, when the Policy panel renders, then it uses the studio theme and contains no ClassTap branding. - Given the user’s locale is available, when the Policy panel renders, then all strings are localized accordingly. - Given a class has no prerequisites, when the class page is opened, then the Policy panel shows “No prerequisites required” and hides irrelevant sections (acceptable proofs, appeals). - Given the viewport width is less than 768px, when the Policy panel renders, then sections are collapsible and keyboard/screen-reader accessible (ARIA roles/labels present; focus order preserved).
End-to-End Audit Trail for Fit Check Evaluation
- Given a user runs a Fit Check on a class, when eligibility rules are evaluated, then the audit log stores rule set version ID, input payload hash, evaluated outputs, evaluation timestamp, user ID, class ID, and studio ID. - Given an evaluation error occurs, when the Fit Check fails, then the audit log records an error code, a redacted stack-trace reference ID, and a request correlation ID. - Given multiple evaluations occur for the same user and class, when entries are written, then each entry is immutable and strictly ordered by timestamp (ISO 8601) with a monotonic sequence number. - Given a studio data retention period is configured (e.g., 180 days), when an entry exceeds the period, then it is purged automatically and a purge event with reason and timestamp is added to the meta-log.
Reviewer Decision Logging and Appeals
- Given a reviewer records a decision on an uploaded proof, when they submit, then the audit log stores reviewer ID, decision (approve/reject), reason code, optional comment, evidence links, timestamp, and the rule version applied. - Given a decision is appealed, when an appeal outcome is recorded, then the original decision remains unchanged and a new linked decision entry is added with relation type (appeal_of) and outcome. - Given a studio sets a review SLA (e.g., 48 hours), when a pending review exceeds the SLA, then an SLA breach event is logged and included in exports.
Downloadable Policy and Audit Logs Export
- Given a studio admin requests an export with a valid date range and scope (studio/class/user), when the export is generated, then a signed download is produced in CSV and JSON within 5 minutes containing rule version IDs, inputs (hashed), outputs, decisions, notifications, consent records, and timestamps. - Given PII redaction is enabled, when the export is generated, then fields marked as sensitive (email, phone, IP, document IDs) are masked or hashed using SHA-256 with salt. - Given the export is ready, when delivery occurs, then an email with a one-time expiring link (>=24 hours validity) is sent to the requester and an audit entry of the export activity is recorded. - Given invalid parameters are supplied (bad date range, unsupported scope), when the export is requested, then a 400 response with field-level validation errors is returned and no export job is created.
Consent Tracking for Proof Uploads and Reviews
- Given a user uploads proof to satisfy a prerequisite, when they reach the submission step, then an explicit consent checkbox referencing the studio’s data policy is displayed and unchecked by default; submission is blocked until checked. - Given the user submits with consent, when stored, then the audit log records consent text version ID, timestamp, user ID, IP address, and jurisdiction/country code. - Given the studio updates the consent text to a new version, when the same user attempts another upload, then they are re-prompted for consent mapped to the new version and the old consent remains in the log.
Notification History for Fit Check Events
- Given notification channels are enabled, when Fit Check events occur (submission received, approved, rejected, SLA reminder), then each SMS/email sent is logged with template ID, channel, recipient identifier, delivery provider response, final status, timestamp, and correlation ID. - Given a notification delivery fails, when retry policy applies, then each retry attempt and the final outcome are logged. - Given a user is unsubscribed from marketing, when notifications are transactional, then they are sent and logged with reason “transactional allowed”; when they are marketing, then they are suppressed and suppression reason is logged.

Pathway Tracker

Guided paths that turn prerequisites into a clear progress bar. Suggests the next available prerequisite sessions that fit the attendee’s schedule, supports “Add All” to book required steps at once, and applies bundle pricing when rules allow. Converts ineligible interest into committed enrollments while keeping capacity optimized.

Requirements

Prerequisite Graph & Path Definition
"As a studio admin, I want to define structured learning paths with prerequisites so that learners can see exactly which classes they must take and in what order."
Description

Model class prerequisites as directed paths with support for ordered sequences, co-requisites, equivalencies, expiration windows, minimum score requirements, and instructor/admin overrides. Provide an admin UI to compose and version pathways per tenant, attach steps to existing classes/sessions, and publish changes safely with draft/preview. Enforce eligibility checks at search, booking, and check-in time via shared validation services. Expose APIs to import/export paths, migrate legacy rules, and sync with external LMS/CRM identifiers. Log audit trails for edits, and surface clear failure reasons when a user is ineligible. Integrate with ClassTap scheduling to reflect capacity and avoid double-bookings across steps.

Acceptance Criteria
Admin composes, versions, previews, and publishes a pathway
- Given I am a tenant admin, When I create a new pathway with ordered sequences, co-requisites, equivalencies, expiration windows, and minimum score rules, Then I can attach each step to existing classes/sessions and save as Draft. - Given a Draft pathway, When I save changes, Then the pathway version increases (e.g., v1 -> v2) and the Draft does not affect eligibility checks for end users. - Given a Draft pathway, When I open Preview with a selected learner profile and date range, Then I see eligibility outcomes and conflicts exactly as the validation service would produce if published. - Given a Draft pathway, When I click Publish, Then the system blocks publish if structural validation fails (no cycles, all references valid, no orphaned nodes) and surfaces specific errors. - Given validation passes, When I publish, Then the new version becomes Active, the prior Active version is retained as Read-only history, and all subsequent validations use the Active version. - Given any create/edit/publish action occurs, When I view audit logs, Then each event records actor, timestamp, change summary (before/after), and version, and is immutable. - Access control: Only admins of the tenant can create/edit/preview/publish pathways; others receive 403.
Eligibility validation at search, booking, and check-in
- Given an end user browsing classes, When search results are returned, Then each class requiring prerequisites shows eligibility status (Eligible/Ineligible/Eligible with Co-req) with a reason code and human-readable message. - Given the same user, When attempting to book, Then the validation service enforces ordered sequences, co-requisites, equivalencies, expiration windows, minimum score thresholds, capacity availability, and time-conflict checks across already-booked sessions. - Given any rule is not satisfied, When booking is submitted, Then the request is rejected with a domain error (e.g., 409) including a stable error code and failure reason, and (if available) suggested next eligible sessions. - Given a successful booking, When check-in occurs, Then the system re-validates time-bound rules (expiration windows, minimum scores) and prevents check-in if newly ineligible, logging the reason. - Given co-requisites are required, When only one of the co-req pair is selected, Then the UI/API requires simultaneous selection or blocks booking with a specific co-requisite failure code.
Instructor/Admin override with audit trail
- Given I have override permission, When I apply a prerequisite override for a learner and target session/pathway step, Then the system records an audit entry with actor ID, role, timestamp, target IDs, reason text, scope (single session vs entire path), and optional expiry date. - Given an active override, When the learner searches, books, or checks in, Then eligibility validations honor the override within its scope and expiry. - Given an override is revoked or expires, When validations run, Then the override is no longer applied and subsequent actions follow standard rules. - Given audit reporting is requested, When I export audit logs for a date range, Then all override events (create/update/revoke) are included and immutable. - Given a booking/check-in affected by override, When staff view the record, Then the UI clearly indicates that an override was applied with the recorded reason.
Path import/export and external ID synchronization APIs
- Given a valid API token, When I POST a pathway definition that conforms to the published schema (including external LMS/CRM identifiers for steps), Then the system creates or updates a Draft pathway and returns the new pathway ID and version. - Given existing pathways, When I GET an export for a pathway, Then I receive the Active version with full graph, rule metadata, and external identifiers, suitable for re-import without loss. - Given a legacy rules file, When I call the migration endpoint, Then the system translates supported rules to the new schema and returns a report listing created entities, unmapped items, and warnings. - Given an idempotency key header is supplied, When I retry the same import, Then no duplicate pathways or steps are created and the same response idempotency key is echoed. - Given references to unknown external IDs are provided, When import runs, Then the request fails with a 422 and a machine-readable list of invalid references.
Equivalency, expiration, and minimum score enforcement
- Given a pathway step A has equivalencies {B,C} and an expiration window of 180 days with a minimum score of 80, When the learner has completed B 120 days ago with score 85, Then A is satisfied. - Given the same rules, When the learner completed C 200 days ago with score 95, Then A is not satisfied and the failure code is EXPIRED with an expiry date in the payload. - Given the same rules, When the learner completed B 30 days ago with score 75, Then A is not satisfied and the failure code is MIN_SCORE_NOT_MET with the recorded score included. - Given multiple equivalent completions exist, When evaluating, Then the highest valid (non-expired) completion is used and evaluation time is logged for traceability.
Capacity reflection and double-booking prevention across pathway steps
- Given a learner uses Add All to book multiple required steps, When the booking is submitted, Then capacity is reserved atomically across all selected sessions and the transaction succeeds only if all steps can be booked. - Given any selected step is full or conflicts in time with another selected or already-booked session, When submitting Add All, Then the entire transaction fails with specific error codes (e.g., FULL, TIME_CONFLICT) and no partial bookings are created. - Given a learner attempts to book two pathway steps scheduled at overlapping times across different classes, When submitting, Then validation blocks the second booking and returns a time-conflict reason. - Given capacity is modified externally during the flow, When the user retries Add All, Then the system re-evaluates availability in real time and returns updated outcomes without creating duplicates.
Dynamic Progress Bar & Eligibility Indicator
"As an attendee, I want to see a clear progress bar and eligibility status on each class page so that I understand what I’ve completed and what I need to do next."
Description

Render a real-time, accessible progress bar on class and pathway pages that reflects completed, in-progress, and locked steps, with tooltips explaining lock reasons (e.g., unmet prerequisite, expired completion). Display remaining steps, estimated time/cost to completion, and support multiple concurrent pathways per learner. Make components mobile-first, themeable to white-label brands, and localizable. Link each step to detail pages and completion artifacts (attendance, assessments). Consume eligibility state from the prerequisite service and update instantly on new enrollments or cancellations.

Acceptance Criteria
Real-time Progress Bar Status Rendering
- Given a learner with a pathway containing completed, in-progress, and locked steps, When the pathway page loads, Then the progress bar renders segments with text labels that accurately reflect each step status and order. - Given eligibility state is retrieved from the prerequisite service, When rendering, Then all steps with eligibility=false are shown as Locked with their associated reason codes mapped to statuses. - Given the learner completes enrollment in a prerequisite step, When the enrollment confirmation event is emitted, Then the progress bar updates affected steps within 2 seconds (p95) without a full page reload. - Given the learner cancels an enrollment or a completion expires, When the cancellation/expiration event is received, Then dependent steps switch to Locked within 2 seconds (p95) and the change is visually indicated. - Given the eligibility service is unavailable, When rendering, Then the component shows a retryable non-blocking error state and does not display stale or misleading progress.
Eligibility Tooltip and Lock Reason Disclosure
- Given a step is Locked, When the user hovers, focuses via keyboard, or taps the step indicator/lock icon, Then a tooltip/popover appears within 150ms showing a human-readable lock reason derived from the prerequisite service reason code. - Given multiple lock reasons apply, When the tooltip opens, Then all reasons are listed with brief actionable guidance and a link to the required prerequisite step(s). - Given the page locale, When tooltip text renders, Then all strings are localized via i18n keys and numbers/dates are formatted per locale. - Given the user presses Escape, taps outside, or shifts focus, When interacting, Then the tooltip closes and focus returns to the triggering element. - Given a step is not Locked, When activated, Then no tooltip is shown and the step proceeds to navigation or booking.
Remaining Steps and Estimated Time/Cost Calculation
- Given a pathway with defined step durations and prices, When rendering, Then the UI displays: remaining step count, total estimated time (sum of remaining durations), and estimated cost using active pricing rules. - Given bundle discounts, caps, or promos apply, When calculating, Then the estimate applies eligible rules and displays pre-discount and post-discount amounts with currency formatted to the user locale. - Given the learner books or cancels a remaining step, When state changes, Then remaining count, time, and cost recalculate within 2 seconds (p95) and update without full reload. - Given a previously completed step expires, When the expiration time is reached, Then the metrics recalculate and the step returns to the remaining set. - Given a step lacks duration or price, When calculating totals, Then the UI excludes it from sums, shows N/A for the missing field, and provides an info tooltip explaining the omission.
Multi-Pathway Support per Learner
- Given a learner is enrolled in multiple pathways, When viewing their dashboard or relevant class/pathway page, Then separate progress bars render per pathway with clear labels and no cross-contamination of state. - Given the user switches pathway context using a selector, When switched, Then the progress bar, eligibility, remaining metrics, and links update to the selected pathway without a full page reload. - Given two pathways share steps, When a shared step is completed, Then both pathways reflect the completion consistently and dependent locks update accordingly. - Given up to 5 pathways are present, When rendering, Then initial content paint completes within 2.5s (p95) on a 4G network and interaction latency remains under 150ms.
Accessible and Mobile-First Interaction
- Given a keyboard-only user, When navigating the component, Then all interactive elements are reachable in a logical order with visible focus indicators and actionable via Enter/Space. - Given a screen reader user, When the component renders or updates, Then the progress is exposed with appropriate ARIA semantics (e.g., role="progressbar" or list with status labels), includes aria-valuenow/valuemin/valuemax, and dynamic updates are announced via aria-live without forcing focus changes. - Given color is not a reliable indicator, When communicating statuses, Then the UI also provides text labels or icons/patterns and maintains WCAG 2.1 AA contrast (≥4.5:1 for text, ≥3:1 for large text and non-text contrast). - Given a mobile viewport ≤ 375px width, When rendering, Then the component adopts a vertical/condensed layout, touch targets are ≥ 44x44 px, and tooltips are accessible via tap with clear dismissal controls.
White-Label Theming Compliance
- Given tenant theme tokens (colors, fonts, radii) are configured, When the component renders, Then it applies the tenant theme via CSS variables without inline overrides and persists per tenant domain. - Given extreme brand color combinations, When applied, Then the component preserves WCAG AA contrast or automatically adjusts/surfaces warnings if tokens violate contrast thresholds. - Given switching between tenants A and B, When navigating, Then no style bleeding occurs and the correct theme loads within 200ms of route change. - Given an admin updates theme tokens, When saved, Then the component reflects changes on next render and updates in-session without full reload.
Step Links to Details and Completion Artifacts
- Given an unlocked step, When the user activates the step segment or label, Then the app navigates to the step detail route with correct context (pathwayId, stepId) and returns to the same scroll position on Back. - Given a completed step with attendance or assessment artifacts, When the user selects "View proof" or the step, Then the artifact opens with correct permissions and audit logging, and download/print options are available if allowed. - Given a locked step lists prerequisites, When the user selects "Go to prerequisite", Then they are routed to the prerequisite detail or earliest eligible session booking page. - Given a link target is unavailable, When navigation fails, Then a non-destructive error is shown with a retry and a fallback link to the pathway overview.
Schedule-Aware Next-Step Suggestions
"As an attendee, I want the system to suggest the next prerequisite sessions that fit my schedule so that I can enroll without manually searching."
Description

Recommend the next available prerequisite sessions that satisfy eligibility and align with the attendee’s preferences (time windows, locations, modality, travel radius) and timezone. Filter out clashes with existing bookings and personal calendar blocks, consider transit buffers, and rank options by earliest completion or user preference. Provide fallback suggestions (nearby locations, alternative dates, equivalent classes) when ideal slots are full, and support quick actions to join waitlists. Expose a lightweight API to power suggestions in booking pages, emails, and SMS.

Acceptance Criteria
Suggest Next Eligible Sessions Within User Preferences
Given a user has completed pathway steps up to N and has preferences for time windows (Mon–Fri 18:00–21:00), modality (in-person and online), preferred location (Downtown Gym), travel radius 10 miles, and timezone America/Los_Angeles When the user requests next-step suggestions for pathway P Then return only sessions that satisfy step N+1 or an allowed equivalent for pathway P And ensure all session times fall within the user’s preferred time windows in the user’s timezone And ensure all in-person sessions are within 10 miles of the preferred location (online sessions are distance-eligible by default) And ensure suggested sessions match the selected modalities And return at most 10 suggestions (or fewer if fewer are eligible)
Exclude Clashing Sessions Using Calendar and Transit Buffers
Given the user has existing bookings and personal calendar blocks synced and a transit buffer of 20 minutes configured And the user has a booking ending at 17:30 at Location A and a calendar block from 19:00 to 20:00 When generating next-step suggestions Then exclude any session starting before 17:50 at Location A And exclude any session at a different physical location starting before 18:15 if estimated travel time from Location A is 25 minutes (travel time + buffer must fit between events) And exclude any session that overlaps the 19:00–20:00 calendar block, applying buffer on both sides (i.e., disqualify sessions starting before 20:20 or ending after 18:40) And ensure no returned suggestion violates any booking, calendar, or buffer constraint
Rank Suggestions by Earliest Completion and User Preferences
Given multiple eligible sessions exist across dates and locations for the remaining pathway steps When ranking suggestions Then compute a predicted pathway completion date for each candidate assuming the earliest feasible sequence for remaining steps starting with that candidate And rank primarily by earliest predicted completion date (earlier is higher) And rank secondarily by a weighted match score: time-window match 40%, distance (closer is better) 30%, modality match 20%, preferred days 10% And break ties by earliest start time, then lexicographic sessionId for deterministic order And include a rankingScore field in the response for each suggestion in the range [0.0, 1.0]
Provide Fallback Options and Waitlist Actions When Ideal Slots Are Full or Unavailable
Given no sessions fully satisfy preferences or all ideal sessions are at capacity When generating next-step output Then include up to 5 fallback options labeled by category: Nearby (expand radius to 1.5x), Later Date (extend horizon up to 6 weeks), Equivalent (allowed equivalency group) And do not include sessions at capacity unless waitlistEligible = true And include joinWaitlistUrl for each waitlist-eligible session that enrolls the user with a single tap/click And present fallback options after primary suggestions and clearly marked with their category
Suggestion API Response Contract and Performance
Given a GET request to /v1/suggestions/next-step with valid auth and parameters (userId, pathwayId, tz, preferences) When the request is processed Then respond with HTTP 200 within 500 ms p95 and 1500 ms p99 for payloads ≤ 100 suggestions at 50 RPS And return application/json UTF-8 containing: suggestions[] and fallbackOptions[] arrays And each suggestion includes: sessionId, stepId, startAt (ISO8601 with timezone), endAt (ISO8601 with timezone), timezone, modality, location {name, lat, lng}, distanceKm (if in-person), rankingScore, category ("Primary"), capacity {total, available}, waitlistEligible (bool), bookingUrl, joinWaitlistUrl And order suggestions deterministically by ranking And return appropriate errors: 400 (invalid params), 401/403 (auth), 429 (rate limit), 500 (server error) with machine-readable error codes
Timezone and Localization Correctness Across Channels
Given a user with timezone Europe/Berlin and locale de-DE receives suggestions on booking page, email, and SMS while sessions are stored in UTC When generating content for each channel Then convert and display all session times in Europe/Berlin with correct DST handling And include tz=Europe%2FBerlin in booking and waitlist deeplinks, preserving sessionId and pathwayId And format dates/times using the user’s locale (e.g., 24-hour clock) where channel supports localization And ensure the set and order of suggestions are identical across channels for the same inputs
Respect Capacity and Prevent Double-Booking
Given capacity can change and the user may have pending cart items or held reservations When generating suggestions and the user proceeds to book via a suggestion Then exclude sessions with available = 0 unless waitlistEligible = true And suppress suggestions that overlap with items currently in the user’s cart or held reservations And if availability changes between suggestion and checkout, surface a clear capacity-changed error and refresh suggestions within 2 seconds And ensure no two suggested sessions for the same pathway step overlap in time
Add-All Bulk Booking with Conflict Resolution
"As an attendee, I want to add all required steps to my cart at once so that I can check out quickly without conflicts."
Description

Enable an “Add All” action to place all required steps for a pathway into the cart in a single flow, performing atomic eligibility checks, capacity holds, and payment authorization. Detect conflicts (time overlaps, location constraints, instructor caps) and provide guided alternatives (swap session, split across dates) before checkout. Reserve seats with expirations, prevent double-bookings across pathways, and commit all bookings in one transaction with rollback on failure. Support mixed modality sessions, attendee-specific pricing, and per-tenant policies for deposits or installments.

Acceptance Criteria
Atomic Add All Booking and Rollback
Given an attendee selects "Add All" for a pathway with N required steps and is eligible for each selected session When the Add All flow is initiated Then eligibility for every step is validated atomically And capacity holds are placed for all steps And a single payment authorization for the total amount is requested And all steps are added to the cart as a single operation Given any step fails eligibility, capacity hold, or payment authorization When the Add All flow executes Then no seats remain held for any step And no bookings are created And the cart remains unchanged And an actionable error lists the failed steps with reasons Given a transient error occurs during the Add All flow When the user retries within the same session Then the operation is idempotent and does not duplicate holds or cart lines And the outcome is consistent with a single successful attempt
Conflict Detection and Guided Alternatives
Given the selected steps include time overlaps, location transfer constraints, or instructor capacity caps When the user clicks Add All Then all conflicts are detected before checkout and displayed per step with reason codes Given conflicts are detected When the system proposes alternatives Then for each conflicted step, the next available sessions that satisfy pathway order and constraints are suggested when available And options to swap a session, split the pathway across dates, or remove a step are presented Given the user selects guided alternatives When the revised set is confirmed Then the system re-validates eligibility and capacity atomically And the user cannot proceed to checkout until all conflicts are resolved
Seat Reservation Holds and Expiration
Given Add All is initiated and capacity is available When holds are placed Then each seat hold uses the tenant-configured hold duration And a countdown timer is shown per held session in the cart And available capacity reflects the holds immediately Given the hold duration elapses before checkout When the timer expires Then all associated holds are released And cart lines are marked expired with an option to re-validate And availability is restored Given checkout completes before expiration When payment is authorized Then holds convert to confirmed bookings without capacity oversell
Cross-Pathway Double-Booking Prevention
Given the attendee has existing bookings or active holds in the same time window from any pathway When Add All is attempted Then double-bookings are blocked And conflicting items are listed with options to swap to non-conflicting sessions Given the same session is included multiple times across pathways or browser tabs When Add All is executed Then duplicates are prevented at validation time And only a single cart line and hold exist per session per attendee Given the attendee resolves conflicts When validation is rerun Then the system allows checkout only if no time conflicts remain across all active holds for the attendee
Mixed Modality and Location Constraints
Given a pathway includes in-person, online, and hybrid sessions When Add All is validated Then travel-time/location constraints are applied only to in-person segments And time zone is derived from each session's location and respected in conflict checks Given a hybrid session with a defined in-person component When conflicts are evaluated Then constraints use the in-person component's time/location for travel checks Given online-only sessions adjacent to in-person sessions When conflicts are evaluated Then no location constraint is applied to the online segment And time overlap rules still apply
Attendee-Specific Pricing, Bundles, and Tenant Policies
Given the attendee meets bundle rules and all required steps are in the cart When pricing is calculated Then bundle pricing is applied once per eligible bundle And line-item and order totals reflect the discounted amount Given attendee-specific pricing tiers are configured When the cart is priced Then each line uses the attendee’s applicable tier price And taxes and fees are applied per tenant policy Given the tenant enables deposits or installments When the attendee selects a payment plan Then the payment authorization equals the required deposit or first installment And the schedule of remaining payments is displayed And the sum of installments equals the total price including taxes and fees
Single Transaction Commit and Idempotency
Given all items are valid and the attendee confirms checkout When the system commits the order Then all bookings are created in a single transaction And if any booking creation fails, all are rolled back And any payment authorization or capture is voided or reversed accordingly Given the attendee double-submits or the network retries When the request is received with the same idempotency key Then only one order is created And subsequent retries return the original result without creating duplicates Given a successful commit When post-commit processes run Then a single order reference is generated with per-session booking references And confirmation notifications are sent for all booked sessions
Bundle Pricing & Rule Application
"As a studio owner, I want bundle pricing to apply automatically when I book a pathway so that the total cost reflects pathway discounts."
Description

Apply pathway-specific bundle pricing when eligible steps are booked together or within a configured window. Support fixed-discount, percentage, and tiered pricing models; handle taxes, fees, promo codes, and membership entitlements. Display transparent savings breakdowns in cart, receipts, and reports. Recalculate on reschedules/cancellations with prorations and rule re-evaluation. Provide an admin rule builder with previews and guardrails to avoid stacking conflicts. Export pricing outcomes to finance reports and reconcile with payment providers.

Acceptance Criteria
Checkout: Apply Bundle Pricing for Multiple Pathway Steps
Given a shopper adds multiple eligible steps from the same pathway to the cart in a single session And a bundle rule exists for those steps with model fixed, percentage, or tiered When the shopper proceeds to checkout Then the system applies exactly one applicable bundle rule per item based on rule priority And calculates the discounted subtotal per item and order And shows the calculated savings amount and percentage where applicable And leaves ineligible items at standard price
Windowed Eligibility: Retroactive Bundle Application Across Separate Bookings
Given a customer books at least one eligible pathway step and pays standard or partially discounted price And a bundle rule defines a window to complete required steps When the customer books remaining qualifying steps within the configured window Then the system retroactively applies the bundle pricing to all qualifying steps And issues automatic prorated refund or additional charge to align to the bundle price And emails an updated receipt with line-level savings breakdown And records an audit entry of the retroactive adjustment
Pricing Precedence and Stacking Guardrails
Given an order contains eligible pathway items, an active membership entitlement, and a promo code And at least one bundle rule would apply When pricing is calculated Then apply membership entitlements first, then bundle pricing, then promo codes And compute taxes and fees on the post-discount amounts per tax configuration And block promo codes marked no-stack when a bundle discount is present And prevent the final payable from dropping below zero And persist a log of applied rule IDs and calculation steps
Transparent Savings Breakdown in Cart, Receipts, and Reports
Given discounts and charges have been calculated for an order When the shopper views the cart and checkout summary Then display per-item original price, discount type (bundle/membership/promo), discount amount, taxable amount, tax, fees, and final item total And display order-level total savings and final payable When the receipt is emailed or downloaded Then include the same breakdown and the rule IDs or names applied When viewing revenue and finance reports Then expose columns for base price, discount by source, taxes, fees, net, and savings with consistent rounding
Reschedules/Cancellations: Recalculation and Proration
Given an order received bundle pricing and one step is rescheduled or canceled When the change is committed Then re-evaluate bundle eligibility for the remaining items against current rules And if the bundle is no longer eligible, recalculate prices and collect the incremental amount or issue a prorated refund according to policy And if eligibility remains, preserve bundle pricing and adjust only affected items and dates And sync adjustments to the payment provider via partial capture/void/refund where supported, otherwise create a credit balance And send an updated receipt and store an audit trail with before/after prices
Admin Rule Builder: Create, Preview, and Conflict Prevention
Given an admin creates or edits a pathway bundle rule When defining discount model (fixed/percentage/tiered), thresholds, eligible classes, time window, stacking flags, priority, and activation schedule Then the UI validates required fields and value ranges and prevents overlapping rules that target the same items with incompatible stacking settings And provides a live preview that simulates pricing for a sample cart and shows expected savings and taxes And supports draft, test-mode, and publish states with effective date/time And requires a unique rule name/ID and records version history
Finance Export and Payment Provider Reconciliation
Given orders with bundle pricing, memberships, promos, taxes, fees, reschedules, and refunds exist When exporting finance data for a date range Then include per-transaction fields: order ID, customer, item IDs, base amounts, discount amounts by source (bundle/membership/promo) with rule IDs, taxes, fees, net, tender type, payment provider transaction IDs, settlement dates, and refund references And totals in the export match provider settlements within a 0.01 currency unit tolerance per day and per batch And discrepancies beyond tolerance are flagged in a reconciliation report with itemized differences
Cross-Step Waitlist Orchestration
"As an instructor, I want the waitlist to coordinate across pathway steps so that students can move into open seats without breaking their sequence."
Description

Coordinate waitlists across all remaining pathway steps so that when a seat opens in one step, downstream bookings remain viable. Offer pathway-aware waitlist enrollment, prioritized holds, and automatic multi-step seat allocation when a compatible chain becomes available. Notify users via SMS/email with consolidated prompts to confirm or adjust multiple steps at once. Respect capacity, hold expirations, and instructor overrides, and avoid creating stranded enrollments that break sequence.

Acceptance Criteria
Pathway-Aware Waitlist Enrollment
Given a user is viewing a pathway with remaining required steps for which they meet prerequisites When the user selects “Join Waitlist” from the pathway tracker Then the system enrolls the user onto the waitlists for all remaining eligible steps in sequence via a single action And records the user’s schedule constraints, location/modality preferences, and blackout dates for allocation use And displays the user’s per-step waitlist positions and a unified pathway waitlist status And prevents waitlist enrollment on any step where prerequisites are unmet or the user already has a confirmed booking
Chain Viability Validation Before Allocation
Given seats may be available or become available across multiple remaining steps in a pathway When the system evaluates a potential allocation chain for a candidate Then it verifies prerequisite order, chronological feasibility, and absence of time overlaps across all steps And verifies configured travel buffers, instructor blackout dates, modality compatibility, and location constraints And verifies there are no conflicts with the user’s existing bookings across all calendars And rejects any chain that would result in a stranded or out-of-order enrollment and returns structured validation reasons
Atomic Multi-Step Hold Placement
Given a compatible chain is identified for the highest-priority candidate When the system attempts to reserve seats Then it places time-bound holds on all sessions in the chain atomically And if any single hold cannot be placed, all holds are rolled back with no partial reservations And held seats decrement visible availability and are labeled as “On Hold” to other users And each hold stores an expiration timestamp derived from a configurable hold duration
Consolidated Confirmation and Adjustments
Given multi-step holds exist for a candidate When the system notifies the candidate Then it sends a single consolidated SMS and email containing step names, dates/times, locations, prices, and the hold expiration And provides Confirm and Adjust links that deep-link to a consolidated confirmation screen And clicking Confirm within the hold window converts all holds to confirmed bookings in one transaction and sends a single confirmation receipt And no payment is captured before confirmation; upon confirmation, payment is authorized/captured per configured policy And clicking Adjust allows schedule-compatible substitutions that preserve a viable chain prior to confirmation; invalid adjustments are blocked with reasons And declining releases all holds immediately
Hold Expiration and Automatic Reallocation
Given a candidate does not confirm before the hold expiration When the hold window elapses Then the system automatically releases all holds and restores capacities And notifies the candidate that their holds expired with a link to rejoin or update preferences And offers the same chain (or next best viable chain) to the next eligible candidate by priority within the configured SLA And prevents immediate reallocation back to the same candidate unless their status or constraints have changed
Priority Rules and Instructor Overrides
Given waitlist priority is determined by defined rules (e.g., pathway progress, timestamp, membership, and fairness windows) When an instructor or staff applies an authorized override to promote, demote, or directly allocate a candidate Then the override updates the cross-step priority calculation and is applied on the next allocation run And direct allocation still enforces chain viability checks unless a force-override is explicitly chosen by a user with proper permissions And all overrides are permission-gated and audited with actor, timestamp, and reason
Concurrency, Capacity, and Audit Integrity
Given multiple seats open and multiple candidates are evaluated concurrently When allocation and hold placement execute Then no session capacity is exceeded, and each seat is assigned to at most one active hold or booking And a candidate cannot receive duplicate holds for the same step And race conditions are resolved deterministically so only one candidate wins a given chain And all holds, confirmations, expirations, overrides, notifications, and errors are logged with correlation IDs to enable end-to-end traceability

Timed Override

Permissioned, auditable overrides for edge cases. Managers can grant a one‑time or time‑boxed exception with a reason code, scope it to a specific class or series, and set auto‑expiry. Rosters display a subtle badge and follow‑up tasks (e.g., “Upload cert by 09/30”). Delivers flexibility without sacrificing control or compliance.

Requirements

Override Creation & Access Control
"As a studio manager, I want to create a one-time, permissioned override with a reason and scope so that I can handle justified exceptions without weakening our overall policies."
Description

Provide a manager-only workflow (UI and API) to create permissioned overrides with one-time or time-boxed duration, capturing reason code, scope, notes, and optional follow-up tasks. Enforce role-based access control (RBAC) so only authorized roles can create, edit, or revoke overrides within their tenant. Integrate with ClassTap’s booking and roster services to persist overrides as first-class entities with unique IDs and tenant isolation. Validate input (dates, scope, conflicts) and prevent creation of overrides that violate global policies (e.g., hard legal limits). Ensure overrides are traceable, editable until activation, and revocable with immediate effect.

Acceptance Criteria
Manager creates one‑time override for a class
Given an authenticated Manager with Override:Create permission in tenant T And a class C in tenant T with a scheduled session at time S When the Manager submits an override with type=one-time, scope=class C@S, reasonCode=R, optional notes N, and optional followUpTasks F Then the system validates inputs and creates override O with a unique ID, tenantId=T, scope=class C@S, type=one-time, reasonCode=R, notes=N, followUpTasks=F, status=Scheduled or Active based on current time And O is persisted as a first-class entity retrievable via UI and API And the API responds 201 Created with O.id and canonical fields; the UI shows a success state referencing O.id And an audit entry records creation with actor, timestamp, and fields captured
Time‑boxed override auto‑expiry
Given a time-boxed override O in tenant T with start=Ts and end=Te And current time < Te When current time reaches Te Then O transitions to status=Expired automatically within 60 seconds without manual action And subsequent attempts to use O are rejected with error code OVERRIDE_EXPIRED And an audit entry records the status change with timestamp and system actor
Booking flow honors an active override
Given an active override O scoped to class C that permits an otherwise blocked booking condition And a member M attempts to book class C within O's valid window When the booking request includes overrideId=O.id and satisfies O's scope and constraints Then the booking service confirms booking B and persists B.overrideId=O.id And the roster service exposes B.overrideId via its API for the attendee record And an audit entry links booking B.id to override O.id
RBAC enforcement for override actions
Given users U1 (Manager with Override:Create/Edit/Revoke) and U2 (role without override permissions) in tenant T, and user U3 (Manager in tenant T2) When U1 performs create, edit, or revoke on an override in tenant T Then the action succeeds subject to validations When U2 attempts any override create/edit/revoke in tenant T Then the API returns 403 Forbidden with error code RBAC_DENIED When U3 attempts to access or modify overrides in tenant T Then the API returns 403 Forbidden and no tenant T data is exposed
Input validation for dates, scope, and conflicts
Given a Manager submits an override with start=Ts and end=Te for class C in tenant T When Ts >= Te Then the API returns 422 Unprocessable Entity with error code INVALID_TIME_RANGE When class C does not exist or is not owned by tenant T Then the API returns 404 Not Found with error code SCOPE_NOT_FOUND When an existing active or scheduled override conflicts on the same scope with incompatible parameters Then the API returns 409 Conflict with error code OVERRIDE_CONFLICT including the conflicting override IDs
Global policy guardrails are enforced
Given a Manager attempts to create an override that would violate a global policy P (e.g., statutory capacity or legal age limit) When the request is submitted Then the API returns 422 Unprocessable Entity with error code GLOBAL_POLICY_BLOCKED and a reference to policy P And no override entity is created And the audit log records the denied attempt with actor, reasonCode, and policy reference
Editable until activation; revocation has immediate effect
Given an override O with start=Ts in the future When a Manager edits O before Ts to change allowed fields (reasonCode, notes, followUpTasks, window, scope within tenant T) Then the API returns 200 OK and O is updated; an audit trail records who, when, and what changed When current time >= Ts and O is Active Then any edit attempt is rejected with 422 INVALID_STATE and O remains unchanged When a Manager revokes O at any time Then O transitions to status=Revoked within 60 seconds and becomes unusable for new bookings And booking and roster services stop honoring O for subsequent operations And an audit entry records the revocation with actor and reason
Time-Boxing & Auto-Expiry
"As an operations lead, I want overrides to automatically expire at a set time so that exceptions don’t persist longer than intended."
Description

Implement a scheduling subsystem that activates overrides at start time and auto-expires them at end time, with idempotent jobs and recovery on service restarts. On expiry, re-enforce standard rules (capacity, prerequisites, payment) and trigger configured notifications to stakeholders. Support immediate start (one-time) and future-effective windows, with safeguards against backdating beyond audit policy. Expose status states (Scheduled, Active, Expired, Revoked) and update dependent systems (roster, waitlist, check-in) in real time.

Acceptance Criteria
Immediate One-Time Override Activation
Given a manager with override permissions selects "Immediate start" for a specific class and sets an end time in the future When the override is saved Then the override status is set to "Active" within 2 seconds and the effective window is persisted And roster, waitlist, and check-in engines apply the override scope within 5 seconds of activation And a roster badge "Override Active" with the configured follow-up task is displayed within 5 seconds And a start notification is delivered to configured stakeholders within 60 seconds of activation And start job execution is idempotent; if retried, the state remains "Active" and only one notification set is sent
Future-Effective Window Scheduling and Auto-Expiry
Given Tstart > now and Tend > Tstart and a valid scope (class or series) When the override is saved Then the status is "Scheduled" and a single start job is scheduled When current time reaches Tstart Then the status transitions to "Active" within 2 seconds, scope is applied within 5 seconds, and the roster badge appears When current time reaches Tend Then the status transitions to "Expired" within 2 seconds, the badge is removed within 5 seconds, and the override scope is removed And an expiry notification is delivered to configured stakeholders within 60 seconds And job executions are idempotent; retries do not create duplicate state changes or notifications
Service Restart Recovery for Overrides
Given one or more overrides in "Scheduled" or "Active" state and the service restarts When the service is back up Then pending start/expiry jobs are rehydrated from durable storage within 10 seconds If now is between Tstart and Tend Then the status is "Active" and scope is applied within 5 seconds of restart If now > Tend Then the status is set to "Expired" within 2 seconds of restart and standard rules are enforced within 5 seconds And any required start/expiry notifications are sent exactly once; duplicates are suppressed across retries
Backdating Safeguards & Audit Compliance
Given an audit policy defining maxBackdateMinutes = M When a manager attempts to save an override with Tstart < now - M Then the save is blocked with error code "BACKDATE_NOT_ALLOWED" and no override is created And the rejected attempt is recorded in the audit log with user, timestamp, proposed window, reason code, and outcome "Rejected" When Tstart is within the allowed backdate window Then the override is saved, the action is logged with outcome "Approved", and the status reflects the correct state based on now When Tend <= Tstart Then the save is blocked with error code "INVALID_WINDOW"
Rule Re-Enforcement on Expiry
Given an "Active" override that bypasses capacity, prerequisites, or payment When the override expires (Tend reached or revoked) Then standard capacity, prerequisite, and payment rules are re-applied within 5 seconds And if capacity is exceeded due to the override Then the most recently admitted attendee(s) are moved to the waitlist within 5 seconds, per standard ordering rules, and are notified within 60 seconds And if prerequisites are unmet Then affected bookings are set to "Hold - Prereq" with check-in disabled and a follow-up task created within 5 seconds And if payment is outstanding Then affected bookings are set to "Unpaid" and a payment request is sent within 60 seconds And the roster badge is removed within 5 seconds of expiry
Early Revocation of Overrides
Given an override in "Scheduled" or "Active" state When a manager revokes it with a reason code Then the status becomes "Revoked" within 2 seconds and all scheduled jobs for that override are cancelled And override effects are removed within 5 seconds; roster, waitlist, and check-in reflect the change And a revocation notification is delivered to configured stakeholders within 60 seconds And the revocation is idempotent; repeated revoke actions neither change the final state nor duplicate notifications And an audit log entry records revoker identity, timestamp, reason code, and prior state
State Exposure and Real-Time Propagation
Given any state change among ["Scheduled","Active","Expired","Revoked"] When the change is committed Then the UI roster, waitlist, check-in, and public booking page reflect the new state and rule effects within 5 seconds And the API returns the updated status and effective window on GET endpoints within 2 seconds (read-your-writes) And a domain event for the state change is emitted within 2 seconds with a stable deduplication key; consumers receive at least once but process idempotently And any attempt to set a status outside the allowed set is rejected with "INVALID_STATUS"
Scoped Application Engine
"As a product engineer, I want a consistent way to determine which overrides apply to a booking event so that behavior is predictable across all entry points."
Description

Provide a rule-evaluation engine that applies overrides at decision points (booking, waitlist promotion, roster edits, check-in). Support granular scoping: specific attendee, class session, series, location, instructor, or combination. Define precedence and conflict resolution when multiple overrides might apply, and ensure evaluation is performant and cache-aware. Expose read APIs to query effective overrides for a given entity and time. Ensure consistency across web, mobile, and API clients.

Acceptance Criteria
Apply One-Time Attendee Override at Booking
Given an active one-time override scoped to attendee A and class session S with reason_code R, effect allow_book, and expiry T And class session S has reached capacity When A attempts to book S before T Then the decision engine returns allow_book=true with override_id set to the applied override and consumed=true And a subsequent booking attempt by A for S without another applicable override returns allow_book=false due to capacity_exceeded And the decision evaluation completes within 50ms p95 and 150ms p99 under warm cache conditions
Time-Boxed Override Auto-Expiry at Check-In
Given a time-boxed override scoped to attendee A and series X with effect allow_check_in_without_cert and expiry T When A attempts to check in to a session in series X at time t < T Then the decision returns allow_check_in=true with applied_override_id When A attempts to check in at time t >= T Then allow_check_in=false with code cert_required and the expired override is not returned in effective_overrides And the Read API for A and the same session at t >= T returns no active overrides matching the expired one
Granular Multi-Scope Resolution (Attendee+Series+Location)
Given three active overrides: O1 scoped to attendee A + series X with effect deny_book; O2 scoped to location L with effect allow_book; O3 scoped to attendee A + series X + location L with effect allow_book When evaluating a booking decision for A into a session of series X at location L Then O3 is selected and allow_book=true And precedence is determined by specificity: (attendee+series+location) > (attendee+series) > (location) And at equal specificity, explicit deny prevails over allow; at equal specificity and effect, the override with the later expiry_time prevails
Cache-Aware Evaluation and Invalidation
Given applicable overrides for attendee A and session S are present in cache When a relevant override for A/S is created, updated, or deleted Then cache entries for A/S and any affected scopes are invalidated within 1s of write acknowledgment And the next evaluation reflects the change with cold-cache latency <=80ms p95 and subsequent warm evaluations <=30ms p95 And no stale override influences a decision more than 1s after the write is acknowledged
Read API: Query Effective Overrides
Given overrides applicable to attendee A for session S at time t When calling GET /overrides/effective?attendee_id=A&session_id=S&at=t Then the API returns HTTP 200 with items ordered by effective precedence And each item includes id, scope, effect, reason_code, starts_at, expires_at, created_by, updated_at, active And filtering by scope_type and effect returns only matching items And response time is <=120ms p95 and <=300ms p99 for up to 100 items
Cross-Channel Consistency (Web/Mobile/API)
Given identical inputs (attendee A, session S, evaluation time t) across web, mobile, and public API clients When each client requests a booking decision within a 2s window Then all channels return the same decision outcome and the same applied_override_id (if any) And each response includes a correlation_id that maps to a single server-side evaluation trace
Waitlist Promotion Honors Overrides
Given an active override scoped to attendee A and class session S with effect allow_waitlist_promotion and expiry T And S is at capacity and A is position 1 on the waitlist When a seat becomes available before T Then the promotion job promotes A to confirmed regardless of prerequisite or cooldown checks and records applied_override_id When a seat becomes available at or after T Then A is not auto-promoted unless another applicable override exists
Reason Codes & Policy Mapping
"As a compliance coordinator, I want standardized reason codes linked to policy gates so that overrides are consistent and reportable."
Description

Offer a configurable catalog of reason codes (e.g., Certification Pending, VIP, System Error) with optional required fields (e.g., document link, approver note). Require selection of a reason code when creating an override and map codes to the specific policy gates they may bypass. Provide analytics hooks to report on override frequency, by code, by staff, and by class. Localize labels and support tenant-level customization with sensible defaults.

Acceptance Criteria
Admin configures tenant-level reason codes
Given I am a Tenant Admin on ClassTap When I open Settings > Overrides > Reason Codes Then I can create a new reason code with a unique key (unique per tenant), default label, optional description, and configurable fields (e.g., Approver Note [text], Document Link [URL]) And I can mark each field as Required or Optional And I can edit an existing reason code’s labels, descriptions, and field requirements And I can activate or deactivate a reason code And attempts to save a duplicate key within the same tenant are blocked with a clear validation message
Override creation requires reason code and enforces fields
Given I am creating an override for a class or series When I attempt to submit without selecting a reason code Then submission is blocked and an inline error states a reason code is required Given I select a reason code with required custom fields When I attempt to submit without completing those required fields Then submission is blocked and field-level errors indicate what is missing Given all required fields are completed When I submit the override Then the override is created successfully and persists the selected reason code and field values
Policy mapping controls which gates an override may bypass
Given a reason code is mapped to specific policy gates (e.g., Certification Check, Booking Cutoff, Payment Requirement, Capacity Limit) When that code is used on an override Then only the mapped gates are bypassed and all other gates remain enforced Given the code “Certification Pending” is mapped only to Certification Check When the target class is at capacity Then the override does not bypass capacity and cannot enroll unless another capacity-allowed code is applied Given a code is mapped to multiple gates When used, Then each mapped gate is bypassed and recorded in the audit entry
Audit log captures override reason and details
Given an override is created, updated, or deleted When I view the audit trail for that override Then each entry includes: tenantId, overrideId, approverUserId, actedAt (ISO 8601), scope (classId or seriesId), selected reasonCode key and label, custom field values, and list of gates bypassed And audit entries are immutable (no edits in place); subsequent changes create new entries with before/after deltas And audit entries are searchable by date range, reasonCode, approver, class/series
Analytics hooks: overrides by code, staff, and class
Given analytics instrumentation is enabled When an override is created, updated, or expires Then the system emits events (override_created, override_updated, override_expired) to the analytics bus with: tenantId, overrideId, reasonCode, approverUserId, classId/seriesId, studentId (if applicable), gatesBypassed[], timestamp And a test sink can receive and assert these events within 2 seconds of action And analytics can aggregate override counts grouped and filtered by reasonCode, approverUserId, classId/seriesId, and time window
Localized labels and tenant-level defaults
Given a new tenant is provisioned When they first access Reason Codes Then a sensible default catalog is available (e.g., Certification Pending, VIP, System Error) in the tenant’s default locale Given the tenant configures translations for a reason code When a staff user’s UI locale is set to that language Then the reason code label and custom field prompts render in that locale, otherwise they fall back to the default locale Given a reason code is deactivated When viewing historical audit entries or analytics Then the localized labels still display for past records while the code remains unavailable for new overrides
Audit Trail & Compliance Reporting
"As a studio owner, I want a complete audit history of overrides so that we can demonstrate control and trace issues when needed."
Description

Record immutable audit events for the full override lifecycle (create, edit, activate, expire, revoke) including actor, timestamp, IP/device, before/after values, reason code, and scope. Provide searchable, exportable logs (CSV/JSON) with filters by date, user, class, and reason code. Enforce data retention policies per tenant and ensure logs are tamper-evident. Surface audit summaries in the admin UI and expose read-only endpoints for SOC/QA reviews.

Acceptance Criteria
Record Override Lifecycle Events
- Given a manager creates a timed override for a class or series, When the create action is confirmed, Then an event "override.created" is recorded with an immutable ID. - Given a manager edits any override field, When the edit is saved, Then an event "override.edited" is recorded containing a before/after diff of changed fields only. - Given an override is set to activate at a future time, When the activation time arrives or a manager activates it manually, Then an event "override.activated" is recorded indicating trigger type (auto/manual) and context. - Given an override has an auto-expiry set, When the expiry time is reached, Then an event "override.expired" is recorded exactly once with reason "auto_expire". - Given an active override is revoked before expiry, When a manager revokes it, Then an event "override.revoked" is recorded with the revoker identity and supplied reason code. - Given any lifecycle event is recorded, When attempting to update or delete that audit entry via API/UI/DB user, Then the operation is rejected and the original record remains unchanged.
Required Audit Field Coverage
- Given any override lifecycle event is recorded, When inspecting the event payload, Then it includes: actor.userId (or "system"), actor.role, timestamp in ISO 8601 UTC with millisecond precision, requestId/correlationId, source.ip (IPv4/IPv6), source.userAgent/device, tenantId, scope (classId and/or seriesId), reasonCode (if provided), and before/after values for edited fields. - Given a system-initiated event (e.g., auto-expiry), When recorded, Then actor.userId is "system" and trigger context is present (scheduler/job identifier). - Given a multi-tenant environment, When two tenants perform similar actions, Then each event contains the correct tenantId and no event references resources from another tenant. - Given optional fields are not applicable (e.g., before/after on create), When the event is stored, Then those fields are null or absent, not populated with incorrect defaults. - Given clocks may vary, When events are ingested from different nodes, Then stored timestamps are normalized to UTC and monotonic order is preserved per overrideId.
Tamper-Evident Log Integrity and Verification
- Given an audit event is written, When it is stored, Then it contains eventHash = H(payload) and prevHash referencing the previous event in the tenant’s chain. - Given a sequence of events exists for a tenant, When the integrity verification endpoint GET /api/v1/audit/integrity?tenantId=...&from=...&to=... is called, Then it returns 200 with {valid:true} when all hashes and ordering validate. - Given an event is modified out-of-band, When the integrity verification runs, Then it returns {valid:false} with the first failing eventId and reason (hash_mismatch or missing_link) and the Admin UI displays an integrity warning badge. - Given storage or transport errors occur, When an event cannot be appended with a valid prevHash, Then the write is rejected and an "audit.write_failed" alert is logged with correlationId. - Given a read-only reporting replica, When verifying integrity against it, Then verification yields the same result as primary within eventual consistency window (<= 60 seconds).
Admin Audit Log: Search, Filters, Sort, Pagination, and Summaries
- Given the admin navigates to Audit Logs, When filtering by date range (start/end, inclusive, UTC), Then only events within the range are returned. - Given filters for actor.userId, eventType, classId/seriesId, and reasonCode, When applied in any combination, Then the result set reflects all filters with AND semantics. - Given results are displayed, When sorting by timestamp desc/asc, Then ordering updates accordingly and is stable across pages. - Given large datasets, When paginating with page/limit (default limit 50, max 500), Then results include totalCount and next/prev cursors and return within 2 seconds for up to 100k events with active indexes. - Given a tenant boundary, When a user queries logs, Then only events for their tenantId are visible and cross-tenant records are never returned. - Given a date range and filters are applied, When viewing the Summary panel, Then counts by eventType and reasonCode and a daily trend are shown and match the result set; clicking a summary chip applies the corresponding filter.
Export Logs with Filters to CSV/JSON
- Given filters are applied in the Audit Logs, When the admin clicks Export CSV or Export JSON, Then the exported file contains only the filtered events in the same order as the current sort. - Given CSV export, When the file is generated, Then it includes a header row with columns: eventId, tenantId, timestampUtc, eventType, actor.userId, actor.role, source.ip, source.userAgent, classId, seriesId, reasonCode, before, after, eventHash, prevHash, requestId. - Given JSON export, When the file is generated, Then it is newline-delimited JSON (NDJSON) with one event per line conforming to the public schema version. - Given the result size exceeds 50,000 events or 50 MB, When export is requested, Then an async job is queued and the user receives a secure link via email/in-app once ready; the link expires in 24 hours and requires the same permissions to access. - Given an export is completed, When downloaded, Then a SHA-256 checksum is provided and matches the file contents; timestamps are UTC; purged events (per retention) are not included.
Data Retention and Legal Hold per Tenant
- Given a tenant has a retention policy (e.g., 180 days), When events exceed the retention period, Then a nightly purge removes those events and records an aggregate "audit.purged" entry with counts and cutoff timestamp. - Given a legal hold is enabled for a tenant, When the purge job runs, Then no events newer than the hold start date are purged and the job logs "skipped_due_to_legal_hold". - Given retention changes from 365 to 180 days, When the next purge runs, Then events older than 180 days are purged and more recent events are retained. - Given an admin searches or exports after a purge, When viewing results, Then purged events are absent and totalCount reflects only retained events. - Given SOC review endpoints are queried, When requesting ranges beyond retention, Then the API returns only retained events and includes retention window metadata in the response.
Read-Only SOC/QA Audit API Endpoints
- Given a user with role soc_auditor, When calling GET /api/v1/audit/logs with supported filters (date range, actor, eventType, classId/seriesId, reasonCode) and pagination, Then the API returns 200 with results matching filters and a documented JSON schema; write methods (POST/PUT/PATCH/DELETE) return 405. - Given an unauthenticated or unauthorized user, When calling the same endpoints, Then the API returns 401 (unauthenticated) or 403 (forbidden) without leaking tenant existence. - Given multi-tenant isolation, When a soc_auditor from Tenant A queries, Then only Tenant A events are returned; specifying another tenantId is ignored/denied with 403. - Given large result sets, When paginating with cursor-based pagination, Then the API returns 206 Partial Content with next/prev cursors and consistent ordering by timestamp,eventId. - Given rate limiting and stability, When a client exceeds 60 requests/min, Then responses include 429 with Retry-After; normal requests have p95 latency under 500 ms for 10k/day workloads.
Roster Badges & Follow-up Tasks
"As an instructor, I want to see which attendees are attending under an override and any follow-up tasks so that I can act accordingly before class."
Description

Display a discreet, accessible badge on rosters and booking details when an active override applies, with tooltip summarizing reason and expiry. Allow managers to attach follow-up tasks (e.g., upload certification by date, collect payment) with due dates, assignees (student or staff), and links to completion actions (document upload, invoice). Integrate tasks with reminder cadence and dashboard widgets for instructors and admins. Update badge state as tasks are completed or overdue.

Acceptance Criteria
Badge visibility on staff rosters and booking details
Given an active override is associated with a booking, When a staff roster view is loaded, Then a badge is displayed in that attendee’s roster row for that class. Given an active override is associated with a booking, When the staff booking details page is loaded, Then the badge is displayed adjacent to the attendee record. Given no active override is associated with a booking, Then no badge is displayed in roster or booking details. Given the viewer is an Instructor, Admin, or Manager, Then the badge is visible; Given the viewer is a Student or unauthenticated, Then the badge is not displayed in public-facing rosters.
Tooltip content and accessibility
Given the badge has keyboard focus or pointer hover, When triggered, Then a tooltip is shown within 200ms containing the override reason code and expiry date/time. Then the tooltip is dismissible via Escape key, focus out, or tap outside on touch devices. Then the badge and tooltip meet WCAG 2.2 AA for color contrast and are reachable in the tab order. Then the badge exposes an accessible name including the text "Override active" plus reason and expiry to screen readers.
Create and manage follow-up tasks
Given a user with Manager role opens the override panel for a booking, When they add a task with title, due date, assignee (student or staff), and a valid action link, Then the task is created and shown in the task list for that override. When required fields are missing or invalid (empty title, past due date, invalid link), Then inline validation errors are shown and the task is not saved. When multiple tasks are added, Then all tasks are persisted and displayed in chronological order by due date. Then every create/update/delete action on tasks records an audit log entry with actor, timestamp, action, and before/after values.
Task completion and overdue state updates
Given a pending task, When the linked action completes successfully (e.g., document uploaded, invoice paid) or a permitted user marks it complete, Then the task status changes to Completed with actor and timestamp. Given the due date passes in the class’s local timezone and the task is not completed, Then the system marks the task as Overdue. Given at least one task is Overdue while the override is active, Then the badge state changes to indicate "Overdue tasks" and displays the overdue count. Given all tasks are Completed while the override is active, Then the badge state changes to "All tasks complete". Given the override expires, Then the badge is removed from roster and booking details, while tasks remain accessible in task lists with their final statuses.
Reminder cadence for tasks
When a task is created, Then notifications are sent to the assignee immediately and reminders are scheduled for 3 days before due, 1 day before due, on the due date at 9:00 in the class’s timezone, and daily for 3 days if overdue, then weekly for 2 weeks. Then reminders are delivered via email and SMS where consent and contact details exist; if SMS is unavailable, only email is sent. Then reminders stop immediately when the task is completed or the override expires. Then instructors and admins receive a daily digest listing their Overdue tasks.
Dashboard widgets for tasks
Given an Instructor is signed in, When viewing their dashboard, Then a "Follow-up Tasks" widget shows counts for Pending, Due Soon (≤3 days), and Overdue, and lists up to 10 nearest-due tasks with filters for class and assignee type. Given an Admin is signed in, When viewing their dashboard, Then a widget shows the same plus filters for location and instructor. When a task is marked done from the widget, Then the task status updates within 2 seconds, the counts refresh, and the action is audit logged. Then each list item provides a deep link to the related booking/override panel.
Permissions and audit trail
Given a Manager role, Then they can create, edit, and delete follow-up tasks for any override within their scope. Given an Instructor role, Then they can view tasks for their classes and mark tasks complete but cannot create or delete tasks unless explicitly granted permission. Given a Student assignee, Then they can complete their task via the provided action link but cannot view or edit other tasks. Then all permission violations are blocked with a 403 response in APIs and a user-friendly message in UI. Then an immutable audit log captures user id, role, action, entity id, timestamp, and reason code for all task and badge state changes.
Gate Overrides (Capacity, Cutoffs, Prereqs)
"As a front-desk coordinator, I want to let a member into a full class or without a pending cert just this once so that we can accommodate legitimate exceptions without breaking our rules."
Description

Define which policy gates an override can bypass, including capacity limits, booking/cancellation cutoffs, prerequisite certifications, and payment requirements. Ensure downstream systems handle bypass safely: annotate payments as deferred, mark prerequisites as pending, and prevent automatic penalties while override is active. On expiry, re-validate state and trigger required actions (e.g., invoice creation, task escalation). Maintain compatibility with waitlists by allowing explicit capacity overage and preserving seat accounting.

Acceptance Criteria
Scoped Gate Selection and Permissions
Given a manager user with "Override:Create" permission And a class or series is selected When the manager creates an override and selects gate types to bypass (Capacity, Booking Cutoff, Cancellation Cutoff, Prerequisites, Payment) And sets a reason code and an auto-expiry timestamp Then the override cannot be saved unless all required fields (scope, gates, reason code, auto-expiry) are provided And only the selected gates are bypassed for the scoped class/series and affected attendee(s) within the override's active window And users without "Override:Create" permission cannot create or edit overrides
Capacity Overage with Waitlist Preservation
Given a class with capacity 20 currently at 20/20 and a waitlist of 3 And a manager creates an override with Capacity bypass and an explicit overage amount of 2 seats When the manager books Attendee A and Attendee B using the override Then the system allows booking up to 22 total attendees and blocks the 23rd booking without an additional override And the roster displays an "Override" badge for A and B And seat accounting retains base capacity 20 and separately tracks overage 2 And waitlisted customers do not auto-promote into overage seats unless booked via explicit override
Cutoff Bypass for Late Booking and Cancellation
Given a class with booking cutoff at T-2h and cancellation cutoff at T-12h And current time is T-1h When a manager applies an override bypassing Booking Cutoff for Attendee C Then Attendee C can be booked successfully And no late-booking penalties or blocks are applied while the override is active When a manager applies an override bypassing Cancellation Cutoff for Attendee D at T-1h Then Attendee D can cancel without automatic late-cancel penalties while the override is active
Prerequisite Override with Follow-up Task and Escalation
Given a class that requires Certification X When a manager books Attendee E using an override bypassing Prerequisites and sets follow-up due date 2025-09-30 Then Attendee E is enrolled And Attendee E's prerequisite status is set to "Pending via Override" with due date 2025-09-30 And a follow-up task "Upload Certification X by 2025-09-30" is created and assigned to Attendee E (or staff) with reminders And the roster shows an "Override" badge with reason code When the override expires and Certification X is still missing Then the task escalates to the designated role and notifications are sent per policy
Deferred Payment Override with Invoice on Expiry
Given a class that requires immediate payment at booking When a manager books Attendee F using an override bypassing Payment and sets auto-expiry at 2025-09-20T17:00:00Z Then the booking is confirmed without charging And the payment record is annotated "Deferred via Override" with due date 2025-09-20T17:00:00Z And any automated non-payment penalties or auto-cancellations are suppressed while the override is active When the override expires and balance remains unpaid Then the system generates an invoice for the outstanding amount and sends a payment request And if a vaulted payment method exists and policy permits, the system attempts a charge and records success/failure And on failure, the system escalates per policy and re-enables standard penalties
Override Expiry Re-validation and Action Triggers
Given an override with auto-expiry at 2025-09-21T00:00:00Z that bypassed Payment and Prerequisites When the expiry timestamp is reached Then the system re-validates all previously bypassed gates for the affected booking(s) And triggers the corresponding actions: create invoice for unpaid balance; escalate open prerequisite tasks; re-enable penalty evaluations And all triggered actions are timestamped and linked to the override record
Auditability and Visibility of Overrides
Given an override is created, used to book, edited, and then expires When viewing the audit log for the class and the attendee booking Then the log shows actor, timestamps, gates bypassed, scope (class/series/attendee), reason code, auto-expiry, changes, and outcomes of expiry actions And the roster and booking detail display a subtle "Override" badge with hover/expand details (reason code and due dates) And the override can be filtered and reported on by date range, gate type, manager, and outcome (e.g., invoice created, task escalated)

Compliance Ledger

A tamper‑evident audit trail of rule evaluations, documents, approvals, and check‑ins with timestamps and actors. Exports to CSV/PDF in accreditor‑friendly formats and fires alerts for upcoming expirations that could affect bookings. Makes audits painless and gives admins instant answers to “why was this blocked?”

Requirements

Append-only Compliance Ledger
"As a compliance administrator, I want every compliance-relevant action recorded immutably with timestamps and actors so that I can prove what happened and detect any tampering during audits."
Description

Implement a tenant-scoped, append-only event store that records all compliance-relevant events (e.g., rule_evaluated, document_uploaded, approval_granted, document_expired, booking_blocked, check_in) with immutable, tamper-evident guarantees. Each event must include a monotonic timestamp, actor identity, entity references (user, instructor, class, facility, booking), rule/version identifiers, correlation IDs, and an idempotency key. Events are chained using cryptographic hashes (e.g., SHA-256) to produce per-tenant hash chains; any mutation generates a detectable break. Storage must be write-once (or logically immutable with compensation events), encrypted at rest/in transit, and partitioned by tenant for scalability. Provide ingestion APIs/SDK hooks for all ClassTap services to emit ledger events synchronously with core transactions, with retry/backoff and at-least-once delivery. Include operational tooling for chain verification, backfill from existing logs, and drift detection. This forms the foundation for audits, exports, and explanations.

Acceptance Criteria
Per-Tenant Append-Only Hash Chain Integrity
Given a tenant T with an existing ledger head When a new event E is appended Then E.previous_hash equals the last event’s current_hash for T and recomputing SHA-256 over E’s canonicalized content plus E.previous_hash equals E.current_hash Given any request to update or delete an existing ledger event When the request is processed by the ledger service Then the service rejects the operation and no persisted records are altered; corrections must be recorded as a new compensation event referencing the original event_id Given an event in the middle of a chain is modified out-of-band When the chain verification tool runs for tenant T Then it reports a break at that index including tenant_id, event_id, expected_hash, actual_hash, and exits with a non-zero status
Event Schema Completeness and Monotonic Timestamps
Given an incoming ledger event When validated Then the following fields are required and non-empty: tenant_id, event_id (UUID), event_type ∈ {rule_evaluated, document_uploaded, approval_granted, document_expired, booking_blocked, check_in, booking_created, booking_confirmed, booking_cancelled}, timestamp (server-assigned), actor_id, actor_type, correlation_id, idempotency_key, current_hash, previous_hash, and at least one entity reference among {user_id, instructor_id, class_id, facility_id, booking_id} Given event_type ∈ {rule_evaluated, booking_blocked} When validated Then rule_id and rule_version are required fields; for other event types they are optional Given two consecutively stored events for the same tenant When comparing their timestamps Then later.timestamp >= earlier.timestamp for the entire chain; any event that would violate this constraint is rejected and not persisted
Synchronous Emission with Core Transactions, Retries, and Idempotency
Given a core transaction (e.g., booking creation or approval grant) succeeds When the API returns success to the caller Then a corresponding ledger event with matching correlation_id exists and its timestamp is within 100ms of the transaction commit time Given a ledger write fails due to a transient error When the service retries Then it uses exponential backoff with jitter for up to 5 attempts over ≤10s total before failing the core transaction and returning a retriable error Given duplicate submit attempts with the same tenant_id and idempotency_key When processed Then exactly one ledger event is persisted and subsequent attempts return the original event_id without creating duplicates Given a ledger event is written but the core transaction fails/rolls back When mismatch is detected Then a compensation event is appended referencing the orphan event_id and correlation_id within 1 minute
Encryption In Transit/At Rest and Tenant Partitioning
Given any connection to ledger APIs When negotiated Then TLS 1.2+ is enforced and weak ciphers are rejected Given persisted ledger data When inspected via cloud/KMS configuration Then encryption at rest is enabled using AES-256 with customer-managed keys and key rotation is supported without data rewrite Given tenants A and B When querying with credentials scoped to tenant A Then no events belonging to tenant B are returned and attempts to specify tenant_id=B result in 403 Given tenant A sustains 1,000 events/minute for 10 minutes When measuring p95 write latency for tenant B concurrently at 50 events/minute Then p95 latency for tenant B remains within 10% of its baseline measured with no concurrent load
Operational Chain Verification and Drift Detection
Given the verification CLI is run for tenant T When the chain has no breaks Then it exits with code 0 and prints total events verified and the head hash Given a break, gap, or hash mismatch exists When the verification runs Then it exits non-zero and emits a JSON report with tenant_id, first_bad_event_id, index, expected_previous_hash, actual_previous_hash Given daily scheduled verification When a break is detected Then an alert is sent to the monitoring system within 5 minutes with severity=high and a link to the report
Backfill from Existing Logs
Given a source log of compliance-relevant records for tenant T When backfill is executed Then events are imported in chronological order preserving original timestamps, previous_hash/current_hash are recomputed, and backfilled=true is set on those events Given the same backfill job is rerun When idempotency_keys from the source are reused Then no duplicate ledger events are created and the operation is idempotent Given backfill completes When chain verification runs Then it passes with exit code 0 and the number of imported events equals the number of source records successfully mapped
Booking Block Reason Traceability
Given a booking attempt is blocked by compliance When the event is recorded Then a booking_blocked event exists with correlation_id matching the booking attempt, rule_id, rule_version, evaluation_result=false, actor_id (system), and human_readable_reason present and non-empty Given an admin queries “Why was this blocked?” with the correlation_id When the explanation endpoint is called Then it returns the chain segment including the triggering rule_evaluated and booking_blocked events with actors, timestamps, and hashes, and recomputing hashes over the segment matches stored values
Rule Evaluation Trace Logging
"As an admin, I want a detailed rule evaluation trace for each booking attempt so that I can explain why a request was blocked or allowed and what evidence was considered."
Description

Capture and persist a detailed trace for every policy evaluation that influences bookings, check-ins, and payments holds. For each evaluation, store the rule set name and version, input fingerprints (PII-minimized), referenced documents and their states, decision outcome (allow/block/warn), reason codes, and latency metrics. Link traces to the originating booking attempt or user action via correlation IDs and surface a normalized schema to power explanations. Ensure redaction of sensitive payloads with reversible references to secure stores when needed. Provide APIs to fetch traces by booking ID, user ID, class ID, or time window and index for fast retrieval. This trace directly feeds the “Why Blocked?” view and export reports.

Acceptance Criteria
Immutable Trace Persisted per Policy Evaluation with Correlation to Originating Action
Given a booking, check-in, or payment-hold triggers a policy evaluation using a named rule set and version When the evaluation completes Then exactly one immutable trace record is persisted containing at minimum: trace_id, tenant_id, correlation_id, action_type in {booking, check_in, payment_hold}, action_id, actor_type in {user, admin, system}, actor_id, rule_set_name, rule_set_version, decision in {allow, block, warn}, reason_codes[], evaluation_start_at (ISO-8601), evaluation_end_at (ISO-8601), evaluation_duration_ms >= 0, environment And attempts to update any persisted field are rejected with 409 (immutable_record) And the trace is retrievable by action_id and correlation_id within 1 second of evaluation_end_at
PII-Minimized Input Fingerprints with Secure References and Access Control
Given evaluation inputs include PII (e.g., full_name, email, phone, DOB) When persisting the trace Then no raw PII values are stored in the trace payload And input_fingerprints contains irreversible fingerprints for each PII field present And secure_refs contains reference IDs to PII stored in a secure vault separate from the trace store And dereferencing secure_refs via API requires scope "pii:read"; calls without this scope return 403 and never return raw PII And reason_codes, messages, and metadata in the trace contain no raw PII
Point-in-Time Capture of Referenced Documents and States
Given rules reference compliance documents (e.g., waiver, certification) When an evaluation runs Then the trace includes documents[] each with: doc_id, doc_type, doc_version, state in {valid, expired, revoked, missing}, evaluated_at, effective_at, expires_at (if applicable) And the captured document state reflects a point-in-time snapshot at evaluation_start_at and does not change if the underlying document is later updated or revoked And multiple referenced documents are all recorded in the same trace with their individual states
Decision Outcome, Reason Codes, and Latency Metrics Recorded per Evaluation
Given a policy evaluation executes a set of rules When the trace is persisted Then decision is one of {allow, block, warn} And reason_codes is a non-empty array of registered codes for all decisions And rule_set_name and rule_set_version are recorded And metrics include rules_evaluated_count, rules_matched_count (<= rules_evaluated_count), evaluation_duration_ms, and per_rule_latencies_ms keyed by rule identifier And evaluation_end_at - evaluation_start_at equals evaluation_duration_ms within ±5 ms
Retrieval APIs: Filter by Booking/User/Class/Time Window with Pagination and Tenant Scoping
Given the Traces API is available to authorized callers When requesting GET /traces with any of the filters {booking_id, user_id, class_id, correlation_id, decision, reason_code, from, to} Then the response is 200 with only traces matching the provided filters, scoped to the caller's tenant_id And results are sorted by evaluation_start_at desc by default And pagination supports limit (default 25, max 200) and cursor; responses include next_cursor when more results exist And invalid parameters yield 400 with validation errors; cross-tenant access attempts yield 403; no matches yield 200 with an empty list
Indexing and Performance SLAs for Trace Writes and Reads
Given a tenant with up to 1,000,000 trace records and indexes on (tenant_id, booking_id), (tenant_id, user_id), (tenant_id, class_id), and (tenant_id, evaluation_start_at) When writing traces under normal load Then p95 persist latency <= 120 ms and p99 <= 300 ms And when reading via a single indexed filter (e.g., booking_id or user_id) with limit<=100 Then p95 query latency <= 300 ms and p99 <= 700 ms And time-window scans (from/to over evaluation_start_at) return first page (limit<=100) with p95 <= 400 ms And system sustains >= 99.9% successful read/write operations over a rolling 30-day window
Normalized Schema Feeds "Why Blocked?" View and Redacted CSV/PDF Exports
Given a booking is blocked and an admin opens the Why Blocked? view When the UI requests the related trace by booking_id Then the API returns a normalized schema with fields required for explanation: decision, reason_codes, reason_params, rule_set_name, rule_set_version, documents[], actor, action_type, action_id, timestamps, and input_fingerprints/secure_refs And the UI can render a human-readable explanation by mapping reason_codes to localized messages without additional data fetches And when exporting the same trace(s) to CSV/PDF Then the export contains the normalized fields, excludes raw PII, and includes secure_ref identifiers only; attempts to include PII without scope "pii:read" return 403
Evidence Document Lifecycle & Expiration Tracking
"As an instructor, I want to upload certifications and see their approval and expiration status so that my classes aren’t blocked due to lapsed credentials."
Description

Enable secure ingestion, storage, and lifecycle management of compliance evidence (certifications, waivers, IDs) with metadata (type, issuer, identifier, effective/expiration dates, reviewer, status). Support versioning with supersede relationships and approvals capturing approver identity, timestamps, and comments. Documents are stored in secure object storage with content hashes and size, linked to instructors, staff, classes, or facilities. All create/update/approve/expire actions emit ledger events. Implement automatic expiration computation and status derivation, with APIs to query current compliance state per entity and to block bookings when required artifacts are missing or expired.

Acceptance Criteria
Upload evidence document with required metadata
Given an authenticated user with upload permissions When they POST /evidence with a file and required metadata {type, issuer, identifier, effective_date, expiration_date (optional), reviewer_id (optional), linked_entities[]} Then the file is stored in secure object storage and metadata is persisted with status = "Pending Review" And the system computes and stores SHA-256 content_hash and byte_size And the response is 201 with document_id and stored metadata And a ledger event DOCUMENT_CREATED is appended with {document_id, actor_id, timestamp, content_hash, byte_size} And if required metadata is missing or type unsupported, the API responds 422 with error codes and no object is stored
Supersede an existing evidence document with a new version
Given an existing document in status Approved or Active lineage When a new document is uploaded with supersedes_document_id referencing the existing document Then the new document is created with a new document_id and supersedes = referenced document_id And the prior document status transitions to "Superseded" and remains immutable (read-only) And lineage (root_id, version sequence) is maintained and queryable And a ledger event DOCUMENT_SUPERSEDED is appended with {old_document_id, new_document_id, actor_id, timestamp} And if supersedes_document_id is invalid or not in a supersedable state, respond 409 with reason code
Approve evidence with audit attributes
Given a document in status Pending Review And a reviewer with approval permissions When the reviewer POSTs /evidence/{id}/approve with {comment (optional)} Then the document status updates to "Approved" if current time >= effective_date, else "Approved (Not Yet Effective)" And the approval record captures {approver_id, approved_at (UTC), comment} And a ledger event DOCUMENT_APPROVED is appended with {document_id, approver_id, approved_at, previous_status, new_status} And if the reviewer lacks permission, respond 403 and no state change And double-submit with the same idempotency key returns 200 and no duplicate ledger entry
Automatic expiration and status derivation
Given an Approved document with an expiration_date When the system scheduler runs at least daily Then at expiration_date 00:00:00 UTC the document status transitions to "Expired" And a ledger event DOCUMENT_EXPIRED is appended with {document_id, expired_at} And documents with effective_date in the future are marked "Not Yet Effective" and excluded from fulfilling active requirements And documents without expiration_date remain "Approved" and do not expire automatically
Associate evidence to entities (instructors, staff, classes, facilities)
Given valid entity IDs exist When a user links a document to one or more entities via POST /evidence/{id}/links Then associations are created with referential integrity and are queryable by entity and by document And unlinking via DELETE /evidence/{id}/links/{entity_ref} updates associations without altering document content And a ledger event EVIDENCE_LINK_UPDATED is appended with {document_id, entity_refs_added, entity_refs_removed, actor_id, timestamp} And attempts to hard-delete a document respond 405; only supersede is allowed for version evolution
Query current compliance state for an entity
Given an entity_id When GET /compliance/state?entity_id={id} is called Then the response includes for each required artifact: {requirement_key, status in [Approved, Expired, Missing, NotYetEffective, Superseded], document_id (if any), effective_date, expiration_date, last_reviewed_at, blocker (bool), reason_code} And the API reflects the latest status derivation as of request time And if the entity_id is unknown, respond 404 And p95 latency for single-entity queries under nominal load is <= 300ms
Block bookings when required artifacts are missing or expired
Given a class has configured required evidence types for instructors/staff/facility And a booking create request is submitted When any required artifact for the relevant actors/entities resolves to status Missing or Expired Then the booking API responds 409 with code = "COMPLIANCE_BLOCK" and details of unmet artifacts And no booking is created or held And a ledger event BOOKING_BLOCKED_COMPLIANCE is appended with {class_id, actor_ids, missing_artifacts, timestamp} And when all required artifacts resolve to Approved and Effective, the booking proceeds with 201 Created
Expiration Alerts & Booking Safeguards
"As a studio owner, I want proactive alerts for upcoming compliance expirations so that I can resolve issues before they disrupt bookings."
Description

Provide a scheduler that continuously evaluates upcoming expirations and compliance risks against configurable thresholds (e.g., 30/14/7 days). Generate actionable alerts with context (affected classes/bookings, required documents) and deliver via email, SMS, and in-app notifications, with acknowledgement, snooze, and escalation workflows. Expose webhooks for third-party systems. Integrate with booking flows to warn prior to expiration windows and automatically block bookings after policy-defined grace periods. All alert creation, acknowledgement, and suppression actions write to the ledger for auditability.

Acceptance Criteria
Threshold-Based Expiration Alert Generation with Affected Bookings Context
Given organization-level thresholds of 30, 14, and 7 days are configured and a venue timezone is set And compliance items (e.g., certifications, documents, background checks) have expiration dates and are linked to instructors/resources/classes When the compliance scheduler runs every 15 minutes Then it evaluates upcoming expirations using the venue timezone and threshold rules And creates one alert per entity per threshold when entering a window not previously alerted And sets alert fields: entity_type, entity_id, rule_id, threshold_days, expiration_at, severity, affected_classes_count, affected_bookings_count, sample_affected_ids, required_documents And writes a ledger entry for event "alert.created" with actor "system", timestamp, and chain hash linking to the previous entry And does not create a duplicate if an active alert exists for the same entity_id+rule_id+threshold_days
Multi-Channel Alert Delivery with Preferences, Retries, and Action Links
Given a new alert is created and recipients have in-app, email, and optional SMS channels with preferences When the alert is published Then an in-app notification appears for targeted users within 10 seconds And an email is sent within 60 seconds including alert context and deep links to View, Acknowledge, and Snooze And an SMS is sent within 60 seconds for high-severity alerts where SMS is enabled and compliant to user preferences And each channel records delivery status, provider message ID, and first-open timestamp (if available) And failed deliveries are retried up to 3 times with exponential backoff (30s, 2m, 10m) And user channel opt-outs and quiet hours are honored per policy configuration And all delivery attempts and outcomes are written to the ledger as "delivery.attempt" or "delivery.failed" with channel metadata
Acknowledgement, Snooze, and Escalation Workflow
Given a pending alert is visible to an authorized user When the user acknowledges the alert Then the system requires an optional note (up to 500 chars) and records user_id, timestamp, and note And the alert state changes to acknowledged and reminder notifications stop And a ledger entry "alert.acknowledged" is written with previous_state and new_state When the user snoozes the alert Then the user can choose 1, 3, or 7 days (not exceeding the expiration time or policy limit) And a wake_time is scheduled and reminders are paused until wake_time And a ledger entry "alert.snoozed" is written with snooze_duration and wake_time When an alert remains unacknowledged within 24 hours of expiration or transitions to a higher-severity threshold (e.g., 14d->7d) Then it escalates to configured roles (e.g., Org Admin, Compliance Officer) with distinct templates per role and channel And escalation notifications are sent and logged And a ledger entry "alert.escalated" is written with escalation_level and recipients
Webhook Notifications for Third-Party Systems with Security and Idempotency
Given a webhook endpoint is registered with a shared secret and subscriptions to events (alert.created, alert.updated, alert.acknowledged, alert.snoozed, alert.escalated, booking.warning_shown, booking.blocked, delivery.failed) When a subscribed event occurs Then the platform sends an HTTPS POST within 60 seconds containing JSON payload {id, type, created_at, data, idempotency_key} And the request includes header X-CT-Signature (HMAC-SHA256 of the body using the shared secret) and X-CT-Idempotency-Key And deliveries to each endpoint retry up to 8 times with exponential backoff over 24 hours until a 2xx response is received And the same idempotency_key is reused across retries so receivers can de-duplicate And only TLS 1.2+ ciphers are accepted and redirects are not followed And per-endpoint delivery logs (status, attempts, last_error) are available and written to the ledger as "webhook.delivery"
Booking Flow Warnings and Post-Grace Automatic Blocking
Given a user attempts to book a class linked to an entity within a warning window but still inside the policy-defined grace period When the user proceeds to checkout Then a non-blocking warning banner/modal displays policy name, entity, expiration date, days remaining, and a link to view requirements And the user can proceed or cancel, and the event "booking.warning_shown" is written to the ledger with booking_id and user_id Given a user attempts to book after the grace period has ended or the item is expired beyond grace When the booking is submitted Then the booking is blocked with error code "COMPLIANCE_BLOCKED" and a user-facing message including the blocking rule and remediation steps And payment authorization/capture does not occur And the event "booking.blocked" is written to the ledger with blocking_rule_id and entity identifiers When an authorized admin applies a policy-defined override Then the system requires an override reason and expiry, allows the booking, and writes "booking.override" to the ledger with auditor fields
Ledger Auditability for Alert Lifecycle and Booking Safeguards
Given any alert lifecycle event, delivery outcome, webhook delivery, or booking safeguard action occurs When the event is processed Then an immutable ledger entry is appended containing: event_type, actor_type, actor_id, timestamp (ISO8601, venue timezone), resource identifiers, old_state, new_state, reason (if any), channel outcomes (if any), previous_hash, and hash And entries are append-only and verifiable via hash chaining And CSV and PDF exports by date range and entity return these fields in chronological order, generating within 60 seconds for up to 50,000 rows And a query for a specific booking returns a trace explaining "why was this blocked?" including governing rule, triggering alert, and decision details within 2 seconds
De-duplication, Resolution, and Timezone-Aware Scheduling
Given an active alert exists for entity_id+rule_id+threshold_days When the scheduler runs again Then it updates the existing alert state instead of creating a duplicate When the expiration date moves outside the threshold window Then the alert is marked resolved with reason "condition_resolved" and a ledger entry "alert.resolved" is written And future reminders for that alert are suppressed Given a DST transition at the venue timezone When the scheduler evaluates thresholds across the transition Then alerts fire at local-day boundaries without duplicate or missed alerts And only one active alert per entity_id+rule_id+threshold_days exists at any time And state changes write "alert.updated" with precise timestamps and timezone context
Accreditor-Friendly CSV/PDF Exports with Integrity Proofs
"As a compliance auditor, I want downloadable CSV/PDF reports in familiar formats with integrity proofs so that I can complete audits quickly and trust the data."
Description

Enable admins to export scoped slices of the compliance ledger and related evidence to CSV and branded PDF with filters by date range, entity, rule, decision outcome, and location. Map output columns and section layouts to common accreditor expectations and include localized timestamps and time zones. For integrity, provide a hash-chain summary and export manifest (export time, filters, chain head, and digital signature if configured). Support large exports via async jobs, progress tracking, and expiring download links. Include redaction options for PII based on role and export purpose; all exports are themselves logged as ledger events.

Acceptance Criteria
CSV Export with Filters and Localization
Given an admin with "Export Ledger" permission selects CSV format, applies filters (date range, entity IDs, rule IDs, decision outcomes, locations), chooses accreditor profile "Profile-CSV-v1", locale "en-US", and time zone "America/New_York" When they click Export Then a CSV file is generated for download And the CSV contains only records that match all selected filters (row count equals the backend filtered result count) And column headers and order exactly match the "Profile-CSV-v1" mapping with no missing or extra columns And all timestamps are rendered in the selected locale and time zone and include ISO 8601 offsets (e.g., 2025-09-15T09:30:00-04:00) And the CSV is RFC 4180-compliant (comma-delimited, quoted as needed) and encoded UTF-8
Branded PDF Export with Accreditor Layout Mapping
Given an admin selects PDF format, applies filters (date range, entity IDs, rule IDs, decision outcomes, locations), chooses accreditor layout profile "Profile-PDF-v2", locale and time zone, and tenant branding is configured When they click Export Then a PDF is generated containing only records that match all selected filters And the PDF displays tenant branding (logo and organization name) in header or cover and page numbers in footer And section order, labels, and fields exactly follow the "Profile-PDF-v2" layout mapping And all timestamps are localized to the selected locale and time zone with visible offset
Export Integrity Manifest and Hash-Chain Summary
Given an export (CSV or PDF) completes Then an export manifest is produced containing: export_id, export_time (UTC and localized), selected filters, format, accreditor profile id, redaction policy id (if any), ledger chain head hash at export time, number_of_records, and signature (if signing configured) And the manifest is attached as an appendix in the PDF and provided as a JSON sidecar file for CSV And the included chain head hash equals the ledger's current chain head at export time And a hash-chain summary is included that allows recomputation of the chain head from the exported records (linear hash proof details and algorithms identified) And if digital signing is configured, the manifest signature verifies with the configured public key; if not configured, the signature field is null
Async Large Export with Progress and Expiring Link
Given an export is requested and the estimated size exceeds 25,000 rows or 50 MB When the export starts Then an asynchronous job is created and the requester sees a status indicator (queued, running with percent_complete, completed with download_url, failed with error_code) And percent_complete updates at least every 5 seconds while running And upon completion, a unique expiring download link is generated and sent via in-app notification/email And the download link expires after the configured TTL (default 72 hours) and returns HTTP 410 after expiry
Role-Based PII Redaction in Exports
Given a user initiates an export and selects an export purpose and the system resolves the user's role When the export runs Then PII fields defined by the redaction policy for the (role, purpose) pair are redacted (CSV fields masked/blanked per policy; PDF fields display [REDACTED]) And exports cannot proceed without a purpose selection when required by policy And users without "Bypass Redaction" permission cannot disable redaction; users with permission can explicitly bypass, which is recorded in the manifest And the manifest records the redaction policy id and a list of redacted fields
Export Event Logged to Compliance Ledger
Given any export is initiated or completed When the system writes the export event Then a ledger event of type EXPORT is created with: export_id, actor_id, timestamp, format, filters summary, accreditor profile id, redaction policy id, record_count, manifest_hash, job_id (if async), outcome (started/completed/failed), and download_link_expiry And the event is included in the hash chain, updating the chain head accordingly And the event is discoverable via ledger search by actor and date within 5 seconds of completion
"Why Was This Blocked?" Explanation API & Admin View
"As a support agent, I want a clear explanation of blocked actions with specific next steps so that I can help users resolve issues on the first contact."
Description

Deliver a low-latency API that composes rule evaluation traces, document statuses, and policy definitions into a human-readable explanation with remediation steps. The response must include the decision outcome, key reasons, missing or expired evidence, responsible actor(s), timestamps, and links to upload documents or request approvals. Provide an embeddable admin UI panel on booking and user detail pages with consistent messaging, search, and deep links. Enforce permissions and redact content based on role. Target p95 latency under 300 ms for recent events via indexed storage and caching.

Acceptance Criteria
Blocked Booking Explanation API — Core Fields and Structure
Given an authenticated Admin requests GET /v1/explanations?booking_id={id} for a blocked booking within the last 30 days When the service composes the explanation Then the response status is 200 and Content-Type is application/json; charset=utf-8 And the payload contains: decision.outcome in ["blocked","allowed"], decision.id (UUIDv4), reasons[] (each with code, message, severity), evidence.missing[] and evidence.expired[] (each with document_type, subject_id, due_at), actors[] (id, role, display_name), timestamps.decided_at (ISO 8601 UTC), policy.version, policy.definition_excerpt, trace.rule_ids[], links.upload_document (URL), links.request_approval (URL) And arrays are present even when empty; timestamps are UTC ISO 8601; IDs are UUIDv4 And requesting a non-existent booking_id returns 404 with error.code = "NOT_FOUND" and a non-empty error.message
Role-Based Access Control and Redaction
Given the Explanation API is requested for a booking that belongs to Organization A When requested by a System Admin of Organization A Then full details are returned with no redaction and HTTP 200 When requested by an Instructor of Organization A not assigned to the booking’s class Then the API returns 403 with error.code = "FORBIDDEN" When requested by an Instructor assigned to the booking’s class Then PII of the student is masked (name -> first_name + initial, email -> partially masked), and document contents are redacted with redaction_reason codes, HTTP 200 When requested by a Front Desk role of Organization A Then decision and high-level reasons are shown, but evidence document filenames and uploader names are redacted, HTTP 200 When requested by an External Auditor with a valid audit scope token Then only policy, rule trace, timestamps, and pseudonymized subject IDs are returned; all PII removed; HTTP 200
Performance SLO — p95 ≤ 300 ms for Recent Events
Given recent events are defined as ledger entries with decided_at within the last 7 days And caches and indexes are warm When executing 10,000 GET /v1/explanations requests over 10 minutes at concurrency 50 against recent events Then server-side p95 latency ≤ 300 ms and p99 ≤ 600 ms measured at the service boundary And HTTP error rate (5xx + timeouts) < 0.1% And cache hit rate for repeated keys ≥ 90% And after a fresh deploy (cold start), p95 returns to ≤ 300 ms within 5 minutes of steady traffic And SLO compliance is visible on a dashboard with alerts when p95 breaches for ≥ 5 consecutive minutes
Embeddable Admin Panel on Booking and User Pages
Given an Admin opens a booking detail page When the panel loads Then it displays the explanation fetched from the API with the same decision.outcome, reasons, and timestamps (message parity validated by matching message_id) And the panel shows status badges (Blocked/Allowed), responsible actors, and remediation links matching the API URLs And the panel loads within 1,000 ms at p95 on a wired connection and renders without layout shift > 0.1 CLS Given an Admin opens a user detail page When the panel loads Then it shows the most recent decision affecting that user with a link to "View all decisions" filtered by user_id And access control and redactions match the API for the current role
Remediation Actions — Upload Document and Request Approval
Given an explanation includes evidence.missing with document_type = "Certification" When the Admin clicks "Upload Document" Then the upload modal opens prefilled with subject_id, document_type, and required policy.version And on successful upload, the modal closes and the panel auto-refreshes within 2 seconds And the refreshed explanation removes the missing evidence item or marks it as pending_review with a new timestamp Given an explanation includes a required approval When the Admin clicks "Request Approval" Then an approval request is created, the approver is notified, and the panel shows a pending_approval badge with approver role and requested_at timestamp When all blocking items are remediated and the decision is re-evaluated Then outcome transitions from "blocked" to "allowed" or the next highest-priority reason is displayed
Search and Deep Links from Panel
Given the admin panel search box is focused When the user searches by booking_id, user_id, email, or document_id Then results return within 500 ms at p95 with up to 50 items paginated And results display decision.outcome, top reason message, decided_at, and entity type When a result is selected Then the panel navigates via deep link to the corresponding explanation view or opens the ledger record in a new tab when requested And the browser URL reflects the selected context with sharable, permission-checked links
Rule Trace Completeness and Policy Versioning
Given an explanation is generated for any decision When retrieving the trace object Then it contains an ordered list of evaluations with rule_id, rule_version, input_snapshot_hash, outcome (pass/fail), evaluated_at (ISO 8601 UTC), and actor attribution where applicable And the trace.order reflects actual execution order and totals match the number of evaluated rules And policy.version in the response matches the version effective at decided_at and includes a definition_excerpt ≤ 500 chars And a downloadable trace_url is provided returning the full trace JSON with the same correlation_id as the main response
Role-Based Access, Redaction, and Retention Controls
"As a data protection officer, I want strict access controls and retention policies on the compliance ledger so that we meet privacy regulations without compromising auditability."
Description

Implement fine-grained RBAC for viewing, exporting, and administering the compliance ledger with roles such as Owner, Admin, Instructor, Staff, and External Auditor. Enforce field-level redaction for PII/sensitive data in APIs, UI, and exports, with configurable policies by tenant and jurisdiction. Provide retention policies (by document type and region), legal hold flags, and deletion workflows that preserve ledger integrity via tombstone/compensation events while keeping hash-chain verifiability. Include audit logs of all access and configuration changes, plus encryption key management and rotation procedures.

Acceptance Criteria
Role Matrix Enforcement for Ledger View/Export/Admin
Given a tenant with roles Owner, Admin, Instructor, Staff, and External Auditor And a populated compliance ledger with entries (rule evaluations, documents, approvals, check-ins) When each role attempts actions: view entries, export CSV/PDF, configure RBAC and policies, apply legal holds, and manage encryption keys Then permissions are enforced as: - Owner: allow all listed actions - Admin: allow view/export/configuration/legal holds; deny encryption key management unless role has KeyAdmin privilege - Instructor: allow view of entries linked to their own classes or documents they uploaded; deny admin/config; exports allowed only if Export privilege present - Staff: allow view limited to assigned location(s) or teams; CSV/PDF export only if Export privilege present; deny admin/config - External Auditor: allow read-only view/export limited to granted scope and time-bound access token; deny any create/update/delete or configuration And out-of-scope attempts return HTTP 403 with error code RBAC_DENY and are audit-logged with actor, role, action, target, and timestamp
Field-Level Redaction Applied Across UI, API, and Exports
Given a tenant redaction policy that marks fields [full_name, dob, email, phone, government_id] as PII And an External Auditor without PII_View privilege and an Owner with PII_View privilege When the External Auditor views a ledger entry in the UI, calls GET /ledger/{id}, or exports CSV/PDF Then the listed PII fields are masked as ***REDACTED*** or omitted as specified by policy in UI, API response, and exported files And the API response includes redaction metadata redacted_fields listing masked/omitted fields And full-text search indexes exclude masked values And automated content scan on the export finds zero occurrences matching PII regex patterns for the masked fields When the Owner performs the same actions Then the PII fields are unredacted and present per policy And policy changes propagate to all channels within 5 minutes and are versioned
Jurisdiction-Specific Redaction Policy Resolution
Given tenant policies exist for jurisdictions Default, US-CA, and US-NY with differing redaction rules And a ledger entry is associated to participant_jurisdiction = US-CA and class_location = US-NY When a Staff user in US-CA requests the entry via API and UI Then the effective policy applied is the most restrictive union of participant and location policies, with conflict rule favoring mask over omit when configured And the applied policy_id, policy_version, and resolved_jurisdictions are recorded in the audit log for the access And the response header X-Redaction-Policy reflects the applied policy_id and version And cache TTL for policy evaluation is <= 5 minutes; a cache purge request results in the new policy being applied on next request
Retention Enforcement with Legal Hold Overrides
Given retention policies: Document = 7 years (US), RuleEvaluation = 3 years (EU), Approval = 5 years (Default) And Document X is eligible for deletion by date but has an active LegalHold flag When the nightly retention job executes Then entries past retention with no legal hold are queued for deletion, and for each a Tombstone event is appended containing reason, actor/system, and timestamp And entries with LegalHold remain intact and are listed in the retention report with hold_reason and hold_owner And an audit record is created for each queued deletion and completed tombstone creation And deletion SLA is met: 99% within 24 hours of eligibility And an upcoming-deletions report lists items due in the next 30 and 90 days
Tombstone and Compensation Events Preserve Hash-Chain
Given a contiguous hash chain across ledger entries E(1)..E(n) And a deletion workflow is initiated for entry E(k) When the workflow completes Then the payload of E(k) is purged per retention, a Tombstone event replaces content with a marker, and a Compensation event C(k) is appended referencing E(k) And the chain verification endpoint GET /ledger/verify returns valid = true and reports zero breaks And recomputing hashes over E(1)..E(n)+C(k) matches stored hash values And CSV/PDF exports display a tombstone marker for E(k) including reason and actor, and do not include the purged payload
Audit Logging of Access and Configuration Changes
Given users perform actions including: view ledger entries, export CSV/PDF, modify RBAC roles, edit redaction policies, set/unset legal holds, and initiate key rotations When any such action occurs Then an immutable audit record is appended with fields: audit_id, timestamp (UTC ISO8601), actor_id, actor_role, action, target_type, target_id, result (success/deny), source_ip, user_agent, correlation_id, previous_hash, hash And audit records are queryable by time range, actor, action, and target with responses under 2 seconds for up to 100k records And audit logs are exportable to CSV within 60 seconds for up to 1M records And attempts to alter or delete existing audit records are rejected (HTTP 409) and a tamper alert is generated And system time synchronization ensures max clock skew across services <= 200 ms; if exceeded, an alert is logged
Encryption Key Management and Rotation
Given all compliance ledger data at rest is encrypted with per-tenant DEKs wrapped by a KMS-managed KEK And rotation policies are configured: KEK every 90 days; DEK annually or on-demand When a KEK rotation is initiated by a KeyAdmin Then all DEKs are re-wrapped successfully without downtime and authorized users retain access to data And old KEKs are disabled within 24 hours and cannot decrypt new writes When a DEK rotation is initiated for a tenant Then affected objects are re-encrypted successfully; key version metadata is updated; and no plaintext is exposed in logs, backups, or exports And failures during rotation trigger alerts, retries with exponential backoff, and detailed audit entries recording key ids, versions, actor, and outcome And key material export is disallowed; access to key operations is restricted to KeyAdmin role and is enforced via RBAC

Gated Waitlist

Waitlists that respect prerequisites. Eligible attendees can join normally; ineligible users get a provisional spot with a deadline and guided steps to qualify. When a seat opens, pay‑to‑claim links only go to those who meet rules (or who completed requirements in time). Keeps last‑minute fills fair and compliant while maximizing utilization.

Requirements

Prerequisite Rules Engine
"As a studio admin, I want to define and enforce prerequisite rules per class so that only qualified attendees can join the waitlist and compliance and safety standards are maintained."
Description

Implements a configurable rules engine to define and evaluate class prerequisites used by the Gated Waitlist. Instructors or studio admins can attach prerequisite criteria per class (e.g., completed prerequisite classes, attendance history, tags/levels, age, signed waivers, active membership/pack, instructor approval, uploaded certifications). The engine evaluates eligibility in real time against user profiles, purchase status, attendance records, and optional external sources via API/webhooks. It returns a clear eligibility decision with human-readable reasons to support guided remediation. Integrates with ClassTap’s class catalog, user accounts, payments/memberships, and CRM integrations. Supports localization of messages, deterministic outcomes, auditability of rule evaluations, and graceful degradation when external data is temporarily unavailable.

Acceptance Criteria
Real-time eligibility evaluation during waitlist join
Given a class has prerequisite rules configured and a user is authenticated When the user opens the class details or taps "Join Waitlist" Then the rules engine evaluates the user's eligibility within 300 ms P95 And returns a machine-readable decision in {eligible, ineligible} And includes a non-empty reasons array when decision is ineligible with stable reason codes and message keys And evaluation uses the latest published ruleSetVersion for the class And no unhandled errors are shown to the user; a structured error or decision payload is always returned
Human-readable reasons and remediation guidance
Given a user is ineligible due to missing waiver and inactive membership When the engine returns the decision Then reasons include codes {waiver_missing, membership_inactive} And each reason includes a messageKey and default English fallback text And each reason optionally includes a remediationHintKey and deepLinkUrl when available And reasons are ordered by rule evaluation order as configured by the admin
Compound rule configuration per class
Given an admin configures prerequisites using AND/OR groups in the Rule Builder When the rule set includes "Completed: Intro to Yoga" AND (Level >= Intermediate OR Tag "Yoga-2") AND Age >= 16 AND Active Membership Then the system validates references and prevents save on invalid class IDs, tags, or fields with inline errors And on successful save, a new immutable ruleSetVersion ID is created and associated to the class in draft state And publishing the rule set updates the class to use the new ruleSetVersion and invalidates caches within 5 seconds
Deterministic outcomes and idempotent evaluations
Given the same user context, inputs, and ruleSetVersion When the engine evaluates eligibility twice within 5 minutes Then both evaluations return the same decision and identical ordered reason codes And evaluation payloads include inputHash values that match across runs And time-based rules resolve "now" using the evaluation timestamp to ensure consistent outcomes
Graceful degradation on external data outages
Given a prerequisite depends on an external certification API experiencing 5xx and high latency And the engine's per-dependency timeout is 300 ms When the engine evaluates eligibility Then the engine uses cached external data if cache age <= 24 hours And if no valid cache exists, decision is ineligible with reason code external_data_unavailable and includes retryAfterSeconds between 60 and 300 And overall evaluation completes within 350 ms P95 despite the outage via timeout and circuit breaker policies
Evaluation audit trail and retrieval
Given any eligibility evaluation occurs When the decision is generated Then an immutable audit record is persisted with classId, userId, ruleSetVersion, decision, reasonCodes, inputHash, dependencyStatuses, durationMs, evaluatedAt, correlationId And the audit record is retrievable by admins via API within 5 seconds of creation and filterable by date range, classId, userId, and decision And exported audit views redact PII per policy (e.g., show userId but not email)
Localization of eligibility messages
Given the user's locale is es-ES When the engine returns reasons Then localized message strings for all reason codes are returned for es-ES And if a translation is missing, the en-US string is returned and a missingTranslation flag is set for that reason And all returned reason messages are UTF-8 and <= 140 characters each
Provisional Waitlist Enrollment with Guided Qualification
"As an ineligible attendee, I want to hold a provisional waitlist spot with clear steps to qualify so that I still have a fair chance to get into the class if I meet the requirements in time."
Description

Enables ineligible users to join a waitlist in a provisional state with a visible deadline to complete requirements. Presents a dynamic checklist of steps to become eligible (e.g., buy membership, enroll in prerequisite session, sign waiver, upload certification), with deep links into ClassTap flows and integrated payment checkout. Tracks progress, re-evaluates eligibility automatically upon step completion, and updates status in real time. Provides clear UI labels for provisional vs fully eligible states across web and mobile booking pages, respecting studio branding. Integrates with notifications to send targeted reminders and with the rules engine for continuous eligibility checks.

Acceptance Criteria
Provisional Enrollment With Visible Deadline
Given an ineligible user attempts to join a class waitlist When they submit the join action Then the system creates a provisional waitlist entry bound to that class and user And a deadline timestamp is assigned per studio policy and displayed in the user's local timezone on the confirmation screen and waitlist detail And the UI shows a "Provisional" state label on both web and mobile booking pages And the backend persists join time, deadline, and ineligibility reasons And eligible users who meet all rules join the waitlist as "Eligible" without a provisional label
Dynamic Eligibility Checklist With Deep Links
Given a provisional user views their waitlist detail Then a dynamic checklist displays only the steps required by the rules engine for that user And each step includes a deep link into the corresponding ClassTap flow (e.g., membership checkout, prerequisite enrollment, waiver signing, certification upload) And payment-related steps open a secure, pre-filled checkout and return the user back to the waitlist view after completion And completed steps are marked "Completed" with timestamp and cannot be re-purchased or re-enrolled from the checklist And optional or already-satisfied steps are not shown
Automatic Re-evaluation And Real-time Status
Given a provisional user completes any required step via a ClassTap flow When the completion event is recorded Then the system re-evaluates eligibility within 10 seconds And if all requirements are met before the user's deadline, the user's state changes to "Eligible" and the "Provisional" label is removed across web and mobile within 5 seconds And the user's waitlist position is preserved And if the deadline elapses before requirements are met, the user is excluded from eligibility-based seat offers until requirements are satisfied
Seat Offer Targeting Honors Eligibility And Deadlines
Given a seat becomes available in a class with a waitlist When pay-to-claim links are generated Then only users who are eligible at generation time or who completed requirements before their individual deadline are included And provisional users who have not met requirements by their deadline are excluded And inclusion order respects waitlist priority (earliest join time first); ties are broken by original waitlist join timestamp And each link is single-use and becomes invalid once the seat is claimed or when the claim window elapses
Targeted Reminders For Provisional Users
Given a user enters the waitlist as provisional Then an email and/or SMS confirmation is sent within 2 minutes including the deadline and the personalized checklist And when the checklist changes, a targeted notification is sent summarizing remaining steps, throttled to a maximum of 1 notification per hour per user And a final reminder is sent 2 hours before the deadline if the user is still provisional And notifications honor the user's communication preferences and opt-in status; unsubscribed users are not sent SMS And links in notifications deep-link the user into the relevant ClassTap flows
Branded, Accessible Eligibility State UI
Given a studio has brand settings configured Then the "Provisional" and "Eligible" labels and checklist components inherit the studio's colors, logo, and typography on web and mobile booking pages And the labels meet WCAG 2.1 AA color contrast (>= 4.5:1) and include accessible names for screen readers And all user-visible strings for these states are localizable; when a locale is set, translated strings are displayed And the UI displays consistently across mobile and desktop viewports without layout clipping or overlap
Admin Eligibility Progress And Audit Trail
Given a studio admin opens the class's waitlist in the dashboard Then they can view each user's eligibility state, deadline, and step-by-step completion status And an event log shows time-stamped entries for provisional enrollment, step completions, re-evaluations, state changes, and notifications sent And admins can export the waitlist with eligibility fields to CSV And manual actions (e.g., admin marking a step complete) are recorded with actor and timestamp
Eligibility Deadline & Hold Expiration Logic
"As a studio manager, I want provisional eligibility deadlines to be enforced and communicated so that the waitlist remains fair and seats are not blocked by users who do not complete requirements."
Description

Introduces configurable time windows for provisional holds, including defaults at the class level and per-user overrides by admins. On expiry, the system automatically releases the provisional spot, updates the queue position, and logs the event. Supports timezone-aware timers, daylight savings, and blackout periods (e.g., do not expire between 11pm–6am local). Sends scheduled reminders ahead of expiry via email/SMS and in-app, and updates the guided checklist countdown. Ensures fairness by preventing indefinite blocking and re-queues users according to defined business rules. Fully auditable with event timestamps for compliance and support.

Acceptance Criteria
Default Hold Duration with Admin Override
Given a class with a default provisional hold duration of 24 hours and a waitlisted ineligible user When no per-user override is applied Then the user’s hold expiry is set to 24 hours from hold assignment in the class’s timezone and is visible to user and admin And the countdown timer displays the remaining time accurate to the minute And reminder jobs are scheduled using the class-level reminder offsets Given a per-user override of 6 hours is applied by an admin after the hold was created When the override is saved Then the expiry is recalculated to 6 hours from the time of override save (unless blackout rules apply) And all previously scheduled reminder jobs are canceled and rescheduled to align with the new expiry And an override event is logged with previous expiry, new expiry, admin ID, and timestamp
Timezone & DST-Safe Expiry
Given the class timezone is America/Los_Angeles and a hold expiry is set for 02:30 local time on a DST transition day When the local wall-clock time 02:30 does not exist due to spring-forward Then the system schedules expiry at the next valid local time (03:00) and logs a DST adjustment with original intended time and applied time Given the class timezone is America/Los_Angeles and a hold expiry is set during a fall-back hour with ambiguous times When local time repeats Then the system expires the hold at the first occurrence after the intended local timestamp, fires exactly once, and logs the UTC instant and local offset used Given any class timezone When displaying expiry to the user or admin Then both local time with timezone abbreviation and the underlying UTC timestamp are available in the audit log
Blackout Window Deferral (11pm–6am Local)
Given a class-level blackout window of 23:00–06:00 local is configured When a hold expiry would land within the blackout window Then the expiry is deferred to 06:00 local on the next valid day in that timezone And the countdown reflects the deferred expiry immediately And the deferral is logged with reason=blackout and prior/new expiry timestamps Given reminders are scheduled relative to expiry When expiry is deferred due to blackout Then reminder times are recalculated from the deferred expiry and rescheduled
Multi-Channel Reminder Scheduling
Given reminder offsets are configured at T-24h and T-1h When a hold is created at 10:00 local with expiry at 10:00 the next day Then email, SMS, and in-app reminders are queued for 10:00 and 09:00 local respectively before expiry, provided the user has verified channels And each reminder send outcome (queued, sent, failed) is logged per channel with timestamp and message ID Given the expiry is changed (override or blackout deferral) When the new expiry is saved Then all pending reminders are canceled and recreated using the same offsets from the new expiry And no duplicate reminders are sent for the same offset and channel
Guided Checklist Countdown Sync
Given a user has a provisional hold with a visible guided checklist and countdown When the user completes a prerequisite step Then the checklist updates status within 5 seconds and the countdown remains unchanged unless the completion triggers a policy-based expiry change Given an admin updates the user’s expiry (override) or the system defers due to blackout When the change is applied Then the countdown updates immediately across web and mobile views And an in-app banner reflects the new exact expiry timestamp and timezone And the change is recorded in the user’s activity feed
Auto-Release on Expiry and Fair Re-Queue
Given a user has a provisional hold that reaches expiry When the expiry instant occurs Then the system releases the held seat atomically so it becomes available to the next candidate without creating double-bookings And the user’s waitlist state changes from Held to Expired And an expiry event is logged with reason=timeout Given re-queue policy is configured as Remove When a user’s hold expires Then the user is removed from the waitlist and notified Given re-queue policy is configured as MoveToEnd When a user’s hold expires Then the user is moved to the end of the waitlist and their new position is recorded and displayed Given re-queue policy is configured as MaintainPositionWithCooldown(30m) When a user’s hold expires Then the user keeps their position but cannot receive a new hold for 30 minutes, enforced by a cooldown flag
Comprehensive Audit Trail
Given any hold lifecycle event (create, override, reminder-queued/sent, blackout-deferral, expiry, re-queue) When the event occurs Then an immutable audit record is written containing: event type, class ID, user ID, actor (system/admin ID), previous and new expiry (if applicable), queue position change (if applicable), channel (for reminders), reason code, local timestamp with timezone, and UTC timestamp And audit records are retrievable via admin UI and API filtered by class, user, date range And records cannot be edited; corrections are appended as new events with linkage to the prior record
Eligibility-Aware Seat Release and Invite Routing
"As a studio owner, I want seat-open invites to go only to users who meet prerequisites so that last-minute fills are fair, compliant, and maximize utilization."
Description

When a seat opens, the system identifies the next eligible waitlisters, prioritizing fully eligible users and those who completed requirements within their deadline. It routes pay-to-claim invites only to those eligible at send time, with batch handling for multiple openings, tie-breaking rules, and configurable invite windows. Integrates with ClassTap’s notification services to send branded, localized emails/SMS with dynamic content (position, expiry, steps remaining). Handles opt-outs, delivery failures, and throttling, and respects compliance guidelines (e.g., TCPA/GDPR). Maintains fairness and transparency by logging routing decisions and exposing invite status in the admin view.

Acceptance Criteria
Single Seat Release with Eligibility Prioritization and Deadline Respect
Given a class with a waitlist containing fully eligible users, provisional users within deadline, provisional users past deadline, and ineligible users When a seat opens and routing begins Then eligibility is re-evaluated at send time And only fully eligible users and provisional users who completed requirements within their deadline are considered And users who missed their provisional deadline or are otherwise ineligible are skipped with reason codes And the first considered user receives a pay-to-claim invite within 60 seconds And the invite token is generated after the eligibility re-check and bound to the user and class instance And no skipped user receives any invite
Invite Window Expiry and Auto-Advance
Given an invite window configured to W minutes and a pending seat offer When the invite reaches its expiry without payment Then the invite is marked Expired with a timestamp And the next eligible waitlister is invited within 60 seconds And attempts to use the expired link are denied with an expiry message And if the invite is claimed before expiry, the seat is reserved and no further invites are sent for that seat And the invite expiry shown to the user equals send time + W minutes in the user’s timezone
Batch Seat Openings and Concurrent Routing
Given S seats open simultaneously and N eligible waitlisters (N ≥ S) When routing invites for the batch Then at most S concurrent pending invites are sent And each invited user is unique with no duplicate active invites And seat inventory and invite issuance are atomic to prevent double-booking And as invites are claimed or expire, the next eligible users are invited until seats are filled or the waitlist is exhausted And initial S invites are dispatched within 60 seconds of the seats opening
Tie-Breaking Among Same-Priority Waitlisters
Given multiple waitlisters in the same priority cohort When determining ordering for invite routing Then ordering is deterministic by: 1) waitlist join timestamp ascending; 2) if equal, eligibility completion timestamp ascending; 3) if still equal, user_id ascending And the final order and applied tie-breakers are logged and reproducible from logs
Branded Localized Notifications with Dynamic Content
Given tenant branding settings and a user’s locale and timezone When generating and sending a pay-to-claim invite Then the message uses tenant branding (logo, colors, sender) and an approved template And content is localized to the user’s locale with fallback to tenant default And dynamic fields include waitlist position at send, invite expiry timestamp in user’s timezone, remaining steps (if any), and a unique pay-to-claim link And all template placeholders are populated (no raw keys rendered) And per-channel delivery status is recorded (Queued, Sent, Delivered)
Opt-Outs, Delivery Failures, Throttling, and Quiet Hours Compliance
Given user channel consents, tenant policies, and provider limits When dispatching invites Then messages are sent only to channels with documented consent; opted-out channels are skipped and logged as Skipped (Opted Out) And if the primary channel fails definitively, a fallback consented channel is attempted within 2 minutes; if none, the user is marked Skipped (Delivery Failure) and the next eligible user is invited And sending respects configured rate limits and provider throttling And SMS are not sent during configured quiet hours; invites are scheduled for the next allowed window and expiry is calculated from actual send time And required compliance elements (e.g., unsubscribe/STOP instructions) are included where applicable and no unnecessary personal data is sent
Admin Audit Log and Invite Status Visibility
Given an admin viewing the class waitlist routing activity When examining seat-open events and invitations Then for each seat-open event the admin sees: timestamp, cause, considered users with reason codes, invites with channel, localization, consent basis, and current status (Pending, Sent, Delivered, Claimed, Expired, Skipped [reason], Undeliverable) And all status transitions are timestamped and attributed to system or user actions And the admin can filter and export the audit log for the class And ordering and routing decisions can be reconstructed from log entries
Secure Pay-to-Claim Links with Atomic Booking
"As an attendee, I want a secure, single-use pay-to-claim link that guarantees my spot upon payment so that I can check out quickly without errors or double-bookings."
Description

Generates single-use, time-bound, user-bound claim links that lead directly to a secured checkout for the released seat. Enforces atomic booking: on payment success, the seat is reserved and the waitlist and class inventory update in a single transaction to prevent double-bookings. Includes token validation, rate limiting, device-agnostic access, and detection of link sharing. Supports payment holds/captures, refund and failure handling, idempotency keys, and instant confirmation messages. Integrates with ClassTap Payments, receipts, and reminder workflows, ensuring the spot is only confirmed for users who remain eligible at payment time.

Acceptance Criteria
Single-Use, Time-Bound, User-Bound Claim Links
Given a seat is released to user U with a 30-minute claim window When the system generates a pay-to-claim link Then the link contains a signed, non-guessable token bound to user U and class instance C with an absolute expiry of 30 minutes And the link can be opened on any device but requires authentication as user U before checkout is accessible And the first successful payment or manual revoke invalidates the token immediately And after expiry or invalidation, visiting the link returns an expired/claimed state and does not expose checkout
Atomic Booking on Payment Success
Given multiple valid claim attempts occur concurrently for the same seat When payment for one attempt succeeds Then seat assignment, waitlist advancement, and inventory decrement are committed atomically with the payment record And other in-flight attempts are blocked before capture with a seat-unavailable error And idempotency keys ensure retries or webhook replays do not create duplicate enrollments or charges And exactly one confirmed booking is present in the roster
Token Validation and Tamper Resistance
Given a claim link is accessed When the token is missing, expired, revoked, or fails signature/nonce validation Then checkout is denied with a non-revealing error and no state changes occur And tokens cannot be reused after success, cancel, or expiry And token payload contains no PII and is only accepted over HTTPS
Rate Limiting and Abuse Protection
Given repeated requests hit the claim verification or checkout-init endpoints When requests exceed 10 per minute per IP or 20 per 15 minutes per token Then subsequent requests are throttled with 429 and no token or booking state changes occur And if 3+ distinct authenticated accounts attempt to use the same token within 15 minutes, the token is revoked and the intended user U is notified of suspected sharing
Payment Holds, Captures, Refunds, and Failures
Given checkout supports authorize-and-capture and immediate capture modes When authorization succeeds with deferred capture Then the seat is reserved and capture executes automatically at the configured time with idempotency enforcement And on authorization or capture failure, no enrollment is created, the seat is returned to the queue, and the next eligible candidate is notified And refunds issued due to cancellation or error are processed via ClassTap Payments and reflected in receipts and booking status
Instant Confirmation, Receipts, and Reminder Workflows
Given payment succeeds and the booking is confirmed When the transaction commits Then an in-app confirmation is displayed and email/SMS confirmation with receipt is sent within 5 seconds And the class roster and user’s bookings update immediately And reminder notifications are scheduled according to class policy
Eligibility Re-Validation at Payment Time
Given prerequisites and conflict checks may change over time When the user proceeds to pay via a claim link Then eligibility and conflict checks are re-validated at payment time And if the user is no longer eligible or a conflict exists, charging is blocked and guidance to regain eligibility is shown And only users who pass re-validation can complete payment and receive confirmation
Admin Oversight & Overrides with Audit Trail
"As support staff, I want tools to review and override waitlist eligibility and invites with full history so that I can resolve edge cases and keep classes full while maintaining accountability."
Description

Provides an admin console to view the waitlist with eligibility status, reasons, deadlines, invite history, and conversion outcomes. Enables permitted staff to adjust deadlines, re-order positions with justification, mark users as eligible, revoke provisional status, resend or cancel invites, and manually enroll a user. Includes role-based access controls, bulk actions, export, and a complete audit trail of changes and automated decisions to support dispute resolution and compliance. Integrates with ClassTap’s reporting and user management, and surfaces key metrics (fill rate, conversion by eligibility state, average time-to-fill) for operational insight.

Acceptance Criteria
Admin Waitlist Overview: Eligibility, Deadlines, History
Given I am an admin with permission to view a class waitlist When I open the Waitlist Overview for a specific class instance Then I see a table listing all waitlisted users with columns: Position, User Name, Eligibility Status (Eligible | Provisional | Ineligible), Reason, Prerequisite Deadline (date/time, timezone), Last Invite Sent (date/time), Invite Status (Pending | Cancelled | Expired | Claimed), Conversion Outcome (Enrolled | Declined | No Response), Notes And for any user I can expand to view an invite history timeline with all invites sent, resent, cancelled, expired, and claimed events in chronological order with timestamps And the data updates within 5 seconds of changes made by any user or automation And positions are sequential starting at 1 with no gaps after any change And clicking a User Name opens the user’s profile in User Management in a new tab And I can export the current waitlist view to CSV with the displayed columns and applied filters
Role-Based Access Controls: Permissions Enforcement
Given the platform defines roles (Owner, Manager, Staff, Read-only) and custom permissions When a user without the "Manage Waitlist" permission views the waitlist Then controls for deadline adjustments, reordering, eligibility/provisional changes, resend/cancel invites, manual enroll, bulk actions, and export are hidden or disabled When a user attempts a forbidden action via API or UI Then the system returns HTTP 403 with an error message, and an audit entry is recorded with attempted action, actor, and reason When a user with only "Export Audit" permission accesses the module Then they can download audit exports but cannot modify any waitlist data
Deadline Adjustments: Single and Bulk with Justification
Given I have "Manage Waitlist" permission When I select one or more provisional users and set a new prerequisite deadline with a required justification (minimum 10 characters) Then the deadlines update, countdown timers recalculate, affected users receive notifications via configured channels (email/SMS) with the new deadline, and an audit entry is recorded per user capturing previous and new deadlines and justification And deadlines cannot be set earlier than current time + 5 minutes or later than 30 days from now; invalid inputs are blocked with validation messages And bulk updates provide a summary of successes and per-user failures with reasons
Waitlist Reordering with Policy Guardrails
Given I have "Manage Waitlist" permission When I reorder users via drag-and-drop or by moving a user to position N and provide a required justification Then the new positions are saved, invitation order is recalculated, and an audit entry records previous and new positions and justification And if a conflicting change occurred since I loaded the list, I am prompted to refresh and review before saving And reordering that places ineligible users ahead of eligible users requires explicit confirmation with justification and is flagged as a policy override in the audit
Eligibility Overrides and Provisional Revocation
Given I have "Manage Waitlist" permission When I mark a user Eligible or revert a user to Provisional with a required justification Then the eligibility status updates immediately, prerequisite checks are bypassed or reinstated accordingly, and automated invitation eligibility is recalculated And bulk eligibility changes are supported with per-user success/failure results and corresponding audit entries And before confirmation, the UI displays which prerequisite rule(s) are being overridden (e.g., CPR Cert on file)
Invite Management and Manual Enrollment Controls
Given I have "Manage Waitlist" permission and the class has available seats (or I have override capacity permission) When I resend an invite to selected users Then only users currently Eligible receive a pay-to-claim link, Provisional users receive guidance without a claim link, and delivery status is logged When I cancel an outstanding invite Then the invite link is invalidated, the user is notified, the queue advances to the next eligible user, and the audit captures actor and reason When I manually enroll a user Then capacity rules are enforced; if capacity is full I must confirm an override with justification to proceed, double-booking is prevented, the user is enrolled, removed from waitlist, and Conversion Outcome updates to Enrolled And bulk resend/cancel actions provide per-user delivery outcomes and errors
Audit Trail, Export, and Operational Metrics Integration
Given any automated decision or admin action affects the waitlist When I view the audit trail Then each entry shows: timestamp (UTC ISO 8601), actor (user or system), role, IP/device fingerprint, entity, action type, previous value, new value, justification (if provided), related rule/automation id, and correlation id; entries are immutable and in chronological order When I export audit data Then I can export CSV and JSON with a selectable date range and fields; exports include a checksum and metadata; exported totals match on-screen counts When I open the Metrics panel or Reports Then I see fill rate, conversion by eligibility state, and average time-to-fill for a selectable date range; definitions are shown in-tooltips; metrics reflect underlying events within 1% variance

AutoClaim

Let waitlisted attendees opt into automatic seat claiming within a chosen timeframe and price cap. When a seat opens, ClassTap instantly charges the saved payment method and confirms the spot—no taps required. Providers get reliable fills and fewer abandoned holds, while attendees never miss out due to timing.

Requirements

AutoClaim Opt-in with Time Window and Price Cap
"As a waitlisted attendee, I want to opt into automatic seat claiming with a time window and price cap so that I can secure a spot without constantly checking my phone."
Description

Enable waitlisted attendees to enroll in AutoClaim by selecting a claim window (e.g., until class start minus X minutes) and setting a maximum price they are willing to pay. The flow collects explicit consent to auto-charge a saved payment method and auto-confirm the spot if a seat opens within the defined parameters. Integrates into existing waitlist and booking UIs (web/mobile), showing the configured window, price cap, countdown, and the ability to edit or cancel before the window ends. Enforces timezone correctness, accessibility standards, and clear disclosures about charges and cancellation policy. Requires a saved payment method or prompts to add one. Persists preferences at the waitlist-entry level and optionally as user defaults. Provides APIs and backend persistence to store auto-claim state, window bounds, and cap, and guards against enabling AutoClaim for ineligible classes (e.g., invite-only).

Acceptance Criteria
Opt-in Flow with Time Window and Price Cap (Web & Mobile)
Given a logged-in attendee is viewing an eligible class with an open waitlist on web or mobile When they choose Join Waitlist, enable AutoClaim, select a claim window of “until class start minus X minutes” within the allowed range, and enter a price cap in the class currency formatted to two decimals Then the system validates that X is within policy bounds and the cap is a non-negative amount in the correct currency And the UI shows a summary with the exact window end timestamp labeled with the class timezone and the price cap And a waitlist entry is created with AutoClaim enabled and the summary is visible on the confirmation screen
AutoCharge Consent and Saved Payment Enforcement
Given the attendee has no saved payment method When they attempt to enable AutoClaim Then the system blocks enablement and prompts the attendee to add and verify a payment method before proceeding Given the attendee has a saved payment method but has not provided explicit consent When they attempt to enable AutoClaim Then the system requires checking a consent box containing the cap amount and window end and prevents submission until checked Given a saved payment method exists and explicit consent is provided When a seat opens within the configured window and the total class charge (price plus taxes/fees) is less than or equal to the cap Then the system charges the saved method immediately and confirms the booking, sending email and SMS confirmations Given the seat opens within the window but the total charge exceeds the cap or the payment is declined or requires SCA that cannot be completed automatically When AutoClaim evaluation runs Then the system does not claim the seat and notifies the attendee via email and SMS
Real-time Countdown, Edit, and Cancel Controls
Given AutoClaim is enabled for a waitlist entry When the attendee views the entry Then a countdown to the window end is displayed, updates at least once per minute and at most once per second, and includes the timezone label Given AutoClaim is enabled and the window has not ended When the attendee edits the time window or price cap Then inputs are validated, changes are persisted within 1 second, the countdown and summary update accordingly, and an audit record is created Given AutoClaim is enabled and the window has not ended When the attendee cancels AutoClaim Then AutoClaim is disabled immediately, the countdown stops, the waitlist entry remains active, and a cancellation confirmation is shown Given the window has ended When the attendee attempts to edit AutoClaim Then edits are blocked and a message explains the window has expired
Timezone Accuracy and Class Start Cutoff
Given a class is scheduled in a specific IANA timezone and an attendee is in any timezone When the attendee selects “until class start minus X minutes” Then the window end is computed in the class timezone as class start minus X minutes, stored as a UTC timestamp, and displayed with the class timezone abbreviation Given a DST transition occurs between now and class start in the class timezone When the window end is calculated Then the UTC timestamp aligns with class start minus X minutes in the class timezone across the DST change Given the current time passes the window end When the page refreshes or the countdown elapses Then AutoClaim is automatically disabled and the UI updates within 10 seconds
Accessibility and Disclosure Compliance
Given a keyboard-only or screen-reader user is interacting with AutoClaim on web or mobile web When navigating to, through, and from the AutoClaim controls and disclosures Then all controls are reachable by keyboard in a logical order, have visible focus states, correct roles and labels, and meet WCAG 2.2 AA contrast ratios Given the countdown is updating When a screen reader is active Then updates are exposed via a polite live region and announced no more than once per minute Given the AutoClaim consent UI is displayed When the attendee reviews the disclosures Then the consent text clearly includes the cap amount, the exact window end timestamp, and a working link to the provider’s cancellation policy
Backend Persistence and API State Integrity
Given AutoClaim is enabled for a waitlist entry When the server persists the configuration Then fields enabled, payment_method_id, window_end_utc, price_cap_amount, currency, consent_version, consent_timestamp, and audit metadata are stored and retrievable Given a client submits the same enable request with an idempotency key multiple times When the API processes the requests Then only one configuration is created or updated and identical responses are returned Given concurrent enable, edit, or disable requests are received When the API processes them Then the latest write wins based on server timestamp and no partial or conflicting state is persisted Given a client requests the waitlist entry state When the API responds Then it returns the current AutoClaim state including window bounds and cap and masks payment details per PCI requirements
Eligibility Guardrails and Error Handling
Given a class is invite-only, private, pass-only, comped, or AutoClaim is disabled by the provider When an attendee views the class Then the AutoClaim option is not displayed, and direct API attempts to enable it return 403 or 422 with a specific error code Given an attendee already has a confirmed booking for the same class or a time conflict with another confirmed booking When a seat opens and AutoClaim evaluation runs Then the system does not charge, does not claim the seat, and notifies the attendee Given the saved payment method is removed or expires before the window ends When AutoClaim evaluation runs Then AutoClaim is disabled for that waitlist entry and the attendee is notified Given AutoClaim successfully confirms a seat When another seat opens for the same class Then the attendee’s waitlist entry is marked fulfilled or removed to prevent double-booking
Secure Payment Vaulting & SCA Handling
"As a waitlisted attendee, I want my saved payment method securely charged automatically and handle any necessary verifications so that my seat is confirmed instantly when available."
Description

Store and use tokenized payment methods (via PSP vaulting) for AutoClaim with PCI DSS SAQ-A compliance. Validate the payment method during opt-in (e.g., $0/low-value authorization) and capture the payment instantly when a seat opens. Implement 3D Secure/SCA step-up flows that can be completed asynchronously when required, including links in notifications to authenticate within a short hold period. Ensure idempotent charge attempts, retries with backoff on transient errors, and graceful fallback (skip to next candidate) on hard declines or expired cards. Support region-specific mandates, refunds, and receipts through the PSP. Log PSP response codes for troubleshooting and analytics.

Acceptance Criteria
PSP Vaulting & Payment Method Validation at AutoClaim Opt-In
Given a user opts into AutoClaim and adds a payment method via PSP-hosted fields or redirect When the PSP returns a token and validation authorization completes Then the system stores only PSP token, brand, last4, and expiry; no PAN/CVV or sensitive data is stored or logged And a $0 or $1 authorization (or PSP-supported minimal validation) is performed and automatically reversed And on validation failure, opt-in is blocked and the user is shown an actionable error and a retry option And SAQ-A scope is maintained: payment inputs are PSP-hosted and no ClassTap-controlled code handles PAN/CVV
Instant Capture, Booking, and Idempotency on Seat Release
Given a seat opens and a waitlisted candidate is within their opt-in window and the class price is less than or equal to the candidate’s price cap When the system attempts to charge using the stored PSP token Then the charge is captured immediately and a single booking is created atomically with inventory decremented to prevent double-booking And an idempotency key (waitlistEntryId + releaseId) is used for all PSP charge calls and booking writes And duplicate release events or PSP webhooks within 24 hours result in exactly one successful charge and exactly one booking And a confirmation notification is sent within 15 seconds and the PSP receipt URL is stored on the booking
Asynchronous 3DS/SCA Challenge with Timed Hold
Given the PSP indicates SCA is required for a candidate When a seat opens Then a 10-minute hold on the seat is created for that candidate and an email/SMS with a deep link to authenticate is sent within 10 seconds And if the user completes SCA successfully within the hold, the payment is captured and the booking is confirmed And if SCA fails or the hold expires, the seat is released to the next candidate and the user is notified of failure within 15 seconds And audit logs include SCA status, challenge outcome, timestamps, and PSP response codes
Retry Strategy with Exponential Backoff for Transient Failures
Given a transient error occurs during authorization or capture (e.g., network timeout, PSP 5xx, retryable soft decline) When processing an AutoClaim charge Then the system retries up to 3 times at approximately 1 minute, 3 minutes, and 7 minutes backoff intervals, reusing the same idempotency key And retries occur only within the candidate’s opt-in window and before class start time And if all retries are exhausted or the window closes, the candidate is marked skipped for this release and the next candidate is attempted And each attempt logs PSP response codes, retry reason, attempt number, and final outcome
Hard Decline and Expired Card Fallback to Next Candidate
Given a hard decline or expired card is returned by the PSP (e.g., stolen_card, do_not_honor, expired_card) When attempting to charge for a released seat Then no further retries are performed for that release and the candidate is marked Skipped-Hard-Decline And a notification with an update-payment link is sent to the candidate within 30 seconds And the next eligible waitlist candidate is attempted within 5 seconds And the decline reason code is stored for analytics and support
Region-Specific Mandates and Data Requirements
Given the candidate’s billing country falls under a mandate (e.g., EEA/UK PSD2 SCA, India RBI, Brazil mandates) When validating or charging the payment method Then the required SCA flows, mandate flags, and payer data are sent via the PSP and collected using PSP-hosted UI And for out-of-scope regions, no SCA challenge is initiated unless the PSP requests a step-up And test transactions per region demonstrate successful capture when compliant and appropriate handling when authentication is declined or times out
Refunds, Receipts, and Reconciliation for AutoClaim Bookings
Given an AutoClaim booking is canceled under a refundable policy When a refund is triggered by the provider or system Then a PSP refund is created within 5 seconds, the booking status updates to Refunded, and a refund confirmation with receipt URL is sent to the attendee And partial refunds are supported by specifying an amount; the PSP refund id and status are stored on the booking And when PSP webhooks update the refund status, the system reconciles to the final state and logs PSP response codes
Instant Seat Claim Orchestration
"As a platform operator, I want a robust seat-claim engine that allocates openings fairly and atomically so that we prevent double-bookings and fill classes reliably."
Description

Implement an event-driven engine that listens for seat-available triggers (cancellations, hold expirations, capacity increases) and evaluates eligible AutoClaim candidates in priority order (e.g., waitlist position, provider rules). Acquire a distributed lock on the class instance and seat allocation to ensure atomicity and prevent double-bookings. For each candidate within their active window and under price cap, attempt payment capture; on success, immediately create the booking and release the lock; on failure, proceed to the next candidate. Support configurable hold durations when SCA is needed, concurrency safety across nodes, idempotent operations, rate limiting, and transparent error handling. Expose operational metrics and logs for monitoring throughput, success rate, and time-to-fill.

Acceptance Criteria
Cancellation Trigger AutoClaim
Given a class instance with an open seat created by a cancellation and at least one AutoClaim-enabled waitlist candidate within their active window and at or below their price cap When the seat-available event is emitted Then within 1 second the engine begins evaluation and payment for the highest-priority eligible candidate And upon successful payment capture, the booking is created atomically, seat inventory decremented, the candidate’s waitlist entry marked fulfilled, and the distributed lock released within 100 ms of booking persistence And no manual user action is required to complete the booking And only a single booking is created for the seat
Priority Ordering Respects Provider Rules and Eligibility
Given a waitlist with multiple candidates and provider-defined priority rules When the engine evaluates candidates for an open seat Then candidates are ordered deterministically by waitlist position and applicable provider rules; ties are resolved FIFO by waitlist join timestamp And candidates outside their active window or above their price cap are skipped without payment attempts And the decision order and skip reasons (with candidate IDs) are logged for audit And the first successfully charged candidate is booked; all lower-priority candidates remain on the waitlist
Distributed Lock Prevents Double-Booking Across Nodes
Given two or more engine nodes receive the same seat-available trigger concurrently When they attempt to acquire the class-instance seat lock Then exactly one node obtains the lock and proceeds; others do not perform payment attempts while the lock is held And at most one booking record is persisted for the seat And the lock has a max TTL of 30 seconds and auto-releases on process crash And on lock contention, non-winning nodes back off with jitter and retry up to 3 times within 5 seconds And all lock acquisition and release events are logged with correlation IDs
Payment Capture Success and Failure Handling
Given an eligible candidate is selected under their price cap and active window When payment capture succeeds Then booking and payment records are persisted atomically (or with compensating logic) to guarantee exactly one booking per seat And the distributed lock is released immediately after persistence Given an eligible candidate’s payment is declined, times out, or the gateway errors When the attempt completes/fails Then no booking is created for that candidate, no charge is settled, the failure is logged with reason code, and the engine proceeds to the next eligible candidate within 500 ms And if no further eligible candidates remain, the seat remains available and the lock is released
SCA Flow with Configurable Hold Duration
Given a candidate whose payment requires SCA When a seat becomes available Then the engine places a hold on the seat for the configured SCA duration (default 10 minutes; configurable 1–20 minutes) And an SCA-required state is recorded for that candidate; no other candidates are attempted during the hold Given the candidate completes SCA within the hold window When payment is authorized Then the booking is created and the hold is cleared Given the candidate does not complete SCA within the hold window When the hold expires Then the hold auto-releases, the expiration is logged, and the engine proceeds to the next eligible candidate
Idempotency and Retry Safety
Given duplicate seat-available events or retried payment attempts with the same idempotency key within 24 hours When processed by the engine Then no duplicate charges occur and at most one booking is created, with subsequent duplicates returning the original outcome reference Given a node crash after payment capture but before booking persistence When the workflow resumes Then the system detects the prior capture and completes booking creation exactly once, or issues an automatic void/refund within 5 minutes if booking cannot be completed, with an audit log linking actions
Rate Limiting, Metrics, and Observability
Given high event volume When processing AutoClaim evaluations Then the engine enforces configurable rate limits per tenant and per class instance (default 60 payment attempts/min/tenant and 10 attempts/min/class), delaying or shedding excess with exponential backoff And counters for throttled attempts are emitted When operating under normal and peak load Then metrics are exposed for events processed, payment attempts, booking successes/failures, average and p95 time-to-fill, and lock contention rate, labeled by tenant and class instance, and available to monitoring within 15 seconds When errors occur (declines, timeouts, lock failures) Then structured logs include correlation IDs, event ID, candidate ID, decision reason, and timing spans; alerts are emitted when failure rate exceeds 50% over a 5-minute window
Automated Notifications & Receipts
"As an attendee, I want clear notifications about AutoClaim actions and required steps so that I know when I’ve secured a seat or need to intervene."
Description

Provide SMS and email notifications for key AutoClaim events: opt-in confirmation, successful seat claim, payment failure, SCA authentication required, window expiring soon, price cap exceeded (not attempted), opt-out confirmation, and provider-side overrides. Templates are white-label branded per provider, localized, and customizable with variables (class name, time, price, links). Include deep links to authenticate payments, manage settings, or cancel AutoClaim. Ensure compliance with messaging regulations (e.g., TCPA/CAN-SPAM), provide unsubscribe preferences where applicable, and attach receipts or link to hosted receipts after successful charges. Track delivery/open/click metrics for operational insights.

Acceptance Criteria
AutoClaim Notification Events Coverage and Dispatch Timing
Given provider has AutoClaim enabled and a user has provided SMS/email consent, When any of the following events occur: opt-in confirmation, successful seat claim, payment failure, SCA required, window expiring soon, price cap exceeded (not attempted), opt-out confirmation, provider-side override, Then a notification is enqueued for email and for SMS (if SMS consent exists) within 5 seconds of the event. Given a notification is enqueued, When processed, Then it is delivered by the channel provider within 30 seconds in 95% of cases and retried on transient failures, with no duplicate sends for the same event ID. Given any event is triggered multiple times with the same event ID, When notifications are processed, Then idempotency ensures a single send per channel. Given a user lacks SMS consent, When an event occurs, Then only email is sent and no SMS is attempted. Given any notification is sent, When stored, Then an auditable record of event type, user, channel, timestamps, and delivery status is persisted.
Successful Seat Claim Notification and Receipt Delivery
Given AutoClaim captures payment successfully, When the seat is confirmed, Then email and SMS notifications are sent within 10 seconds containing: class name, start time in user timezone, location or virtual link, amount charged, last4 of payment method, and a link to manage AutoClaim settings. Given a successful charge, When sending the email, Then the email includes either a PDF receipt attachment or a link to a hosted receipt accessible without login via an unguessable token. Given the receipt is generated, When viewed, Then it displays provider name and branding, itemized price/taxes/fees, receipt ID, date/time, and currency, and is printable and mobile-friendly. Given the SMS is sent, When received, Then it contains a shortened link to the hosted receipt that resolves within 2 seconds in 95% of cases.
Payment Failure and SCA Required Action Flows
Given a payment authorization fails with SCA required, When the event is raised, Then email and SMS include a secure deep link to authenticate the payment, the amount, the applicable deadline, and class details. Given the SCA deep link is clicked, When authentication completes successfully before timeout, Then the system captures payment, confirms the seat, and sends the successful seat claim notification; otherwise it sends a payment failure notification and releases any hold. Given a payment fails for non-SCA reasons, When the event is raised, Then notifications include the failure status (generic reason), a link to update payment method, and state that no charge was made. Given a single payment attempt, When determining which message to send, Then the user receives at most one of “SCA required” or “payment failure” for that attempt.
Price Cap Exceeded (No Attempt) Notification
Given a user's AutoClaim price cap is below the current seat price when a seat opens, When the system evaluates the claim, Then no authorization attempt is made and the seat is not claimed for that user. Given the above condition, When notifying, Then email and SMS are sent within 10 seconds including current price, user’s price cap, class name/time, and links to adjust price cap or cancel AutoClaim. Given multiple seats in the same class occurrence open within 60 seconds, When evaluated, Then a single consolidated notification is sent to the user.
Opt-In and Opt-Out Confirmations with Compliance
Given a user opts into AutoClaim notifications, When the opt-in is saved, Then email and SMS confirmations are sent including provider brand name, what they opted into, how to manage preferences, and SMS instructions for STOP/HELP. Given a user replies STOP to an SMS or uses the unsubscribe link in email, When processed, Then the user's notification preferences are updated within 60 seconds, a final confirmation is sent, and no further non-essential messages are delivered via the opted-out channel. Given emails are sent, When composed, Then they include a valid provider postal address and an unsubscribe or preferences link to satisfy CAN-SPAM and equivalent regulations. Given SMS messages are sent, When composed, Then they include the provider name and comply with TCPA guidelines, including honoring HELP/STOP keywords and carrier/jurisdiction keyword policies.
Localization, Branding, and Variable Substitution
Given a user's locale and preferred language are known, When notifications are generated, Then templates are selected in that locale with fallback to the provider default and then to English if unavailable. Given a user's timezone and the provider currency are known, When rendering variables, Then start times are displayed in the user’s timezone and amounts are formatted in the provider currency with appropriate symbols and decimals. Given template variables {provider_name}, {class_name}, {start_time_local}, {price}, {manage_link}, {sca_link}, {receipt_link}, When rendered, Then all placeholders are resolved with correct values and no raw tokens remain. Given a provider updates branding, When new notifications are sent, Then emails reflect updated branding (name/logo/colors) and SMS includes the updated provider name.
Delivery and Engagement Metrics Tracking
Given any notification is sent, When delivery updates are received, Then the system stores per-message delivery status (queued, delivered, bounced/failed) with timestamps and error codes. Given emails are opened or links are clicked, When measurable, Then opens (email) and clicks (email and SMS) are recorded with timestamps and attribution to message ID and user. Given metrics are stored, When a provider queries the dashboard or API, Then they can filter by date range, class, event type, and channel, and export CSV for at least the last 90 days. Given a message hard-bounces, When detected, Then the user’s channel is automatically suppressed for 7 days and the event is surfaced in the provider’s operational log.
Provider Controls & Policy Governance
"As a provider, I want granular control over AutoClaim policies and defaults so that the feature aligns with my pricing, cancellation, and customer experience strategy."
Description

Add provider-facing settings to enable/disable AutoClaim per class, series, or account default; configure default claim windows, price cap rules, SCA hold duration, and waitlist priority logic. Allow providers to set cancellation/refund rules specific to AutoClaim, blackout periods (e.g., no auto-claims within N minutes of start), and per-user limits (e.g., max auto-claims per week). Provide visibility into current AutoClaim enrollments, queued candidates, fill outcomes, and override actions (manually claim, skip, or pause). Enforce role-based access control, audit changes to settings, and expose reporting/exports for policy compliance.

Acceptance Criteria
AutoClaim Toggle & Inheritance (Account > Series > Class)
Given account default AutoClaim=On When a provider creates a new series Then the series AutoClaim setting defaults to On Given series AutoClaim=Off When a provider creates a new class within that series Then the class effective AutoClaim is Off unless explicitly overridden at the class level Given class AutoClaim override=On When a consumer views the class booking page Then the AutoClaim opt-in control is visible Given effective AutoClaim=Off for a class When a consumer views the class booking page Then the AutoClaim opt-in control is hidden Given a provider changes the account default AutoClaim value When a series or class has an explicit override Then the override remains unchanged and the effective status reflects the override source Given a provider views class settings When checking AutoClaim status Then the UI shows Effective value and Source level (Account/Series/Class)
Default Claim Window & Price Cap Enforcement
Given provider default claim window ends 30 minutes before class start and default price cap=$30 When a candidate opts in using defaults Then the system auto-claims only if a seat opens at least 30 minutes before start and the price is less than or equal to $30 Given a seat opens 45 minutes before start at price $25 for an opted-in candidate with cap $30 When processing auto-claim Then the seat is auto-claimed and payment of $25 is captured Given a seat opens 45 minutes before start at price $35 for an opted-in candidate with cap $30 When processing auto-claim Then the system skips auto-claim and records PriceCapExceeded for that candidate Given a seat opens 20 minutes before start When evaluating auto-claim Then the system does not auto-claim due to OutsideClaimWindow and the candidate remains queued Given the provider updates the default price cap to $25 When a new AutoClaim opt-in is created afterward Then the opt-in form pre-fills $25 as the price cap
SCA Hold Duration Configuration & Expiry Handling
Given provider SCA hold duration=7 days When a candidate opts in with a payment method requiring SCA Then a payment authorization hold is created with a 7-day expiry and status=Active Given an SCA hold has expired before a seat opens When a seat later becomes available Then the system does not auto-claim, the candidate is notified to re-authenticate, and the event is logged as SCAHoldExpired Given a seat opens while the SCA hold is active When processing auto-claim Then the system captures the authorized amount up to the candidate's price cap without additional user action and marks enrollment Confirmed Given the provider changes SCA hold duration to 10 days When new AutoClaim opt-ins occur after this change Then new holds use a 10-day expiry and existing holds retain their original expiry
Configurable Waitlist Priority Logic
Given provider selects priority rule=FIFO When a seat opens Then the next candidate evaluated is the one with the earliest waitlist join timestamp who also meets window and price cap constraints Given provider selects priority rule=MembersFirstThenFIFO When a seat opens Then candidates with membership status are ordered ahead of non-members and then sorted by waitlist join timestamp Given multiple candidates meet all constraints simultaneously When applying the priority rule Then the system uses a deterministic tie-breaker based on join sequence and logs the evaluated ordering Given an auto-claim is executed When writing the audit record Then the audit includes the applied priority rule and the evaluated candidate order
Blackout Period Before Class Start
Given provider sets AutoClaim blackout=20 minutes When current time is within 20 minutes of class start Then the system suppresses auto-claim attempts and logs BlackoutActive Given an auto-claim is suppressed by blackout When a provider with permission performs a manual claim override Then the manual claim succeeds, the queue updates immediately, and an Override record is created with reason=BlackoutBypass
Per-User AutoClaim Limits
Given provider sets max auto-claims per user per rolling 7 days=3 When a user's 4th auto-claim would be executed within the same 7-day window Then the system blocks the auto-claim, notifies the user and provider, and logs LimitExceeded Given an auto-claim was blocked by the weekly limit When a provider with permission applies a one-time override for that user Then the next qualifying seat opening auto-claims once and the override is recorded with actor, timestamp, and scope Given the rolling 7-day window advances When the user has fewer than 3 successful auto-claims in the last 7 days Then auto-claims execute without limit-related errors
Dashboard Visibility, Overrides, RBAC, Audit & Exports
Given a provider user with Owner or Admin role When viewing the AutoClaim dashboard Then they can view and edit account defaults and series/class-level settings Given a provider user with Manager role When viewing the AutoClaim dashboard Then they can view and edit series/class-level settings but cannot edit account defaults Given a provider user with Staff role When viewing the AutoClaim dashboard Then they have read-only access to AutoClaim settings and current enrollments Given the dashboard is loaded When viewing Enrollments Then current AutoClaim enrollments, queued candidates, fill outcomes, and scheduled attempts are displayed with filters by class, series, and date range Given a queued candidate is selected When the user triggers Manually claim, Skip, or Pause Then the system confirms the action, updates the queue within 2 seconds, and records an override with actor, reason, and before/after status Given any AutoClaim setting change or override is performed When writing to audit Then the log includes actor, role, timestamp, entity path (Account/Series/Class), old value, new value, and reason (if provided) Given an Owner or Admin requests an export for a date range When the export is generated Then a CSV downloads within 10 seconds containing fields: class_id, class_start, user_id, action_type, outcome, price, price_cap, priority_rule, blackout_applied, sca_hold_status, actor_id, actor_role, timestamp
Audit Trail, Refunds & Dispute Handling
"As support staff, I want complete audit logs and streamlined refund/dispute tools so that I can resolve issues quickly and maintain compliance."
Description

Record an immutable audit log of AutoClaim state transitions and payment attempts, including timestamps, actors, and PSP responses, viewable in admin tools for support. Implement refund and reversal workflows for scenarios such as class cancellations, mistaken auto-claims, or SCA failures post-hold, honoring provider policies and local regulations. Provide rapid lookups of evidence (notifications sent, authentication links, user consents) to assist with chargebacks. Support partial refunds, pro-rated fees, and automated refund triggers where configured. Define data retention and privacy controls for logs and receipts, with export capabilities for compliance and dispute packages.

Acceptance Criteria
Immutable AutoClaim Audit Log Entries
Given any AutoClaim state transition or payment attempt occurs When the event is persisted Then an append-only audit entry is created with: event_type, previous_state, new_state, actor, UTC timestamp (ISO-8601 ms), claim_id, class_id, user_id, request_id, idempotency_key, psp_transaction_id, amount, currency, response_status, response_code, response_message, and a prev_entry_hash for chain integrity And attempts to update or delete an existing entry return HTTP 403 and create a new corrective entry instead And 95th percentile write latency <= 300 ms; success rate >= 99.99% over any rolling 5-minute window And all entries share a monotonically increasing sequence per claim_id and pass hash-chain verification
Admin Audit Log Search, Filter, and Export
Given a support admin is authenticated with role Support or higher When they search the audit log by claim_id, class_id, user_id, event_type, date range, or psp_transaction_id with pagination (page size up to 200) Then results return within 2 seconds P95 for datasets up to 50,000 records And filters combine with AND semantics and are accurately applied And CSV and JSONL exports include all returned fields with a signed checksum and are available within 60 seconds P95 for up to 100,000 records And PII masking follows role policy (e.g., PAN proxies last4 only; emails masked) And all accesses and exports are logged in an admin activity audit with actor, timestamp, and filter parameters
Automated Refunds on Class Cancellation
Given a provider cancels a class that has AutoClaim-confirmed bookings When the cancellation is saved Then refunds are created per provider policy (full/partial/pro-rated, excluding non-refundable fees where configured) and applicable local regulations And PSP refund requests are submitted idempotently and receive success responses within 5 minutes P95 And attendees receive notification of refund including amount, currency, and expected settlement time And audit entries capture policy version, refund amounts, calculation basis, and PSP response And failures retry up to 6 times with exponential backoff and produce alerts after final failure
Grace-Window Reversal for Mistaken Auto-Claim
Given an attendee's seat was auto-claimed and a grace period and price cap were configured When the attendee clicks Undo within the grace period and before the provider's cutoff Then the authorization is voided if not captured; otherwise a refund is issued per policy And the seat is returned to the waitlist or the next eligible attendee is auto-claimed per rules without double-booking And all related notifications are sent (attendee and provider) and logged with delivery status And the workflow completes within 2 minutes P95 from Undo action And audit entries include the user's explicit Undo action with IP, user agent, and timestamp
SCA Failure Post-Hold Handling
Given a hold requires SCA to capture When the attendee fails or times out authentication Then the seat is released and capacity updated within 5 seconds P95, the authorization is reversed/voided, and no capture occurs And the attendee is notified with a link to re-authenticate or rejoin the waitlist And audit entries include authentication prompts, outcomes, and notification delivery statuses And if any partial capture occurred, an immediate refund is issued and logged with PSP receipt
Dispute Evidence Package and Rapid Lookup
Given a chargeback or retrieval request is received from the PSP When a support agent opens the dispute record Then an evidence bundle is assembled containing: opt-in consent artifacts (screen snapshot, terms version, IP, timestamp), notification logs (content hashes, delivery receipts), SCA logs, booking details (price cap, grace window), and full audit timeline with PSP receipts And the evidence preview loads within 2 seconds P95 And a ZIP/PDF export is generated within 30 seconds P95, cryptographically signed, and delivered via a link that expires in 24 hours And all accesses and downloads are permission-checked and logged
Data Retention, Privacy, and Redaction Controls
Given retention policies are configured for audit logs, receipts, and notifications When data reaches end-of-retention Then PII fields are irreversibly anonymized or records deleted per policy while preserving minimal financial metadata required for compliance And redacted fields are omitted from exports and UIs for all roles And Right-to-Erasure requests anonymize user PII within 30 days while retaining dispute-essential metadata where legally permissible And policy changes require dual approval, are versioned, and are logged And backups containing deleted PII are purged within 30 days And data residency configurations ensure storage remains within selected region(s)

FairQueue

A configurable, transparent ranking engine that blends join time with smart priorities like membership status, attendance streak, proximity, prerequisites, and “keep-together” group rules. Shows users their position and ETA to build trust, and uses round‑robin fairness to prevent the same person from always being first. Reduces support questions and aligns fills with your business goals.

Requirements

Priority Rules Configuration Engine
"As a studio owner, I want to configure how the waitlist ranks attendees using business rules so that class spots are filled according to my priorities."
Description

A configurable ranking engine that blends join time with weighted business factors such as membership status, attendance streak, proximity, prerequisite completion, and group rules. Admins can define per-class or template-based rule sets, adjust weights and tie-breakers, simulate outcomes, and preview rankings before publishing. Configurations are versioned with change history and safe rollbacks. The engine integrates with ClassTap scheduling, user profiles, passes, and attendance records, and exposes a deterministic API used by booking flows and waitlists. This enables transparent, goal-aligned fills while eliminating manual sorting and reducing admin time.

Acceptance Criteria
Per-class rule set creation, preview, and publish
- Given I am an admin with Manage Rules permission, When I create a rule set for a specific class and select factors (join time, membership status, attendance streak, proximity, prerequisites, keep-together) with integer weights 0–100 and ordered tie-breakers, Then Save is enabled only if at least one factor weight > 0 and all fields validate. - Given invalid inputs (e.g., non-integer weight, weight < 0 or > 100, duplicate tie-breakers), When I attempt to save, Then the save is blocked and inline error messages identify each invalid field. - Given a valid configuration, When I click Preview and choose a roster/waitlist dataset, Then I see a deterministic ranked list with position, ETA, and per-factor score breakdown per user. - Given I publish the configuration, Then a new version with ID, timestamp, author is created, becomes active for the class within 10 seconds, and all booking/waitlist evaluations use this version. - Given identical inputs and the same active version, When rankings are re-evaluated, Then the order is identical across repeated runs.
Template-based rule sets with per-class overrides
- Given I create a rule set template, When I define default weights, tie-breakers, and metadata (name, description), Then I can mark it as the default for new classes and save without errors. - Given classes are linked to the template, When I publish the template, Then linked classes inherit its settings immediately without service interruption. - Given a linked class requires deviations, When I set a class-level override for any factor or tie-breaker, Then the class shows an Overridden status and a diff from the template is viewable. - Given the template is updated/published again, Then non-overridden fields propagate to linked classes and overridden fields remain unchanged. - Given a class is unlinked from the template, Then it retains the last class-level configuration and evaluations continue without disruption.
Simulation and impact analysis before publishing
- Given a draft configuration, When I run a simulation using an existing class roster/waitlist or a CSV upload of candidates, Then the system returns a deterministic ranked order, predicted fill sequence across N available seats, and per-user factor explanations. - Given the same inputs and configuration, When I rerun the simulation, Then results are identical (same order and scores) using the same seed. - Given I adjust a factor weight or tie-breaker, When I rerun the simulation, Then the UI highlights position changes (delta up/down) and allows export of results to CSV. - Given a constraint conflict (e.g., keep-together group size exceeds remaining capacity), When simulation runs, Then the conflict is flagged with suggested resolutions based on configured policy (skip vs allow partial).
Deterministic ranking API for booking and waitlists
- Given a POST to /ranking with classId, candidateUserIds, and configVersion, When up to 500 candidates are evaluated, Then the API responds within 500 ms (p95) with ordered candidates, positions, ETAs, and per-factor scores. - Given identical inputs and the same configVersion, When multiple requests are made, Then the response order and scores are identical each time. - Given invalid candidates (e.g., missing prerequisites, unknown userId), When the request is processed, Then those candidates are excluded and the response contains a validationErrors collection detailing reasons. - Given trace=true is provided, When the request is processed, Then the response includes a correlationId and a rule evaluation traceId suitable for audit log lookup.
Round-robin fairness for equal composite scores
- Given two or more candidates have equal composite scores after weighting, When round-robin is enabled, Then first position rotates among them across successive fills within the configured rotation window (e.g., per day) so no candidate leads twice before others have led once. - Given rotation state is persisted, When the service restarts, Then the rotation order continues from the last recorded state. - Given round-robin is disabled, When equal scores occur, Then the configured tie-breakers determine the final order deterministically.
Keep-together group handling under capacity constraints
- Given a booking/waitlist evaluation includes a group marked keep-together of size G, When remaining capacity ≥ G, Then the group is placed as a unit without splitting and in accordance with overall ranking. - Given remaining capacity < G, When policy is set to Skip, Then the group is skipped as a whole with a logged explanation; When policy is set to Allow Partial, Then the system admits up to remaining capacity and logs the partial decision, with preview indicating the impact. - Given waitlist advancement, When the group is next in line, Then all members advance together and ETA reflects the group size and policy. - Given simulation or API response, Then each affected decision includes rationale indicating the keep-together rule outcome.
Versioning, change history, and safe rollback
- Given a configuration is saved or published, When the action completes, Then a new immutable version is recorded with versionId, author, timestamp, change summary, and a diff from the prior version. - Given an admin selects a prior version, When they confirm rollback, Then the selected version becomes active within 10 seconds without disrupting in-flight booking transactions and a rollback entry is added to history. - Given version history view, When accessed, Then it lists all versions with metadata and supports export of the active rule set and audit log. - Given rollback occurs, When the API is called thereafter with the classId, Then responses reflect the restored version deterministically.
Real-time Queue Position and ETA Display
"As a waitlisted attendee, I want to see my position and estimated time to get a spot so that I know what to expect and can plan."
Description

User-facing components for web booking pages, embeddable widgets, and SMS/email deep links that show current position in queue, estimated time to promotion, and a concise explanation of factors influencing rank. Updates in real time as capacity or rules change, handles race conditions, and degrades gracefully on poor networks. Fully white‑label with branding, localization, and accessibility support. Builds trust, reduces support tickets, and encourages users to stay on the waitlist rather than overbooking alternatives.

Acceptance Criteria
Real-time Position and ETA on Web Booking Page
Given a user is on a provider’s web booking page viewing their active waitlist entry When the queue state changes due to promotion, cancellation, or capacity update Then the displayed position and ETA update within 2 seconds without a full page reload And the position is shown as an integer >= 1 and <= current queue length And the ETA is shown as a localized absolute time and relative duration (e.g., “3:45 PM — in ~12 min”) And the ETA confidence indicator displays one of: low, medium, high based on variance thresholds And the time zone used matches the configured class time zone or the user’s preference And the values match the authoritative queue snapshot within one update cycle
Embeddable Widget Displays Branded, Localized Queue Info
Given a host site embeds the FairQueue widget with theme configuration and a locale When the widget renders at viewport widths from 280px to 1200px Then all queue elements are visible without horizontal scroll and wrap responsively And primary color, typography, and logo from the theme are applied to UI elements And no ClassTap branding appears unless the poweredBy flag is enabled And all user-visible strings (including time formats and pluralization) reflect the provided locale And interaction latency (tap to response) is <= 150ms at 60fps on a mid-tier mobile device
SMS/Email Deep Link Opens Live Queue View
Given a user opens a signed deep link from SMS/email for a specific waitlist entry When the link is valid and not expired Then the page displays the user’s current position and ETA without requiring login And the view auto-updates in real time with the same <= 2s latency target as the web booking page And if the link is expired or invalid, a localized error with a Resend Link CTA is shown And no personally identifiable information other than the user’s first name (if configured) is displayed
Concise Explanation of Ranking Factors
Given a waitlisted user views their queue card When ranking factors are available for that user Then a “Why this position” explanation lists up to 3 applicable factors in priority order And the explanation is <= 200 characters in English and fits within 2 lines on a 320px-wide viewport (truncating with ellipsis if needed) And it always includes Join time and any applicable priorities (e.g., membership status, attendance streak, proximity, prerequisites, group keep-together) And no comparative or identifying information about other users is shown And the content is localized to the user’s language
Race Condition Safety and Atomic Promotion
Given concurrent promotions or capacity changes occur while the user is viewing their position When the queue processes these updates Then the UI never shows negative positions, skipped numbering, or duplicate promotions And each entry transitions atomically through states: waiting -> offered -> booked/expired -> removed And a promotion hold window (configurable, e.g., 10 minutes) is honored and reflected in the ETA/countdown And the UI resolves detected conflicts by showing a brief “Refreshing…” state (<= 1s) and reconciling to the authoritative state within 2 seconds And the 99th-percentile consistency gap between backend state and UI is <= 2 seconds
Graceful Degradation on Poor Networks
Given network latency > 1000ms, packet loss > 10%, or offline is detected When the live connection drops or stalls Then the UI falls back from WebSocket/SSE to HTTP polling every 15 seconds with exponential backoff up to 2 minutes And a non-blocking banner “Connection limited — data may be delayed” is displayed And a last-updated timestamp is shown and refreshed after each successful fetch And a manual refresh control is available and updates the data when tapped/clicked And core content remains readable; cumulative layout shift during reconnect attempts stays <= 0.1
Accessibility and Localization Compliance
Given a user relies on assistive technologies or right-to-left locales When the position or ETA changes on the page Then screen readers announce the change via an ARIA live region within 1 second without moving focus And all interactive elements are keyboard-accessible with visible focus indicators and logical tab order And color contrast meets WCAG 2.2 AA (text >= 4.5:1; UI components >= 3:1) And time, date, and number formats are localized, and RTL layout is supported with correct directionality And language attributes are set on localized content for accurate pronunciation
Round‑Robin Fairness Scheduler
"As a community program coordinator, I want the queue to rotate priority among frequent joiners so that the same people don’t always get first access."
Description

A fairness mechanism that rotates priority among similarly ranked users across recurring classes and repeated promotions so the same person is not always first. Supports configurable fairness windows (per class, series, instructor, or organization), respects hard eligibility rules, and records fairness adjustments in the audit log. Includes simulation tools to forecast distribution effects and guardrails to avoid disadvantaging newcomers. Ensures equitable access while preserving business priorities.

Acceptance Criteria
Rotate First Position Among Tied Users Over Consecutive Occurrences
Given a set of M users with equal effective priority within a fairness window for a recurring class or repeated promotion When the scheduler runs across M eligible occurrences Then each user is assigned the top available slot exactly once before any user receives it twice And the rotation state persists across runs and is updated atomically with each scheduling decision And if a user is ineligible or absent for an occurrence, they are skipped without consuming their turn and remain next in sequence on their next eligible occurrence And the algorithm can rotate the top-K slots when rotationMode=topK with K configurable; default K applies if unspecified And the produced ordering remains stable for the occurrence after publication (no reordering without a state/version change)
Scope-Controlled Fairness Windows (Class/Series/Instructor/Org)
Given fairnessWindowScope ∈ {class, series, instructor, organization} When scheduling is computed Then rotation state is isolated per selected scopeId and does not bleed across other scopes And changing the scope on a rule clears prior rotation state and initializes a new sequence from current join-time order And time-bounded windows reset precisely at the configured boundary (by occurrence count or date), with no carryover And the default scope is series and can be overridden per schedule rule And scope configuration changes are versioned and auditable
Eligibility-First Scheduling
Given hard eligibility rules (e.g., prerequisites, membership status, payment validity, capacity constraints) applied before fairness When determining the final ordering Then only users passing hard eligibility are considered for round-robin rotation And fairness never promotes an ineligible user ahead of any eligible user And when a user becomes eligible after being skipped, they re-enter at the next position in the rotation without losing their unserved turn And fairness adjustments never cross a configured priority gap epsilon that protects significant business priority differences
Audit Trail for Fairness Adjustments
Given any rank change attributable to round-robin fairness When the schedule is finalized for an occurrence Then an immutable audit event is recorded containing: eventId, timestamp, scope, scopeId, occurrenceId, userId, preRank, postRank, slotType (top1/topK), algorithmVersion, reason="round_robin", correlationId And the write success rate is ≥ 99.9% with up to 3 retries and exponential backoff; on final failure, a critical alert is raised and the change is queued for retry without blocking scheduling And audit events are searchable by scopeId, occurrenceId, and userId, and exportable as CSV And access to audit events is restricted to authorized roles
Distribution Simulation and Forecast Metrics
Given an admin selects a historical time range, target scope, and fairness configuration, with guardrails toggled on/off When the simulation is executed Then the system outputs metrics including per-user share of top-K slots, Gini coefficient of slot distribution, average time-to-first-slot, newcomer top-K share, and count of priority-gap guardrail conflicts And results include a baseline (no fairness) comparison and delta values, and are downloadable And simulations complete within 60 seconds for up to 10,000 users and 1,000 occurrences or provide progress with partial results if longer And any metric breaching configured thresholds is flagged in the results
Newcomer Guardrails Without Breaking Priorities
Given a newcomer is defined by tenure < T days or completedOccurrences < C (configurable) When a fairness demotion would apply to a newcomer within the active window Then demotion is capped at maxDemotionSteps and cannot reduce their chance below newcomerMinTopKShare across the window if capacity allows And within P eligible occurrences per newcomer, each receives at least one top-K slot if they remain eligible And if enforcing a guardrail would cross the priority gap epsilon against a significantly higher-priority user, the guardrail is deferred and a guardrail_conflict audit event is recorded And simulation confirms newcomer top-K share ≥ the configured minimum under historical demand scenarios
Keep‑Together Group Handling
"As a parent booking for two children, I want our spots kept together so that we can attend the same class."
Description

Group-aware queuing that treats multi-seat bookings as a unit, calculating a composite score and only promoting when contiguous spots are available. Configurable policies allow admins to enable intelligent splitting as a fallback with explicit user consent, suggest alternate sessions with sufficient capacity, and prioritize smaller groups when space is tight. Integrates with capacity management, prevents accidental splits, and communicates decisions clearly to users.

Acceptance Criteria
Group Promotion Requires Contiguous Seats
- Given a waitlisted group booking of size G for session S and available spots A in S, When A >= G, Then the system promotes the entire group in a single atomic transaction and reserves G seats simultaneously. - Given A < G and intelligent splitting is disabled or the group has not provided explicit split consent, When capacity changes, Then the group is not promoted and no partial seats are reserved. - Given any promotion decision, When shown to the group owner, Then the UI displays current position, estimated wait time, and the reason "Waiting for G seats together" within 1 second of the decision.
Group Composite Score Ranking
- Given admin-configured priority weights and aggregator = average, When computing group rank, Then the group's composite score equals the arithmetic mean of member composite scores using the configured weights and is rounded to 4 decimals. - Given two groups with equal composite scores, When ranking, Then the earlier group join timestamp orders first; if still tied, round-robin fairness rotates the ordering for the next promotion cycle. - Given an admin updates either weights or aggregator, When the queue is re-evaluated, Then group scores update within 5 seconds and the new ordering is reflected without losing any existing reservations.
Intelligent Split with Explicit Consent
- Given intelligent splitting policy is enabled and the group has explicitly consented to splitting at join time or via a confirmation prompt, When fewer than G contiguous seats are available but a valid split into at most MaxSplits subgroups each >= MinSubgroupSize can be accommodated, Then the system offers the split, clearly stating subgroup sizes and seat holds, and requires a positive confirmation before committing. - Given no consent is present or the user declines within HoldWindowMinutes, When the window expires, Then no split occurs and all held seats are released to the queue. - Given a split is accepted, When promoting, Then each subgroup is confirmed atomically and seated together; audit logs record consenting user, timestamp, policy settings, and resulting subgroup sizes.
Alternate Session Suggestions for Full Group
- Given the current session cannot seat G together within ForecastWindow and splitting is disabled or not consented, When evaluating alternatives, Then the system suggests at least one upcoming session with capacity >= G that satisfies prerequisites and proximity filters, sorted by earliest start time. - Given suggestions are presented, When the user selects an alternative, Then the entire group is moved in one action to that session and their previous waitlist position is released; confirmation is sent via email/SMS immediately. - Given no suitable alternatives exist, When the evaluation completes, Then the UI shows "No sessions can seat your group together" and provides a link to search other dates or locations.
Tight-Space Small-Group Prioritization
- Given TightSpaceMode is enabled with threshold T, When A <= T and the head-of-queue group size > A but a trailing group size <= A exists, Then the system may promote the smaller trailing group using round-robin fairness and logs the policy reason. - Given a smaller group is promoted under TightSpaceMode, When recalculating fairness, Then the skipped larger group receives a priority bump in the next cycle to prevent starvation, and the rationale is visible to admins. - Given TightSpaceMode is disabled, When A <= T, Then no trailing group may overtake the head group due to size considerations.
Capacity Change Re-evaluation and Atomicity
- Given a capacity change event (cancellation, room size update, admin override), When it occurs, Then the queue is re-evaluated and eligible groups are promoted within 2 seconds. - Given any group promotion, When reserving seats, Then the operation is atomic and idempotent; no partial confirmations occur for a group and concurrent promotions do not split a group. - Given a promotion fails mid-transaction, When rollback completes, Then all seats are returned to the pool and the group's position and ETA remain unchanged; the user is notified with a concise non-technical message.
Eligibility and Prerequisite Validation
"As an instructor, I want FairQueue to verify prerequisites and membership before ranking so that only eligible students get priority."
Description

Pre‑ranking checks that validate membership status, attendance streak thresholds, completion of prerequisites, age or skill constraints, and outstanding balance or waiver status. Supports hard blocks and soft preferences, with clear user messaging and admin override workflows. Pulls data from ClassTap profiles, passes, attendance, and external systems via API. Ensures only eligible users are ranked or prioritized, improving class outcomes and reducing last‑minute failures.

Acceptance Criteria
Hard Block: Financial, Waiver, and Age Compliance
Given the organization setting "Block on outstanding balance" is enabled and the user's balance in ClassTap > 0 When the user attempts to join a waitlist or book a class that uses FairQueue Then the user is not added to the ranking pool and a blocking message provides a link to resolve payment And an eligibility event "BLOCK_BALANCE" is recorded with userId, classOccurrenceId, balanceAmount, and timestamp Given the class requires a signed waiver valid through the class date and the user's waiver is missing or expired When eligibility is validated pre-ranking Then the user is not added to the ranking pool and a blocking message links to sign the waiver And an eligibility event "BLOCK_WAIVER" is recorded Given the class has a minimum age requirement and the user's age on the class start date is below the minimum When eligibility is validated pre-ranking Then the user is not added to the ranking pool and a message states the minimum age and how age is calculated And an eligibility event "BLOCK_AGE" is recorded
Attendance Streak Threshold Enforcement
Given the class defines attendanceStreakThreshold = 3 within the last 30 days and mode = Hard And the user's attendance streak from ClassTap attendance records < 3 within the window When eligibility is validated pre-ranking Then the user is blocked from ranking with message explaining the streak requirement And an eligibility event "BLOCK_STREAK" is recorded with computed streak count Given the class defines attendanceStreakThreshold = 3 within the last 30 days and mode = Soft And the user's attendance streak < 3 When eligibility is validated pre-ranking Then the user remains eligible but receives a priority penalty equal to the configured streakPenalty And an eligibility event "PREF_STREAK_SOFT" is recorded with applied penalty
Soft Prerequisite: Missing Course Applies Priority Penalty
Given the class defines prerequisite "Intro to Aerial 101" with mode = Soft and priorityPenalty = -20 And the user's completion records across ClassTap and connected LMS do not show completion of that prerequisite When eligibility is validated pre-ranking Then the user is added to the ranking pool with a priority adjustment of -20 And the user-facing message indicates eligibility with lower priority until the prerequisite is completed And an eligibility event "PREF_PREREQ_SOFT" is recorded including prerequisiteId and applied penalty
Admin Override: Time-Bound Waiver With Audit Trail
Given a user fails a hard block rule and an admin with role Owner or Manager initiates an override with a required reason When the admin confirms an override scoped to a specific class occurrence with an expiry Then the system marks the user eligible for that occurrence and bypasses only the selected rule for ranking And an audit record is stored with adminId, userId, classOccurrenceId, ruleId, reason, createdAt, expiresAt And the admin UI shows an "Overridden" badge; the end-user sees standard success messaging without exposing the override And the override expires automatically at expiresAt or after the occurrence, whichever comes first
External Membership API: Validation and Graceful Degradation
Given the class requires active membership and the source of truth is an external membership API When the API responds within 2 seconds with status = active for the user Then the user passes membership eligibility and is added to the ranking pool When the API responds with status = inactive or expired Then the user is blocked with a message to renew membership And an eligibility event "BLOCK_MEMBERSHIP" is recorded with returned status When the API request times out or returns 5xx Then the system places the user on a temporary hold (not ranked), retries up to 3 times over 10 minutes, and informs the user to try again And admins see an alert with the failure details and may apply an override
Revalidation on Waitlist Promotion and Check-In
Given a user is on the waitlist for a class occurrence and was previously eligible When a seat becomes available and the user is next in line to be promoted Then all eligibility and prerequisite checks are re-run against the latest data sources before promotion And if any hard block fails, the promotion is canceled, the next eligible user is considered, and the user is notified with reasons And if only soft preferences are affected, the promotion proceeds with existing ordering And an eligibility revalidation event is recorded with before/after rule states When the user checks in at class start Then eligibility is re-verified; if a hard block is detected, check-in is rejected and staff are notified
User Messaging: Clear Reasons, Actions, and Localization
Given one or more eligibility checks fail or soft preferences are applied When presenting the result to the end-user in the booking/waitlist flow Then the message lists each failed rule by readable name, a brief reason, and actionable next steps with deep links (e.g., pay balance, sign waiver, enroll in prerequisite) And the message is localized to the user's language preference and contains only the user's own data (no third-party PII) And the exact message variant and display timestamp are recorded for analytics And admin views show technical rule codes and logs, while end-user views remain non-technical
Auto‑Promotion with Payment and Hold Window
"As a waitlisted student, I want to be automatically offered a spot with an easy way to confirm and pay so that I don’t miss openings."
Description

Automated promotion workflow that offers open spots to the top‑ranked users, holds the seat for a configurable countdown window, and captures payment or deducts a pass upon confirmation. Sends branded SMS/email notifications with secure deep links, escalates to the next candidate on expiry or decline, and prevents double‑bookings via atomic reservations. Integrates with ClassTap payments, refunds, and reminders, and emits webhooks for downstream systems. Increases conversion from waitlist to attendance while reducing manual intervention.

Acceptance Criteria
Seat Offer and Hold Creation on Spot Availability
Given a class seat becomes available and a ranked waitlist exists When the system identifies the top-ranked eligible candidate Then an offer and hold are created for that candidate within 5 seconds And the hold duration equals the configured hold window (10 minutes in this test) And the seat is reserved atomically for the hold duration and cannot be booked by others And the candidate’s booking page displays the hold expiry timestamp and remaining time And the offer is persisted with a unique ID, timestamps, and candidate ID
Branded Notification with Secure Deep Link
Given an offer is created for a candidate with valid email and phone When notifications are dispatched Then branded email and SMS are sent containing a secure, single-use deep link And the deep link token expires when the hold expires and cannot be reused And opening the link shows a prefilled confirmation page with class details and price on mobile and desktop And notification delivery status and any failures are recorded with timestamps And if SMS delivery fails, an email retry is attempted within 60 seconds
Payment Capture or Pass Deduction on Confirmation
Given the candidate opens the offer link during an active hold When the candidate confirms using a saved card, new card, or valid class pass Then payment is authorized and captured via ClassTap Payments or a pass is decremented And the booking status changes to Confirmed and the hold is released immediately And a receipt and booking confirmation are sent to the candidate And class reminders are scheduled and the candidate is removed from the waitlist And if payment authorization fails, the candidate can retry without shortening the remaining hold time
Auto-Escalation on Decline or Hold Expiry
Given an active offer with a hold window When the candidate declines the offer or the hold expires with no action Then the seat is released and the next eligible ranked candidate receives an offer within 5 seconds And the declined or expired candidate is not charged and no pass is decremented And both the decline/expiry and the subsequent offer are logged with timestamps and actor/source
Atomic Reservations and Double-Booking Prevention
Given two candidates attempt to confirm the same held seat concurrently When two acceptance submissions are received within the same second Then exactly one confirmation succeeds and the other receives an Offer Unavailable message And no charges or pass deductions occur for the unsuccessful confirmation And the system prevents the same user from being confirmed into overlapping classes or duplicate seats
Webhooks for Offer and Booking Lifecycle
Given webhooks are configured for the tenant When offer_created, offer_accepted, offer_declined, offer_expired, payment_succeeded, payment_failed, booking_confirmed, or refund_issued events occur Then a signed webhook is delivered within 10 seconds per event with tenant_id, class_id, user_id, offer_id, event_type, and timestamps And failed webhook deliveries are retried with exponential backoff for at least 24 hours and a maximum of 10 attempts And webhook delivery outcomes are visible in tenant logs
Refund on Post-Payment Failure
Given payment has been captured but booking confirmation subsequently fails due to system error or capacity race When the failure is detected Then a full refund is automatically issued within 10 minutes and linked to the original payment And the candidate is notified of the refund and offered to rejoin the waitlist And a refund_issued webhook is emitted and the audit log records the failure and refund
Decision Audit Log and Explainability
"As a studio admin, I want to see why each attendee was ranked and promoted so that I can answer support questions and ensure fairness."
Description

A comprehensive audit trail capturing ranking inputs, weights, tie‑breakers, fairness rotations, eligibility results, promotion offers, expirations, user actions, and notifications. Provides admin tooling to view per-user explanations ("why this position") and an optional attendee-facing summary for transparency. Supports filtering, export, and retention policies with privacy controls. Reduces support load, improves trust, and enables compliance reviews and A/B evaluation of rule changes.

Acceptance Criteria
Admin Per-User Ranking Explanation
Given an admin with permission Rank:View and tenant scope T, When they open the Explain view for user U in class C, Then within 2s the system displays input factors (join_time, membership_status, attendance_streak, proximity, prerequisites, keep_together_group), factor weights with weights_version, normalized values, computed score, current position, tie-breaker chain, fairness rotation bucket and round index, eligibility result with reason codes, and timestamps. Given factor weights are updated, When a new weights_version becomes effective, Then the explanation shows weights_version and effective_at and links to the triggering audit event. Given user U is in a keep-together group, When the explanation is viewed, Then it shows group_id, group_size, grouping outcome, and any position adjustment applied. Given multi-tenant isolation, When an admin from tenant T2 attempts to open U from tenant T, Then a 403 is returned and no data is leaked. Given the Explain view is open, When a re-rank occurs that changes U’s position, Then a non-intrusive refresh banner appears within 5s and reloading preserves a permalink to the previous snapshot.
Attendee-Facing Position Summary
Given tenant has enabled Transparency:Basic, When an attendee views their position for class C, Then show current queue position, ETA with 95% CI, last_updated timestamp, and a plain-language rationale summary without exposing other users’ data. Given privacy constraints, When the rationale is rendered, Then no PII, exact weights, or full competitor counts are shown; competitor counts are bucketed (e.g., 0, 1–5, 6–20, 21+). Given locale L and accessibility settings, When the summary is displayed, Then content is localized (i18n), numbers formatted to locale, focus order is logical, ARIA labels are present, and color contrast meets WCAG AA. Given repeated refreshes, When the attendee requests updates, Then responses are rate-limited to 10 explainer fetches/min/user and data are cached with max-age 15s. Given upstream data are unavailable, When the attendee opens the page, Then a friendly fallback is shown with retry control and no internal error details are exposed.
Audit Log Capture of Ranking Decision Events
Given any ranking-related operation occurs (initial_rank, re_rank, join, leave, confirm, promote, expire), Then an immutable audit event is written containing: tenant_id, class_id, user_id(s), event_type, event_id (ULID), correlation_id, sequence, occurred_at (UTC), inputs snapshot (join_time, membership_status, attendance_streak, proximity, prerequisites, keep_together_group), weights_version, computed_scores, tie_breakers_applied, fairness_rotation_state, eligibility_decision with reason codes, notifications (channel, template_id, status), promotion offers with expiry, actor (system/admin/user), and request_id. Given retries or duplicate deliveries, When an event with the same correlation_id and sequence is received, Then the write is idempotent and no duplicate logical event is created. Given PII fields, When persisted, Then they are encrypted at rest with field-level encryption and are excluded from non-privileged exports. Given integrity requirements, When events are stored, Then each includes a content hash and a per-class hash chain linking to the previous event to detect tampering. Given throughput up to 500 events/sec/tenant, Then p95 write latency is <100ms and error rate <0.1% over a 10-minute rolling window.
Filter, Search, and Export Audit Logs
Given an admin with Audit:Read, When they open the Audit UI, Then they can filter by tenant, class, date range, user_id/email_hash, event_type, correlation_id, variant_id, and reason_code, and the first 100 results return with p95 <1.5s for datasets up to 50k events. Given a keyword query, When search is executed, Then full-text search across reason messages and metadata supports exact and prefix matching and highlights matches. Given an export is requested, When filters are applied and Export is clicked, Then a background job produces CSV and NDJSON in 10k-event chunks, includes schema_version and filter metadata in headers, excludes sensitive fields by default, and provides a signed URL expiring in 24h. Given role-based access, When the admin lacks Audit:ExportSensitive, Then sensitive fields cannot be included in export; with the role, inclusion is allowed and audited. Given export status polling, When the job runs, Then UI shows Pending, Running (with percent complete), Completed (with file sizes), or Failed (with error code), and exports are rate-limited to 1 per tenant per minute.
Retention and Privacy Controls
Given a tenant retention policy (30–730 days, default 180) is configured, When nightly retention runs, Then events older than policy are purged and a deletion-audit record is written with counts and ranges. Given a user erasure request is received, When processed, Then PII for that user is anonymized within 30 days while preserving non-identifying aggregates; a compliance audit record is created and linked to the request id. Given a legal hold is placed on class C or tenant T, When retention runs, Then matching events are excluded from purge until the hold is lifted and the exemption is logged. Given backups exist, When purge completes, Then purged data are removed from backup rotation within 35 days according to the documented schedule. Given attendee-facing summaries, When fields are redacted, Then the UI omits them gracefully without showing placeholder tokens that reveal redaction status.
A/B Rule Change Evaluation with Audit Comparison
Given an A/B experiment on ranking weights is active, When ranking events are logged, Then each event includes experiment_id, variant_id, and weights_version for traceability. Given an admin selects Compare Variants for class C or user U, When the comparison view loads, Then it shows side-by-side factor contributions, computed scores, positions, tie-breakers, and simulated promotion timelines for variants A and B, and allows CSV export of the comparison. Given rollout guardrails, When attempting to deploy a weights_version to 100%, Then the system blocks deployment unless audit coverage for re-rank events in the last 24h is ≥99% with zero integrity-chain breaks and displays which criteria are unmet. Given an experiment ends, When the summary is generated, Then metrics (mean wait time, promotion rate, fairness rotation equity) are computed from audit data for the specified window and can be exported with query parameters documented in the file header. Given privacy boundaries, When comparisons are generated, Then only data from the requesting tenant are included and no PII is exposed.

HoldGuard

Adaptive hold timers that flex with context: keep the standard 10‑minute window, shorten near class start, and auto‑extend when a user opens the pay sheet or passes verification. Visual countdowns set clear expectations, while expired holds auto‑cascade to the next in line. Prevents seat slippage without manual intervention.

Requirements

Context-Aware Hold Rules Engine
"As a student booking a popular class, I want the system to reserve my spot with a fair, context‑aware timer so that I don’t lose my seat due to timing edge cases or device switches."
Description

Implement adaptive seat-hold timers that default to 10 minutes, automatically shorten as the class start time approaches, and support configurable thresholds and overrides at the organization, location, and class levels. The timer is server-authoritative, persists across sessions/devices, and exposes a real-time state via API for clients to render synchronized countdowns. Holds are atomic and idempotent to prevent oversells under concurrency, support multi-quantity reservations, and are timezone-aware. Events are emitted on hold start, refresh, extension, and expiry, with graceful recovery after service restarts and guardrails for minimum/maximum durations.

Acceptance Criteria
Adaptive Duration: Default, Near-Start Shortening, and Config Overrides
Given org config default_duration=600s, near_start_window=30m, near_start_duration=180s, min_duration=60s, max_duration=1200s When a hold is created 45m before class start Then the hold expires_at = created_at + 600s (±1s) and API remaining_ms decreases monotonically When a hold is created 20m before class start Then the hold duration is 180s (clamped between min/max) and countdown ends ≤ created_at + 181s Given class-level override near_start_duration=120s When a hold is created 10m before class start Then the hold duration is 120s Given location-level override default_duration=480s When a hold is created 2h before class start at that location Then the hold duration is 480s And precedence order is deterministic: class > location > org
Server-Authoritative Timer: Persistence and Real-Time State
Given a hold exists with 300s remaining When the user refreshes the client or signs in on a second device within 3s Then both clients show remaining time within 1s of each other and of API remaining_ms, with server as source of truth And no client can extend or modify expires_at without a successful server command response When GET /holds/{id} is called Then response includes id, seat_ids, quantity, created_at (UTC), expires_at (UTC), remaining_ms, source_timezone, status within p95 ≤ 200ms And remaining_ms never increases unless an extension event has been accepted Given transient network loss < 30s When SSE/WebSocket reconnects Then stream resumes and state is caught up without time gain or duplicate extensions
Atomic, Idempotent Holds Under Concurrency
Given 10 seats remain and two clients request holds for 10 seats concurrently with idempotency-keys K1 and K2 When requests arrive within 50ms Then exactly one hold is created; the other receives 409 HOLD_CONFLICT; inventory never goes negative and no oversell occurs Given a client retries with the same idempotency-key K1 within 60s When the request is replayed Then the same hold_id and state are returned (HTTP 200) and no additional seats are reserved Given a multi-quantity request exceeds available seats When the request is processed Then 422 INSUFFICIENT_INVENTORY is returned and no partial hold is created Given overlapping seat requests occur in parallel When holds are created Then no seat_id appears in more than one active hold at any time
Context-Based Extensions: Pay Sheet Open and Verification Passed
Given extension_on_pay_sheet_open=120s, extension_on_verification_pass=180s, throttle_window=60s, max_duration=1200s When the user opens the pay sheet with 45s remaining Then expires_at is extended by +120s within 200ms, not exceeding max_duration, and event hold.extend is emitted with delta_seconds=120 When the user passes verification with 30s remaining and last extension was > 60s ago Then expires_at is extended by +180s within 200ms, clamped by max_duration, and event hold.extend is emitted with delta_seconds=180 Given repeated pay sheet open events within the throttle_window When additional open signals arrive Then no further extensions are applied and no duplicate events are emitted
Event Emission and Recovery After Restart
When a hold is started, refreshed, extended, or expired Then events hold.start, hold.refresh, hold.extend, hold.expire are published at-least-once with strictly increasing sequence numbers per hold and payload includes hold_id, occurred_at (UTC), prev_expires_at, new_expires_at, reason Given a broker outage occurs When connectivity is restored Then buffered events are delivered within 5s with per-hold ordering preserved and no loss of state Given the service restarts while holds exist When the service becomes healthy Then holds are reloaded from durable storage and expiry jobs are rescheduled within 5s; no hold exceeds max_duration; no hold expires earlier than its scheduled time by more than 1s
Timezone Awareness and DST Safety
Given a class in America/Los_Angeles on a DST change day with start_at_local set When a hold is created 20m prior Then expires_at is computed from UTC using the class timezone rules and equals created_at + configured_duration regardless of DST shift When API returns timestamps Then created_at and expires_at are ISO-8601 UTC and response includes source_timezone=America/Los_Angeles; clients can render local countdowns that match server remaining_ms within 1s Given users in different timezones create identical holds for the same class When comparing expires_at values Then the UTC expires_at values are identical
Expiry Auto-Cascade to Waitlist
Given a waitlist exists with next_party_size=2 and an active hold for 2 seats expires When the hold reaches expiry Then within 2s the 2 seats are reheld for the next waitlisted party according to near-start rules; events waitlist.offer and hold.start are emitted linking previous_hold_id Given available seats < next_party_size at expiry When evaluating the waitlist Then no offer is created and seats return to general inventory; no seats remain unheld longer than 2s due to cascading logic Given multiple expiries occur in quick succession When cascading Then ordering is FIFO by waitlist position and no user receives multiple simultaneous offers
Pay Sheet Open Extension Trigger
"As a payer at checkout, I want my hold extended when I open the payment form so that I have enough time to complete payment without losing my seat."
Description

Automatically extend an active hold when the user opens the payment sheet to reduce drop‑offs during checkout. The client emits a pay_sheet_opened signal that is verified server-side and applied once per hold (configurable), with caps on total extensions and maximum TTL. Aborted or closed payment sheets revoke the pending extension after a short grace period. All extensions are audited, rate‑limited, and resilient to duplicate events, ensuring consistent behavior across web and mobile flows.

Acceptance Criteria
Standard Extension on Pay Sheet Open (Verified, Single Apply)
Given an active hold H for user U with remaining_time > 0 and applied_extensions_count < max_total_extensions_per_hold and current_ttl_seconds < max_hold_ttl_seconds When the client emits pay_sheet_opened with idempotency_key K and a signed hold_token referencing H Then the server verifies the token, ownership, hold status, and that K is unused And immediately applies an extension of extension_increment_seconds to H (bounded so new_ttl <= max_hold_ttl_seconds) And marks the extension as pending for grace_seconds And responds with HTTP 200 including new_expiration_time and pending=true And persists an audit record with status=pending, delta=+extension_increment_seconds, idempotency_key=K, and source client
Idempotent Handling of Duplicate pay_sheet_opened Events
Given a pay_sheet_opened event for hold H with idempotency_key K has already been processed When the same event (same K) is received again Then no additional extension is applied And the response is HTTP 200 with idempotency_replayed=true and unchanged expiration And only one audit record exists for K with no duplicates created
Revocation on Payment Sheet Close/Abort Within Grace
Given an extension applied from pay_sheet_opened for hold H is in pending state with grace_seconds=G and recorded pre_extension_expiration=T0 When pay_sheet_closed or payment_aborted for H is received within G seconds of the open Then the pending extension is revoked and H.expiration_time is set back to T0 if T0 > now else to now And the original audit record is updated to status=revoked with reason=sheet_closed|aborted And the response is HTTP 200 with extension_revoked=true When no close/abort is received within G seconds Then the pending extension automatically transitions to applied and the audit record is updated to status=applied
Enforcement of Extension Caps and Maximum Hold TTL
Given tenant config sets max_total_extensions_per_hold=N and max_hold_ttl_seconds=M When successive pay_sheet_opened events are processed for hold H Then the count of applied extensions never exceeds N And H.expiration_time never exceeds now + M (or the configured absolute TTL cap for H) And any event that would exceed a cap results in HTTP 200 with no extension applied and limit_reason set to extensions_cap_reached or ttl_cap_reached And an audit record is written with status=rejected and reason=cap_exceeded
Server-Side Verification and Security of pay_sheet_opened
Given the server receives pay_sheet_opened with hold_token T, user context U, and idempotency_key K When T is invalid, expired, does not match an active hold, or the hold is not owned by U Then no extension is applied and the response is an error (401 for invalid signature, 403 for ownership mismatch, 409 for inactive/expired hold) And an audit record is written with status=rejected and the specific reason When T is valid and hold is active and owned by U Then processing continues per extension rules and no elevation of privileges occurs
Rate Limiting of pay_sheet_opened per Hold/User
Given configured rate limits of R events/min per hold and P events/min per user When pay_sheet_opened events exceed R for the same hold or P for the same user within the rolling 60-second window Then excess events are not processed (no extension) and the response is HTTP 200 with rate_limited=true and unchanged expiration And audit entries are recorded with status=rejected and reason=rate_limited
Cross-Platform Consistency (Web and Mobile) and Countdown Sync
Given identical tenant config for extension_increment_seconds and caps When a user opens the payment sheet on web or mobile for the same active hold Then the computed extension and resulting expiration are identical across platforms And both clients receive the updated expiration within 500 ms of server processing And the on-screen countdown updates within 1 second to reflect the new expiration And the hold remains active and consistent if the user switches platform during the pending period
Verified Identity/Payment Extension
"As a customer completing verification, I want my hold to be extended automatically once I pass the security check so that I’m not penalized for mandatory verification steps."
Description

Extend the hold duration upon successful identity or payment verification events (e.g., SCA/3DS/OTP), using webhook-driven, server-to-server confirmation from the payment provider. Apply a configurable extension amount with a hard cap, and do not extend on soft-declines or failed challenges. Protect against replay with signed webhooks and idempotency keys, and log all verification-driven extensions for audit. Store only minimal, tokenized references to remain compliant with privacy and PCI constraints.

Acceptance Criteria
Extension on Successful Verification within Active Hold
Given an active seat hold with expiry T0, a configured extension amount E, and a hard cap C measured from hold_created_at When the system receives a server-to-server webhook for the same checkout/intent indicating a successful identity or payment verification outcome and the webhook signature is valid Then the new hold expiry is set to min(T0 + E, hold_created_at + C) and is persisted atomically And the webhook responds 200 OK within 1s of receipt And exactly one extension is applied per unique provider event_id
No Extension on Soft-Decline or Failed Challenge
Given an active seat hold When the system receives a validated webhook indicating soft-decline, failed 3DS/SCA/OTP challenge, or any status not classified as successful verification/authentication by the provider mapping Then no change is made to the hold expiry and no extension is applied And the webhook responds 200 OK with reason "no_extension:unsuccessful_verification" And the hold proceeds to expire and cascade per normal rules if not otherwise completed
Webhook Authentication and Idempotency
Given an incoming verification webhook When the request signature validates against the configured secret/public key and the signed timestamp is within the allowed skew window (e.g., <= 5 minutes) Then the event is eligible for processing When the signature is invalid or the timestamp is outside the window Then respond 4xx and perform no state changes When a webhook with an already-seen idempotency key or provider event_id is received (including concurrent deliveries) Then respond 200 OK and apply no additional extensions beyond the first processed instance And client-side events alone MUST NOT extend holds; only validated provider webhooks may trigger extensions
Post-Expiry Handling and Waitlist Protection
Given a hold that has already expired and the seat has been cascaded to the next user on the waitlist When a late successful verification webhook arrives referencing the original hold/intent Then the system MUST NOT resurrect the expired hold, MUST NOT reclaim the seat, and MUST NOT extend the original hold And the webhook responds 200 OK with reason "no_extension:hold_expired" And no double-booking or waitlist disruption occurs under load or concurrent deliveries
Audit Log for Verification-Driven Extensions
Given an extension is applied due to a successful verification webhook When the update is committed Then an immutable audit record is written containing at minimum: hold_id, org_id, class_id, user_token, payment_intent_token, provider, provider_event_id, received_at, validated_at, previous_expiry, new_expiry, extension_ms_applied, cap_reached_flag, signature_validation=true, and processor_instance_id And audit records are queryable by hold_id and by time range and exportable in JSON/CSV And all verification webhooks that do not result in extension are also logged with reason codes without storing sensitive payload data
Data Minimization and PCI/Privacy Compliance
Given storage and logging of verification-driven events Then only tokenized/pseudonymous references are persisted (e.g., payment_intent_id, payment_method_token, user_token) and no PAN, CVC, full track data, or raw OTP/3DS challenge artifacts are stored And any cardholder details present in webhook payloads are redacted before logging, with masked values only when required (e.g., last4, brand) And attempts to persist disallowed fields are blocked by schema and fail tests at build-time And data retention for these logs follows the platform’s privacy policy and is configurable, with a minimum retention control and documented DPIA/SOC controls
Configurable Extension Amount and Hard Cap Enforcement Across Multiple Events
Given organization/class-level configuration for extension amount E and hard cap C with sensible platform defaults When multiple distinct successful verification events are received for the same checkout/intent (e.g., identity verified then payment authenticated), each with a unique provider event_id and within an active hold Then each event may extend the hold by E without ever exceeding hold_created_at + C And when the hard cap has been reached, subsequent events result in 200 OK with reason "no_extension:cap_reached" and no expiry change And configuration changes take effect for new holds within 1 minute and are safely cached with fallback to defaults on cache misses
Countdown Timer UI and Alerts
"As a user reserving a spot, I want a clear countdown that matches the actual hold so that I know exactly how much time I have before I lose the reservation."
Description

Render a synchronized visual countdown on booking and checkout screens that reflects the server-authoritative hold TTL with sub-minute accuracy. Support accessible live regions and clear status messages, localize time formats, and theme to match white‑label branding. Provide optional pre‑expiry nudges (banner/toast) and disable actions when the hold expires, prompting users to reattempt or join the waitlist. Ensure the countdown continues seamlessly across page reloads, app backgrounding, or device switches.

Acceptance Criteria
Synchronized Countdown Reflects Server TTL
Given an active seat hold with a server TTL of N seconds When the booking or checkout screen is opened Then the countdown renders within 500 ms and displays N seconds remaining with a deviation ≤ 2 seconds from server TTL And the countdown updates at least once per second And if the server updates the TTL (shorten or extend), the UI reflects the new remaining time within 2 seconds And when the server TTL reaches zero, the countdown reaches 0 and triggers the expiry flow without any client-side extension
Seamless Countdown Continuity Across Reloads and Devices
Given an active seat hold and remaining time R When the user reloads the page or force-refreshes the app Then the countdown resumes from the correct remaining time R’ (server-derived) with a deviation ≤ 2 seconds When the app is backgrounded for T seconds and then foregrounded Then the countdown reflects R − T (not reset or paused) with a deviation ≤ 2 seconds When the same user opens the booking/checkout on a second device while the first is active Then both devices show the same remaining time within 2 seconds of each other and stay synchronized
Accessible Live Updates and Status Announcements
Given a screen reader is active When the countdown is visible Then the timer element has role="timer" and aria-live="polite" and exposes an accessible name that includes the remaining time And announcements are rate-limited to key thresholds (e.g., 60s, 30s, 10s, 5s, 0s) and not every second When the hold expires Then an assertive live announcement communicates expiration and next steps, and the expiry dialog receives programmatic focus without stealing focus beforehand And all timer and alert elements meet WCAG 2.2 AA for semantics and color contrast (no critical accessibility violations)
Localized Time Formats and Copy
Given the tenant locale is set (e.g., en-US, en-GB, fr-FR, es-ES, de-DE, ja-JP, ar) When the countdown and related messages are displayed Then all static strings (labels, nudge text, expiry text) are localized with correct pluralization And the remaining time is formatted as a localized duration using zero-padded minutes and seconds (e.g., mm:ss) with locale-appropriate numerals and separators And right-to-left locales render correctly with proper layout and reading order And if a translation key is missing, the UI falls back to English without breaking layout
Configurable Pre-Expiry Nudge Alerts
Given pre-expiry nudges are enabled with a threshold T seconds and style (banner or toast) When the remaining time first crosses T while the screen is in the foreground Then a single nudge displays within 1 second, is dismissible, and includes the current time remaining And the nudge is announced to assistive technologies via an aria-live region And no additional nudges appear for the same hold in the same session unless the threshold configuration changes When nudges are disabled in configuration Then no nudge renders regardless of remaining time
Expiry Handling: Disable Actions and Recovery Paths
Given the countdown reaches 0 or the server reports the hold has expired Then Pay/Confirm and form inputs are disabled within 500 ms and visually indicate disabled state And an expiry message is shown with CTAs to Retry Hold or Join Waitlist according to class capacity and waitlist settings When Retry Hold is selected and a seat is available Then a new hold is requested, success re-enables actions and restarts the countdown, and failure shows an error without re-enabling actions When Join Waitlist is selected and waitlist is enabled Then the user is placed on the waitlist and the UI confirms the action And if a late payment submit occurs after expiry, the server response triggers the same expiry flow without processing payment
White-Label Theming of Countdown and Alerts
Given tenant branding tokens (colors, typography, spacing, radius, light/dark mode) When rendering the countdown, nudge, and expiry components Then the components use tenant tokens for background, text, and accent without inline hard-coded styles And all text/icon colors maintain contrast ratio ≥ 4.5:1 against their backgrounds And dark mode and high-contrast settings produce legible countdown and alerts with no invisible states And if a token is missing, the UI falls back to defaults without layout shift or unreadable colors
Waitlist Auto-Cascade Fulfillment
"As a waitlisted learner, I want to be automatically offered a seat when one becomes available so that I don’t have to constantly check and risk missing my chance."
Description

When a hold expires or is released, automatically cascade the freed seat to the next eligible person in the waitlist using fair, deterministic rules (e.g., FIFO with priority tiers). Issue time-bound offer holds with configurable windows, notify via email/SMS with deep links, and handle declines or expiries by iterating to subsequent waitlisters. Implement concurrency-safe allocation with transactional checks and comprehensive audit trails to prevent double allocation and ensure transparency.

Acceptance Criteria
Cascade on Hold Expiry
Given a class instance with zero available seats and an active reservation hold that reaches its exact expiration timestamp When the hold expires Then the system immediately creates a new offer hold for the next eligible waitlister determined by the configured deterministic rule set And the new hold window duration is set according to the active HoldGuard policy for the class instance And the freed seat remains unavailable for general booking while the new hold is active And unique deep-link notifications are sent via email and SMS to the selected waitlister within 5 seconds of hold creation And an audit event HOLD_EXPIRED_CASCADED is recorded with prior holder ID, next holder candidate ID, timestamps, and hold window And no overlapping holds exist for the same seat instance at any time
Cascade on Manual Release
Given an instructor or admin manually releases a reserved seat for a class instance with a non-empty waitlist When the release action is confirmed and persisted Then the system allocates an offer hold to the next eligible waitlister without requiring manual intervention And if M seats are released in one action, M distinct offer holds are created for the top M eligible waitlisters And each offer hold inherits the current HoldGuard policy window for the class instance And email and SMS notifications containing a unique deep link are sent to each selected waitlister within 5 seconds And an audit event MANUAL_RELEASE_CASCADED is recorded with actor, reason, seat count, recipient IDs, and timestamps And inventory never reflects more available seats than physically freed during the transaction
Deterministic Next-Eligible Selection with Priority Tiers
Rule: Candidates are ordered by (1) priority tier descending, (2) waitlist join timestamp ascending, (3) waitlist entry ID ascending as final tiebreaker Rule: Ineligible entries (BLOCKED, MEMBERSHIP_EXPIRED, ALREADY_ENROLLED, ACTIVE_HOLD_EXISTS, PAYMENT_BANNED, SELF_DECLINED_COOLDOWN) are skipped and annotated with a skip reason code in audit Rule: The selection result is stable across repeated evaluations with identical inputs and time; two evaluations within 100 ms produce the same candidate ID Rule: When multiple seats are available, selection takes the next K candidates according to the same ordering without reordering between seats Rule: The chosen candidate ID, ordering inputs, and skip reasons for bypassed entries are written to audit with a deterministic selection hash
Configurable, Context-Adaptive Offer Hold Windows
Given HoldGuard policy {default_hold=10m, near_start_threshold=30m, near_start_hold=5m, pay_sheet_extension=+2m per open, verification_extension=+2m, max_hold_cap=15m} When an offer hold is created >=30m before class start Then the expiration is now + 10m When an offer hold is created <30m before class start Then the expiration is now + 5m When the recipient opens the pay sheet while the hold is active Then the hold is extended by +2m up to a maximum of 15m total remaining time When the recipient passes verification while the hold is active Then the hold is extended by +2m up to a maximum of 15m total remaining time And the deep link opens the pay sheet with the hold applied and displays a countdown matching the server-side expiration within ±1s And when the hold expires, the seat is immediately revoked and eligible for next cascade
Iterative Fulfillment on Decline or Expiry
Given an active offer hold for a waitlister When the waitlister explicitly declines via deep link or the hold expires without purchase Then the system marks the offer as Declined or Expired and immediately iterates to the next eligible waitlister And notifications with unique deep links are sent to the next candidate within 5 seconds of iteration And the same person never has more than one active offer hold for the same class instance And if the waitlist is exhausted, the freed seat returns to general inventory and is visible for public booking within 3 seconds And audit events WAITLIST_DECLINED_CASCADED and WAITLIST_EXHAUSTED (if applicable) are recorded with timestamps and candidate IDs
Concurrency-Safe Allocation and Idempotent Acceptance
Given concurrent triggers (hold expiry, manual release, user acceptance) for the same class instance When up to N=50 concurrent processes attempt to allocate or accept the same seat(s) Then exactly-once allocation occurs; no double booking is recorded and available seat count never drops below zero And acceptance API enforces idempotency via key; repeated submits of the same key within 30s return the same result without creating duplicate enrollments And conflicting acceptance attempts receive a 409 Conflict with a stable error code and no side effects And notifications are deduplicated via message ID to ensure at-most-once delivery per recipient per offer And all operations execute within a single transaction boundary with rollback on failure and corresponding audit entries
Comprehensive Audit Trail and Transparency
Rule: Every state transition creates an immutable audit record with fields {event_type, class_instance_id, seat_id/qty, actor (system/user/admin), prior_state, new_state, candidate_id(s), reason_code, timestamp (UTC), correlation_id} Rule: Audit entries are queryable by class instance, user, correlation_id, and time range within 200 ms for datasets up to 1M events Rule: An export endpoint produces a CSV of all waitlist cascade events for a class instance within 10 seconds for up to 100k rows Rule: The deep-link notifications include correlation_id so user support can trace an offer end-to-end Rule: End users can see their own offer history for a class instance (Offered, Declined, Expired, Accepted) with timestamps accurate to ±1s
Admin Controls, Policies, and Analytics
"As an admin, I want to configure hold policies and review hold performance analytics so that I can balance fairness, conversion, and operational needs across classes."
Description

Provide admin settings to configure default hold durations, start‑time proximity shortening thresholds, extension limits, and eligibility rules for extensions. Allow per‑class overrides and environment‑safe previews. Expose dashboards and exports for hold lifecycle metrics (created, extended, expired, converted), waitlist cascade performance, and drop‑off reasons, with filters by class, instructor, and channel. Enforce role‑based access and surface API endpoints for programmatic management.

Acceptance Criteria
Configure default hold policy settings
Given I am an Org Admin with HoldGuard:Admin permission When I open Settings > HoldGuard and update: - Default hold duration to an integer number of minutes - Start-time proximity rule with a threshold (minutes before class start) and a shortened duration (minutes) - Auto-extension policy with max extensions per hold and per-extension duration (minutes) - Eligibility triggers: Pay sheet opened, Verification passed And I click Save Then the new settings persist, are audit-logged with user and timestamp, and apply only to holds created after save And invalid inputs (non-integers, zero/negative values, shortened duration greater than default, negative thresholds) prevent saving and show inline error messages
Per-class hold policy override
Given a class session exists and I have permission to manage it When I enable Hold policy override on the class and set override values Then the class shows the effective policy as Override and lists the overridden values And any new holds for that class use the override values And removing the override reverts the class to the default policy And existing holds are not modified by adding or removing overrides
Environment-safe hold behavior preview
Given Preview mode is available When I simulate a booking flow for a selected class at configurable times relative to start And I simulate opening the pay sheet and passing verification Then I see a countdown and labeled extensions/shortening that reflect the current effective policy And no real holds, seat allocations, payments, notifications, or webhooks occur; inventory remains unchanged And Preview is clearly labeled non-destructive and is accessible only in non-production environments or behind a feature flag
Hold lifecycle analytics dashboard with filters
Given I have Analytics:View permission When I open HoldGuard Analytics and apply filters for date range, class, instructor, and channel, and select a timezone Then I see metrics for holds created, extended, expired, converted, conversion rate, average extensions per hold, and median time-to-convert/expire And I see waitlist cascade metrics: cascades triggered, seats reallocated via cascade, median time-to-fill, and cascade conversion rate And I see drop-off reasons with counts and percentages And all figures reflect the applied filters and timezone and can be drilled down to class detail
Export hold and cascade metrics
Given filters are applied in HoldGuard Analytics and I have Export permission When I export to CSV Then a file is generated within 10 seconds containing only the filtered dataset And the file includes columns: hold_id, class_id, class_start_at, user_id_hashed, created_at, status, extensions_count, final_state_at, drop_off_reason, channel, instructor_id And timestamps are ISO 8601 with timezone offset and values use a consistent delimiter and header row And the row count equals the Records count shown in the UI for the same filters And users without Export permission cannot export
Role-based access for settings, analytics, exports, and APIs
Rule: Admins can view and edit HoldGuard settings, view analytics, export data, and manage API credentials Rule: Analysts can view analytics and export data; cannot edit settings Rule: Instructors can view analytics for their own classes only; cannot export unless granted Export permission; cannot edit settings Rule: Support can view effective policies per class; no access to analytics, exports, or settings Rule: Unauthorized actions are blocked with 403 and hidden in the UI; all access attempts are audit-logged
Programmatic management via API
Given I authenticate with a valid key and scope When I call: - GET /v1/hold-policies/default - PUT /v1/hold-policies/default - GET /v1/classes/{class_id}/hold-policy - PUT /v1/classes/{class_id}/hold-policy - GET /v1/analytics/holds with filters for date range, class, instructor, and channel Then authorized requests return 200/201 with schemas matching the OpenAPI spec; unauthorized return 401; forbidden 403; invalid inputs 400; not found 404 And PUT operations are idempotent and return the effective policy that will apply to new holds And analytics responses totals match the dashboard when called with identical filters and timezones And all requests and changes are rate-limited and audit-logged

Last‑Call Burst

In the final minutes before class, send offers to a small, smart batch (e.g., top 3) with first‑pay‑wins logic. Timers compress and cascades accelerate so empty seats are filled fast, with safeguards to prevent double‑booking. Works hand‑in‑hand with doorline QR waitlists to convert walk‑ins when digital claims lapse.

Requirements

Smart Last-Call Candidate Ranking
"As a studio manager, I want the system to automatically select the top few likely attendees for last‑minute offers so that empty seats are filled quickly without blasting our entire list."
Description

Implements a lightweight scoring engine to select a small, high-likelihood batch (e.g., top 3) of recipients for last‑minute seat offers. Inputs include historical show‑up rate, payment velocity, distance/proximity, past response time to urgent offers, waitlist position, member status, and explicit opt‑ins. The engine outputs a ranked list with ties broken deterministically. Integrates with ClassTap’s user profiles, past booking data, and notification preferences. Provides configurable cohort size, exclusion rules (e.g., already declined today), quiet‑hours overrides, and fallback behavior when fewer than the target number qualify. Improves fill rates by targeting users most likely to convert while minimizing spam and protecting sender reputation.

Acceptance Criteria
Deterministic Top‑N Ranking for Identical Inputs
Given the same candidate set and identical signal inputs And no underlying data has changed between runs When the ranking engine is executed twice within a 5‑minute window Then the ordered list of candidate IDs and their ranks are exactly identical across runs And any ties are resolved using the documented tie‑breaker so that tied candidates appear in the same order across runs
Configurable Cohort Size with Fallback
Given a target cohort size of 3 And there are only 2 eligible candidates When the engine runs Then it returns exactly 2 candidates, preserving rank order And given 5 eligible candidates with target cohort size 3 When the engine runs Then it returns the top 3 ranked candidates only and excludes the remainder And the output contains no duplicate candidate IDs
Exclusion Rules: Declined Today, Booked, Opt‑Outs, and Quiet Hours
Given a candidate who has declined a last‑call offer today Or already holds a confirmed booking for the same class/time or an overlapping class Or has last‑call offers opt‑out enabled in notification preferences Or is within quiet hours and override=false When the engine runs Then that candidate is excluded from the ranked output And when override=true and the actor is authorized as admin Then quiet‑hours candidates may be included, but opt‑outs and already‑booked candidates remain excluded
Signal‑Based Scoring Produces Expected Order
Given a fixture dataset where Candidate A has higher historical show‑up rate, faster payment velocity, closer proximity, faster response to urgent offers, higher waitlist priority, active member status, and explicit last‑call opt‑in relative to Candidate B When the scoring engine calculates scores Then Candidate A’s score is greater than Candidate B’s and A ranks above B And scores are numeric floats and calculation completes without error when some signals are missing (missing signals contribute zero rather than causing failure) And increasing any single positively correlated signal for a candidate (holding others constant) does not decrease that candidate’s total score
Waitlist Priority and Availability Safeguards
Given a class with at least one open seat and an active waitlist And a candidate currently has a live hold/booking for that seat or a time‑overlapping booking in the system When the engine runs Then that candidate is excluded to prevent double‑booking And among otherwise similar candidates, a better (lower number) waitlist position increases ranking relative to non‑waitlisted candidates
Performance Budget for Rapid Last‑Call Selection
Given a class with 1,000 eligible candidates and target cohort size=3 When the engine computes the ranked list on production‑like infrastructure Then time to produce the top‑3 result is ≤ 200 ms at the 95th percentile And peak memory usage for the ranking task is ≤ 50 MB And the computation completes without timeouts or partial results
First‑Pay‑Wins Atomic Seat Allocation
"As an instructor, I want the seat to go to whoever completes payment first so that there are no double‑bookings or manual fixes before class starts."
Description

Ensures strict first‑pay‑wins logic with transactional safeguards so that the first completed payment claims the seat and all concurrent attempts are cleanly rejected or rerouted. Uses short‑lived claim tokens, per‑seat pessimistic locks, and idempotent payment intents to prevent double‑booking. Includes a brief configurable payment window (e.g., 2–3 minutes) with visible countdown and automatic release on timeout or payment failure. Integrates with ClassTap’s inventory, payment gateway, and waitlist so that released seats immediately trigger the next cascade step. Provides clear user feedback states (held, processing, success, expired) and audit logs for dispute resolution.

Acceptance Criteria
Concurrent Checkout: First Pay Wins
Given a class with 1 remaining seat and three users A, B, and C start checkout within the Last‑Call window When their payment intents are processed concurrently Then the first payment intent to reach a succeeded state within the payment window atomically allocates the seat And the inventory decrements exactly once And all other in‑flight intents are rejected with a seat_taken error within <= 1 second of winner commit And rejected users see a message offering to join the waitlist or subscribe to notifications
Short‑Lived Claim Token and Visible Countdown
Given a user taps a Last‑Call offer link When the checkout session loads Then the system issues a short‑lived claim token bound to seatId, customerId, and classId with TTL = configured payment window (120–180s) And the UI displays a server‑synchronized countdown with max drift <= 1s And on TTL expiry the token becomes invalid, the seat hold is released, and the UI transitions to expired state And refreshing or opening a second tab does not extend the TTL
Per‑Seat Pessimistic Locking and Atomic Commit
Given a payment intent is created for seat S under a valid claim token When the intent is confirmed Then a per‑seat pessimistic lock is acquired prior to inventory check and held through commit And exactly one transaction can decrement inventory for seat S And the lock is released on success, failure, or timeout, with lock lifetime <= payment window + 5s And attempts to allocate S while locked return busy_or_taken within <= 200ms
Idempotent Payment Intents and Duplicate Request Handling
Given a user double‑clicks Pay or the network retries When duplicate confirm requests are sent with the same idempotency key Then the gateway processes the payment exactly once And subsequent duplicate requests return the same final status and receipt data And no additional seats are allocated and no duplicate charges occur And without an idempotency key, the API rejects duplicates within the same session with 409 Conflict
Automatic Release and Cascade to Next Candidate
Given a payment fails or a claim token expires When the seat is released Then class inventory increments by 1 atomically And the next candidate in the Last‑Call batch or QR waitlist is notified within <= 5s And the prior user’s session is disabled from payment (no further charges allowed) and shows expired with retry options And an event SeatReleased is emitted with reason, prior holder, and next candidate reference
User Feedback States and Localization
Given a user proceeds through checkout states When the seat is held Then the UI shows held with countdown When payment is confirming Then the UI shows processing (spinner) and disables inputs When payment succeeds Then the UI shows success with confirmation details and receipt link When seat is lost to another payer Then the UI shows seat taken and offers to join the waitlist or view other classes And all messages are localized per tenant language settings
Audit Trail for Dispute Resolution
Given any seat allocation attempt reaches a terminal state (success, failed, expired, seat_taken) When the record is written Then the audit log stores timestamp (UTC), tenantId, classId, seatId, customerId (tokenized), claimTokenId, lockId lifecycle, paymentIntentId, gateway response code, final outcome, and cascade notifications sent And logs are immutable, searchable by date range and identifiers, and exportable as CSV And PII fields are redacted per policy while preserving join keys And audit entries are available for query within <= 10s of event
Expiring Offers with Accelerated Cascades
"As a front‑desk coordinator, I want offers to expire quickly and cascade automatically so that we can fill seats fast without manual intervention."
Description

Delivers time‑boxed offers with visible countdown timers that compress as class start approaches, automatically cascading to the next ranked cohort upon expiry or decline. Supports dynamic window lengths (e.g., 5 minutes then 3 then 1) and configurable maximum rounds. Tracks per‑recipient state (sent, opened, clicked, claimed, expired) to avoid duplicates and to trigger cascades only when capacity remains. Integrates with the ranking engine and seat allocation layer so that each cascade respects real‑time availability. Reduces idle time between rounds, maximizing the chance to fill last seats in the final minutes.

Acceptance Criteria
Dynamic Countdown Timer and Window Compression
Given a class starting at T0 with config window sequence [5m,3m,1m] When the Last‑Call Burst is initiated and an offer is sent Then the offer displays a visible countdown matching the current window length and decrements every second Given the remaining time to start is > 8m When the next round is generated Then the window length is 5m Given the remaining time to start is <= 8m and > 3m When the next round is generated Then the window length is 3m Given the remaining time to start is <= 3m When the next round is generated Then the window length is 1m Given an offer countdown reaches 0 When the timer expires Then the offer state transitions to expired within 1s and cannot be claimed
Cascade Triggering and Maximum Rounds
Given capacity > 0 and maxRounds = M When all offers in the current round are in a terminal state (claimed, declined, expired) Then the next round is sent within 3s Given any recipient explicitly declines When capacity > 0 and other offers remain pending Then the system evaluates the round and sends the next round if all active offers for remaining seats are terminal, within 3s Given roundsSent = M When the current round completes Then no further rounds are sent Given capacity becomes 0 at any time When there are pending offers Then pending offers are auto‑expired within 2s and no further rounds are sent
Per‑Recipient State Tracking and Deduplication
Given an offer is sent to a recipient When delivery, open, click, claim, decline, expiration events occur Then the system stores state transitions in order with UTC timestamps Given a network retry occurs for the same offer When the transport resends Then the recipient receives at most one message and the stored state remains idempotent by offerId+recipientId Given a recipient has a terminal state for the class (claimed, expired, declined) When subsequent rounds are generated Then the recipient is excluded from further invites for that class Given a recipient claims a seat When the claim is confirmed Then all other active offers for that recipient for the same class are auto‑expired within 2s
Ranking Engine Cohort Selection
Given a ranked list R and batchSize B When generating round k Then select the top B candidates from R who have no terminal state and no active offer Given candidates tie on score When selecting within a round Then break ties by most recent positive engagement timestamp, then earliest waitlist join time Given a candidate has a conflicting booking overlap When generating cohorts Then exclude that candidate from selection Given a cohort of up to 10,000 candidates exists When selecting B candidates Then selection completes within 200ms
First‑Pay‑Wins Allocation and Double‑Booking Prevention
Given one remaining seat and multiple recipients attempt checkout When payment authorization requests arrive concurrently Then exactly one payment is captured and the seat is allocated to the first successful commit; other attempts are prevented from capture and receive a “Seat no longer available” error Given a seat is allocated When capacity decreases Then capacity never drops below 0 and is consistent across services Given a seat is allocated When conflicting active offers exist for that seat count Then those offers exceeding new capacity are auto‑expired and recipients are notified within 2s Given a payment authorization succeeds but capacity becomes 0 before commit When commit is attempted Then the authorization is voided or not captured and no seat is allocated
Inter‑Round Latency and Acceleration
Given a round completes (no pending offers for remaining seats) When capacity > 0 and roundsSent < maxRounds Then the next round is dispatched within 3s Given system load of 1,000 concurrent classes with active cascades When measuring inter‑round latency Then median latency <= 2s and 95th percentile <= 5s Given remaining time to start decreases past configured thresholds When generating subsequent rounds Then the system applies the shorter window lengths without manual intervention
Real‑Time Availability Sync with Seat Allocation Layer
Given real‑time availability changes due to external actions (e.g., cancellation, manual adjustment) When capacity increases or decreases Then active and next‑round decisions use the latest capacity within 1s of the change Given capacity drops to 0 mid‑round When evaluating whether to cascade Then no new round is sent and pending offers are auto‑expired within 2s Given capacity increases while roundsSent < maxRounds When evaluation runs Then a new round is dispatched within 3s, respecting ranking order
Multi‑Channel Delivery & Compliance Guardrails
"As a participant who opted into alerts, I want timely, concise last‑minute notifications on my preferred channel so that I can grab a spot without being spammed."
Description

Sends last‑call offers via SMS, email, and push (where available) with channel prioritization based on historical responsiveness and user consent. Implements per‑user and global rate limiting, quiet‑hour suppression with emergency overrides, opt‑out honoring, link tracking, and failover between channels if delivery fails. Templates support dynamic content (class name, start time, price, seat count, countdown link). Ensures compliance with regional messaging rules and logs send outcomes for analytics. Integrates with ClassTap’s notification service and branding to keep white‑label look and feel.

Acceptance Criteria
Channel Prioritization by Consent and Responsiveness
Given a recipient has consent status per channel and a historical response score per channel, When a Last‑Call Burst is triggered for that recipient, Then the system selects the highest‑ranked channel among consented channels based on response score and recency, And it excludes any channel without explicit consent, And it records the final channel order used for the attempt. Given no historical response data exists for a recipient, When prioritization runs, Then the system uses the tenant’s default channel order and still honors consent per channel. Given push is unavailable for the device, When prioritization runs, Then push is removed from consideration and the next consented channel is promoted. Given two channels have equal priority score, When prioritization runs, Then tie‑break by most recent successful delivery within the last 30 days.
Per‑User and Global Rate Limiting
Given the tenant per‑user limit is 2 last‑call offers per rolling 24 hours across all channels, When attempting to send a third offer to the same user within that window, Then the send is suppressed with reason "rate_limited_user" and no message is sent on any channel. Given the tenant global rate limit is 300 messages per minute, When the system would exceed this rate, Then messages are queued and released in FIFO order without exceeding the ceiling, preserving burst ordering per class. Given a per‑channel cooldown of 15 minutes per user per channel, When a subsequent attempt occurs within 15 minutes on the same channel, Then the attempt is suppressed with reason "rate_limited_channel". Given a suppressed send due to rate limits, When the suppression occurs, Then an analytics event is emitted with user_id, class_id, channel, reason, and timestamp.
Quiet Hours Suppression with Emergency Override
Given tenant quiet hours are 21:00–08:00 in the recipient’s local timezone, When a Last‑Call Burst falls within quiet hours, Then the send is suppressed with reason "quiet_hours" and scheduled for the first minute after 08:00 unless the class would have started by then. Given an authorized operator marks a class as an emergency override, When the burst falls in quiet hours, Then messages are allowed to send only on channels permitted by regional rules for overrides, And the event is tagged with "override=true". Given the recipient’s timezone is unknown, When evaluating quiet hours, Then the system uses the tenant’s default timezone for suppression decisions and records the assumed timezone in the event metadata. Given quiet hours suppression occurs, When the class start time is earlier than the next allowed window, Then the message is dropped (not queued) and logged with reason "expired_window".
Opt‑Out and Unsubscribe Honor
Given a recipient has opted out of SMS, When a Last‑Call Burst is sent, Then SMS is not attempted, And no message is sent via SMS even if it is the highest priority channel. Given a recipient clicks the email unsubscribe link or replies STOP to SMS, When the system receives the event, Then the recipient’s consent status for that channel is updated within 60 seconds, And a confirmation (where legally required) is sent only if compliant. Given a recipient is opted out on all channels, When a burst targets that recipient, Then no message is sent, And an analytics event is logged with reason "all_channels_opted_out". Given tenant branding requires mandatory opt‑out language in SMS/email, When rendering the message, Then the opt‑out copy is present and localized according to the recipient’s locale.
Cross‑Channel Failover with De‑duplication
Given the primary channel attempt returns a definitive failure (e.g., bounce, unreachable, provider error) within 30 seconds, When failover is enabled, Then the system attempts the next prioritized consented channel within 10 seconds. Given the primary channel has no delivery receipt within 60 seconds and the class starts in < 15 minutes, When failover is enabled, Then the system triggers conditional failover to the next channel while marking the notification with a de‑dupe token. Given any one channel eventually delivers successfully, When another in‑flight attempt would deliver the same offer, Then the system cancels or suppresses the duplicate with reason "dedup_success" so the user receives at most one offer notification. Given all channels fail or are suppressed, When the attempts complete, Then the final state is "failed" with the last error code and no more attempts are made.
Dynamic Templates with Class and Seat Data + Countdown Link Tracking
Given a template contains {{class_name}}, {{start_time_local}}, {{price}}, {{available_seats}}, and {{countdown_url}}, When rendering a message, Then all variables resolve without placeholders, formatted to the recipient’s locale and currency. Given {{available_seats}} is rendered, When the message is sent, Then the value reflects inventory as of send time with a maximum staleness of 5 seconds. Given {{countdown_url}} is rendered, When the recipient clicks the link, Then tracking records click -> claim_start -> payment events with user_id, class_id, channel, campaign_id, and de‑dupe token. Given a variable is missing from context, When rendering, Then the message is not sent and is logged with reason "template_variable_missing" and the affected key.
Compliance, White‑Label Branding, and Send Outcome Logging
Given the recipient’s region and channel, When composing an SMS/email/push, Then regional compliance rules are enforced (e.g., required sender ID, consent gating, opt‑out language, quiet‑hour laws, and message length/segmentation limits) before send is allowed. Given the tenant’s white‑label branding, When rendering email and push, Then the correct from‑name/sender ID, logo, colors, and reply‑to are applied; For SMS, the studio name prefix is included where required by policy. Given a message lifecycle event occurs (queued, sent, delivered, failed, suppressed), When logging, Then an immutable record is stored with tenant_id, user_id, class_id, channel, campaign_id, provider_message_id (if available), status, reason, and timestamps, retained for at least 90 days and surfaced in analytics. Given ClassTap’s notification service is the delivery backbone, When the Last‑Call Burst sends, Then integration uses the service’s APIs and respects per‑provider routing without bypassing audit hooks.
Doorline QR Waitlist Handshake
"As a walk‑in attendee, I want to scan a code and immediately know if I can claim an open seat so that I don’t wait in line only to be turned away."
Description

Connects in‑venue QR waitlists with the Last‑Call Burst workflow so walk‑ins can instantly queue and claim seats if digital offers lapse. Generates a short‑lived claim link upon QR sign‑up that respects first‑pay‑wins and cascade rules. Displays current countdown and availability to set expectations. Automatically promotes doorline users when a round expires or a token releases, with support for payment on device at the door. Synchronizes states to prevent conflicts between remote claims and walk‑ins. Accelerates conversion of physical foot traffic into confirmed bookings during the last minutes before class.

Acceptance Criteria
Walk-in QR signup issues short-lived claim link
Given a walk-in scans the Doorline QR for a class in the Last-Call window with an active Burst When they submit required min fields (name and mobile) Then a unique claim token is created and an SMS with a claim link is sent within 5 seconds Given a claim link is issued When the user opens it Then the page displays a visible countdown in mm:ss and remaining seats Given a claim link is issued Then its TTL is 120 seconds from issuance Given multiple claim links are requested by the same phone for the same class and round When a new link is issued Then all prior links are invalidated and redirect to the newest link
First-pay-wins conflict resolution between remote and doorline
Given one remaining seat and both a remote recipient and a doorline user attempt to pay When the first payment is authorized by the processor Then that payer is confirmed and the seat inventory is decremented atomically Given a losing party submits payment after the seat is taken Then their authorization is voided or instantly refunded and they see "Seat taken" within 2 seconds and are returned to their previous queue position Given concurrent claim attempts Then inventory lock duration does not exceed 3 seconds and deadlocks are prevented Given a successful payment occurs Then all other open claim links for that class and round show "Seat taken" within 2 seconds
Countdown and live availability display to doorline users
Given a doorline user opens a valid claim link Then the UI shows current seat count and round countdown in mm:ss and updates at least once per second Given availability changes due to remote or doorline claims Then the doorline UI reflects the new seat count within 2 seconds Given the countdown reaches 00:00 and the user has not paid Then the Pay/Confirm controls are disabled immediately and the UI shows "Round expired" with an action to re-join the doorline
Automatic promotion on round expiry or token release
Given a Last-Call round expires with unclaimed seats Then the top N doorline users (N = unclaimed seats) are automatically promoted and receive claim links within 2 seconds Given a claim token is released due to cancel, expiry, or payment failure Then the next eligible doorline user is promoted within 2 seconds Given promotions occur Then promoted users are removed from the waitlist and remaining users shift up with accurate positions displayed Given cascade acceleration rules are enabled Then each subsequent round reduces link TTL by 20% (floor 30 seconds) until seats are filled or class start time is reached
Payment on device at the door
Given a staff member opens the Door Console for an active class When they select a promoted doorline user and start payment Then the device supports tap/chip or stored card and completes within the current link TTL Given door payment succeeds Then the booking is confirmed, the claim token is marked redeemed, and receipt is sent via SMS/email within 1 second Given door payment fails or times out Then the claim token is released and the user is re-queued or marked not-promoted per rules, and next eligible user is promoted within 2 seconds
State synchronization across channels
Given any state change (seat taken, link issued/redeemed/expired, promotion) Then the change propagates to instructor dashboard, public booking page, door console, and claim pages within 2 seconds end-to-end Given a client reconnects after network loss When it resubscribes Then it reconciles to server state within 5 seconds using server timestamps and idempotency keys Given duplicate or out-of-order events are received by the server Then processing is idempotent and results in at most one confirmed booking or promotion per token
QR link expiration, reuse, and abuse safeguards
Given a claim link expires Then it cannot initiate payment and displays "Link expired" with an option to re-join the doorline Given a claim link has been redeemed Then any subsequent attempt to use it is blocked with "Already used" and no new booking is created Given more than 3 claim link requests occur from the same device or phone for the same class within 5 minutes Then subsequent requests are rate-limited for 5 minutes and logged with device fingerprint and phone Given a user is rate-limited in error When a staff member overrides from the Door Console Then a new single-use link is issued and prior links remain invalid
Instructor Controls & Live Monitor
"As an instructor, I want a simple dashboard to launch and oversee last‑call offers so that I can fill seats confidently without technical complexity."
Description

Provides a control panel to enable/disable Last‑Call Burst per class, set batch size, cascade rounds, minimum lead time, and optional last‑minute pricing or promo toggles. Includes a live monitor showing current round, recipients, countdowns, claims, failures, and remaining capacity with manual override actions (skip round, add recipient, cancel token). Offers presets by class type and saves defaults at studio level. Integrates with existing scheduling and class detail pages, respecting user roles and permissions. Reduces operational friction and builds trust through transparency and control.

Acceptance Criteria
Enable/Disable Last‑Call Burst Per Class
Given a class with a future start time and a user with edit permission When the user toggles Last‑Call Burst ON and saves Then the setting persists and is visible on class detail after reload And the class becomes eligible to run burst rounds subject to lead time Given a class within the configured minimum lead time window When the user attempts to enable Last‑Call Burst Then the system blocks save and displays an inline message explaining the lead time restriction Given Last‑Call Burst is enabled for a class When the user toggles it OFF and saves Then any scheduled or active rounds are halted and no new offers are sent
Configure Batch, Rounds, Lead Time, Pricing/Promo
Given the control panel is open When the user sets Batch Size, Cascade Rounds, and Minimum Lead Time Then only positive integers within allowed limits are accepted with inline validation for out-of-range values When the user toggles Last‑Minute Pricing or a Promo Code Then the preview reflects the price/promo and these values are included in subsequent offers When the user saves Then values persist, are audit logged with actor and timestamp, and are applied to the next burst execution
Live Monitor Real‑Time Status and Counters
Given a burst round is active When recipients are sent offers Then the monitor shows current round number, recipient list with per-recipient status (sent, delivered, claimed, expired, failed, canceled), per-recipient countdowns, and remaining capacity And the monitor updates within 3 seconds of any status change Given two recipients attempt payment for the last available seat at the same time When one payment is confirmed Then the other is marked failed with reason "Seat taken" and no double booking occurs Given remaining capacity reaches zero When the current round is still counting down Then the monitor auto-stops further rounds and terminates the countdown
Manual Overrides (Skip Round, Add Recipient, Cancel Token)
Given an active burst When the user clicks Skip Round and confirms Then the system immediately ends the current round, stops its countdown, and proceeds according to configured cascade logic Given an active or scheduled burst When the user selects Add Recipient and confirms Then a new recipient is added without duplication, an offer is sent, and the recipient appears with an active countdown Given a recipient with an active token When the user selects Cancel Token and confirms Then the token is invalidated, the recipient is marked canceled, any hold is released, and the seat is made available to the round logic And all override actions are audit logged and reflected on the monitor within 3 seconds
Presets by Class Type and Studio Defaults
Given a Studio Admin When they create or edit a preset with defined parameters Then the preset is saved with a name and appears in the preset selector Given a new class of a type with a preset When the control panel opens Then fields are auto-populated from the class-type preset or studio default Given an instructor overrides preset values on a class When they save Then the class uses the overridden values without changing the preset And changes do not retroactively alter active or completed bursts
Roles and Permissions Enforcement
Given user roles Studio Admin, Instructor (assigned), Front Desk, and Viewer When accessing the control panel Then Admin and assigned Instructor have edit access; Front Desk has read-only; Viewer sees no access When a user without edit permission attempts an edit via UI or API Then the action is blocked with HTTP 403 and a user-facing error, and the attempt is audit logged
Integration on Scheduling and Class Detail Pages
Given the scheduling page and the class detail page When a user with access navigates to either Then the Instructor Controls & Live Monitor are accessible in both contexts with consistent state Given the Last‑Call Burst feature flag is disabled at the studio When any page loads Then the controls and monitor are hidden and no burst actions are possible
Performance Analytics & Experimentation
"As a studio owner, I want to see which last‑call configurations perform best so that I can optimize fill rates and revenue over time."
Description

Captures and surfaces metrics such as send volume, open/click rates, conversion time, fill‑rate uplift, revenue per seat, no‑show deltas, and channel effectiveness. Breaks down performance by class type, time of day, and cohort size. Supports A/B configurations (e.g., batch size, timer lengths, message copy) with statistically sound comparisons. Exposes insights in the analytics tab and exports to CSV. Feeds learnings back into the ranking engine to improve future targeting. Enables data‑driven tuning of the Last‑Call Burst to maximize utilization and revenue.

Acceptance Criteria
Real-time Metrics Capture & Correct Attribution
Given a Last‑Call Burst runs for a class with offers sent via multiple channels When sends, opens, clicks, purchases, and seat assignments occur Then event records contain class_id, recipient_id, channel, experiment_id, variant, and ISO8601 timestamps with <=1s precision Then analytics reflect these events within 60 seconds at p95 latency Then conversion_time_seconds = purchased_at - sent_at for the attributed send Then a purchase is attributed to the most recent active offer for that class within the offer timer window plus 60s grace; otherwise it is not counted as a Last‑Call conversion Then each purchase is attributed to exactly one channel and one variant (no double counting across cascades) Then fill_rate = seats_sold / capacity at class start snapshot Then fill_rate_uplift = fill_rate - baseline_fill_rate, where baseline is the median fill_rate of the last 10 sessions of the same class type and time-of-day bucket in the past 28 days Then no_show_delta = no_show_rate(last‑call conversions) − no_show_rate(non‑last‑call attendees) for the same class instance
Analytics Tab: Dimensions, Filters, and Visualizations
Given a user opens Analytics > Last‑Call Burst When applying filters for date range, class type(s), time‑of‑day buckets (05:00–10:59, 11:00–16:59, 17:00–21:59, 22:00–04:59 local), cohort size buckets (≤10, 11–20, >20), channel, experiment_id, and variant Then charts and tables refresh within 2 seconds at p95 for datasets ≤100k rows Then user can apply up to two simultaneous breakdown dimensions (e.g., class_type × channel) and totals match the filtered dataset within 0.1% Then displayed metrics include: send volume, open rate, click rate, conversion rate, median and p95 conversion time, fill‑rate uplift, revenue per seat, no‑show delta, and channel effectiveness scores Then drilldown reveals the underlying class/session list with deep links to class detail and experiment detail
A/B Experiment Configuration & Statistical Validity
Given an experiment is created with variables among batch size, timer length, and message copy, with 2–4 variants and traffic splits summing to 100% When the experiment runs and reaches min sample size per variant of 300 sends and at least 50 total conversions, or sequential analysis reaches α=0.05 with power ≥0.8 Then the results show, for the selected primary metric (default revenue per seat), point estimates, 95% CI, p‑value, and a declared winner only if p≤0.05; otherwise marked Inconclusive Then guardrails are computed and surfaced: any variant with no‑show delta > +5 percentage points or fill‑rate uplift < 0 is flagged Do Not Deploy Then subject assignment is sticky per recipient per class instance; recipients do not switch variants mid‑class Then experiment can be paused, resumed, or stopped; stopping freezes data and winner status
CSV Export: Schema, Filters, and Integrity
Given the user clicks Export CSV in Analytics with filters applied When the export is generated Then the CSV contains only rows matching current filters and includes columns: class_id, class_type, class_start_time_local, time_of_day_bucket, capacity, cohort_size_bucket, send_id, recipient_id_hash, channel, experiment_id, variant, sent_at_utc, opened_at_utc, clicked_at_utc, purchased_at_utc, conversion_time_seconds, revenue_amount, revenue_currency, seat_final_status, was_last_call_conversion, fill_rate, baseline_fill_rate, fill_rate_uplift, no_show_indicator, attribution_rule, timezone Then timestamps are ISO8601 UTC; numeric fields are unformatted; delimiter is comma; header row present; line endings LF; max 200,000 rows per file with pagination for larger requests Then row count equals the analytics table count for the same filters and total revenue matches within 0.1%
Channel Effectiveness Reporting
Given Last‑Call sends occur via SMS, email, and doorline QR waitlist When viewing channel breakdowns in Analytics Then the system shows per‑channel metrics: sends, deliverability rate, open rate (if applicable), click rate (if applicable), conversion rate, revenue per send, revenue per seat, median conversion time, and no‑show rate for last‑call conversions Then channels are rank‑ordered by the selected primary metric with ties broken by conversion rate Then a class‑level detail view shows the winning channel per conversion with an audit trail referencing send_id, variant, and timestamps
Automated Feedback to Ranking Engine
Given at least 30 days or 1,000 sends (whichever comes first) of performance data exist for a class type and time‑of‑day bucket When the nightly model update job runs Then the ranking engine updates weights using features including channel, batch size, timer length, and message semantics embeddings to maximize predicted revenue per seat subject to guardrails Then deployment occurs only if backtest on the last 14 days predicts revenue per seat improvement ≥3%, no‑show delta ≤ +2 percentage points, and fill‑rate uplift ≥ 0 Then the system records a changelog entry with timestamp, features changed, expected impact, and provides rollback; rollback restores previous weights within 5 minutes Then post‑deploy monitoring runs for 48 hours and auto‑rollback triggers if observed revenue per seat drops >5% vs. benchmark

KeepTogether

Group‑aware waitlisting for families and friends. Attendees choose “keep together” or “okay to split”; the system holds and offers the right number of seats accordingly, or presents clear partial‑claim options when only some seats free up. Minimizes back‑and‑forth and ensures fair, stress‑free fills for multi‑seat bookings.

Requirements

KeepTogether Selection UX
"As a parent booking for my family, I want to choose to keep us together or allow splitting so that the system knows how to offer seats when they become available."
Description

Add a mobile-first, accessible UI on class booking and waitlist sign-up that captures party size and a clear choice between “Keep group together” or “Okay to split.” Display explanatory copy about how holds and split offers work, enforce class-specific maximum party sizes, and validate input. Persist the selection with the waitlist record, including optional attendee names and contact preferences. Ensure the flow works for guest checkout and white-labeled themes, and that captured data is passed to the allocation engine and checkout to drive seat holds and messaging.

Acceptance Criteria
Mobile-First Accessible Capture of Party Size and KeepTogether Choice
Given a mobile viewport ≤ 414px width When the booking or waitlist form loads Then the party size control and the KeepTogether radio options are visible without horizontal scrolling And the KeepTogether options are a single labeled radio group with choices "Keep group together" and "Okay to split" each with aria-describedby linking to its explanation text And all interactive targets for party size and KeepTogether options have minimum 44x44 px touch area And the controls are operable via keyboard (Tab/Shift+Tab/Arrow keys) with visible focus and correct reading order
Enforce Class-Specific Maximum Party Size
Given class maxPartySize = N When a user inputs a party size > N Then submission is blocked and an inline error "Maximum party size for this class is N" is shown and announced via aria-live And when the user increments party size beyond N via UI controls Then the value is capped at N And when N = 1 Then the KeepTogether choice is hidden or disabled and defaulted to "Keep group together"
Explanatory Copy for Holds and Split Offers
Given the KeepTogether options are rendered When a user focuses either option or taps "What does this mean?" Then explanatory text is displayed within the form And the text for "Keep group together" states that seats are held only when N seats are available and no partial offers will be sent And the text for "Okay to split" states that partial offers may be sent for fewer than N seats with clear partial-claim instructions And the text displays the class's configured hold window duration (e.g., 10 minutes) dynamically
Input Validation and Accessible Error Messaging
Given no selection is made for KeepTogether When the user attempts to submit Then submission is blocked and an inline error "Select how to place your group" is displayed and announced via aria-live And when party size is empty zero negative or non-integer Then submission is blocked with inline error "Enter a whole number of at least 1" and the field is marked invalid And when the primary contact provides neither a valid email nor phone Then submission is blocked with inline error "Provide at least one contact method for offers" And when all validation errors are corrected Then errors clear immediately and the form submits successfully
Persist Waitlist Record with Preferences and Optional Details
Given a valid submission with partySize = N keepTogetherChoice = X attendeeNames = [...] and contactPreferences When the waitlist record is created Then the stored record includes these fields with exact values And when the record is retrieved via API and staff UI Then the values match the submission and attendeeNames count ≤ N And when the user returns via a magic link or reminder Then the form prepopulates partySize N and keepTogether choice X from the stored record
Pass Selection to Allocation Engine and Checkout Messaging
Given a waitlist record with partySize = N and splitAllowed = true When k (1 ≤ k < N) seats free up Then the allocation engine receives payload including partySize N and splitAllowed true and generates a partial offer for k seats And given splitAllowed = false When k (1 ≤ k < N) seats free up Then no offer is sent and no holds are created until N seats are available And when an offer is sent to checkout Then the checkout header displays a banner reflecting the selection: "Keeping your group together" or "Okay to split" with N seats requested
Guest Checkout and White-Labeled Theme Compatibility
Given guest checkout is enabled When a user proceeds without an account Then the form completes end-to-end capturing required contact and KeepTogether data and creates a waitlist record And when the site theme is switched among supported white-label themes Then the form inherits theme colors and fonts without layout breakage and maintains color contrast ≥ 4.5:1 for text and interactive elements And across mobile (≤ 414px) tablet (~768px) and desktop (≥ 1024px) viewports Then the form renders responsively with no horizontal scroll and consistent theming
Group-aware Allocation Engine
"As a waitlisted attendee, I want the system to hold enough seats for my group when it’s our turn so that we can book without losing spots to others."
Description

Implement a server-side allocation engine that continuously evaluates seat availability and waitlist entries to create time-bound holds that honor group size and split preferences. For “Keep together,” only place holds when enough seats are available; for “Okay to split,” generate partial offers as seats open. Apply fairness rules (timestamp-based with tie-breakers), prevent large groups from blocking the queue indefinitely via configurable thresholds, and handle concurrent events, cancellations, and capacity changes without double-booking. Expose deterministic decision logs and operate idempotently across retries.

Acceptance Criteria
Time‑Bound Holds for Keep‑Together Groups
Given a waitlist entry marked keep_together with group_size = N and configured hold_ttl = T minutes, When seat availability for the class reaches >= N at evaluation time, Then the engine creates a single hold for N seats for that entry, expiring T minutes from creation, and marks those seats unavailable to others. Given a keep_together entry and availability < N at evaluation time, When the engine runs, Then no hold is created and the entry’s queue position is unchanged and logged with reason "insufficient_seats". Given a hold exists and expires unclaimed at T, When the next evaluation cycle runs, Then the seats return to the pool, the entry remains on the waitlist with original timestamp, and a decision log records "hold_expired" with hold_id and timestamps.
Partial Offers for Okay‑to‑Split Groups
Given a waitlist entry marked okay_to_split with group_size = N and min_acceptable = 1, When k seats (1 <= k < N) are available, Then the engine creates a partial offer for k seats and leaves the remaining N−k seats queued on the same entry. Given a partial offer for k seats is accepted for m (1 <= m <= k), When the engine confirms, Then m seats are booked, the entry’s remaining requested size becomes N−m, and its original timestamp is preserved for subsequent allocations. Given multiple okay_to_split entries are eligible, When allocating limited seats, Then allocations follow fairness order (timestamp, then deterministic tie‑breaker) and no overlapping holds/offers are created for the same seat.
Fairness Ordering and Deterministic Tie‑Breakers
Given multiple waitlist entries for the same class, When evaluating allocation, Then entries are ordered by ascending join_timestamp; ties are resolved by a deterministic secondary key and the final ordered list is written to the decision log. Given two entries have identical timestamps, When the allocation cycle is retried or replayed with the same inputs, Then the same entry wins the tie and the same actions (holds/offers/skips) occur. Given a later entry is allocated ahead of an earlier one due to a prior skip or anti‑blocking rule, When the earlier entry becomes eligible again, Then it reclaims priority in the next cycle per fairness ordering.
Anti‑Blocking Thresholds for Large Groups
Given a keep_together entry cannot be satisfied for M consecutive evaluation cycles (configured max_blocking_cycles = M), When the Mth unsuccessful evaluation completes, Then the engine marks the entry as temporarily skippable and proceeds to allocate to later entries it can satisfy, recording reason "anti_blocking_skip" with cycle count. Given an entry is temporarily skippable due to anti‑blocking, When enough seats later become available (>= group_size) or the configured skip_cooldown C elapses, Then the entry becomes eligible again and is prioritized by its original timestamp. Given anti‑blocking is active, When allocating seats, Then no eligible smaller group is prevented from receiving seats solely because a larger earlier group lacks sufficient seats.
Concurrency Safety and No Double‑Booking
Given simultaneous cancellations and new bookings occur, When an allocation cycle runs, Then seat assignments use transactional guards so that no seat is assigned to more than one hold/booking. Given two allocation cycles start in parallel for the same event, When both attempt to create holds, Then at most one succeeds and the other observes existing state and creates no duplicate holds. Given venue capacity decreases below the number of active holds, When reconciling, Then the engine cancels the most recent holds first (deterministic policy), updates affected entries with status "hold_canceled_capacity_change", and ensures total allocated + held seats <= new capacity.
Idempotent Execution and Retry Behavior
Given an allocation cycle is invoked with idempotency_key K, When the request is retried, Then the engine returns the same decision set and does not create additional holds or offers; all side effects are deduplicated on K for at least the configured retention window. Given a hold already exists for an entry due to a prior attempt, When a retried request attempts to create it again, Then the engine returns the existing hold reference and leaves its expiration unchanged. Given a batch of actions partially succeeds, When a retry occurs, Then only missing actions are performed and original ordering and identifiers (hold_id/offer_id) are preserved.
Deterministic Decision Logs and Auditability
Given any allocation cycle completes, Then a decision log is persisted containing cycle_id, event_id, input snapshot (capacity, waitlist entries with anonymized IDs and flags), ordered candidates, actions taken, reasons, rule versions, and timestamps. Given decision logs are queried by cycle_id, When the engine replays with the same inputs, Then the resulting decisions match the logged actions and the log records replay_match = true. Given compliance requirements, Then logs exclude PII (e.g., names, phone numbers), expose only internal IDs/hashes, and are accessible only with authorization scope "allocation.read".
Partial-Claim Offers & Remainder Handling
"As a group organizer who allowed splitting, I want to accept some of the seats now and keep the rest waiting so that at least part of my group can attend without losing our place."
Description

When insufficient seats are available for a full group that opted to split, generate clear partial-claim offers that specify how many seats can be taken now and how many remain on the waitlist. Provide a simple selection step to assign available seats to specific attendees, keep the remainder in queue with preserved priority, and allow switching preferences (e.g., from keep-together to split) where permitted. Manage timers for held subsets, release unclaimed seats cleanly, and ensure pricing, discounts, and class policies reflect the number of seats actually claimed.

Acceptance Criteria
Partial-Claim Offer Generation for Split-Eligible Waitlisted Group
Given a waitlisted booking for N seats with preference "okay to split" and the class has M seats free where 0 < M < N and the class policy permits partial claims When the availability is detected Then the system generates a partial-claim offer for M seats and states that N-M seats remain on the waitlist with preserved priority And the offer displays an absolute expiration timestamp and a countdown equal to the class’s partial-claim hold duration And the held seats are reserved from general inventory for the duration of the hold And the offer is not generated if M is below the policy-defined minimum partial-claim threshold And all events are logged with booking ID, counts, timestamps, and actor/system source
Seat Assignment Flow for Partial-Claim Acceptance
Given the recipient opens the partial-claim offer for M seats associated with a named attendee list of size N When they select up to M attendees to assign seats and confirm Then the system validates no more than M are selected and required attendee details are complete per class policy And upon confirmation the roster is updated with the selected attendees and M seats are marked booked And payment is initiated only for the selected seats and a success screen confirms booking details And primary actions are operable via keyboard/screen reader and are mobile-friendly And if the user attempts to assign fewer than 1 seat the confirmation is disabled
Remainder Waitlist Preservation After Partial Claim
Given a partial claim of K seats from an original N-seat waitlist booking with preference state recorded When K seats are confirmed Then the remainder R = N-K stays on the waitlist with the original priority timestamp unchanged And the remainder’s preference state persists unless explicitly changed by the user And the queue position relative to other entries with the same timestamp remains unchanged And the booking record shows split history including K claimed, R remaining, and current preference
Preference Switch Between Keep-Together and Split
Given a waitlisted booking with preference "keep together" and class policy allows switching to "okay to split" When the user toggles preference Then the system updates the preference immediately, preserves the original priority timestamp, and records an audit entry And if seats currently available satisfy a partial claim under the new preference, a partial-claim offer is generated per offer rules And switching preference is blocked while a payment is in progress and informs the user to complete or cancel And users can switch back to "keep together" if no partial claim has been accepted; after acceptance, only the remainder’s preference may be edited per policy
Hold Timer Management and Clean Seat Release for Subset Offers
Given a partial-claim offer is created for M seats with a hold duration H minutes When the hold starts Then the seats are reserved from general availability for exactly H minutes, synchronized to server time And if the user accepts and books L seats (0 < L <= M) before expiry, only M-L unclaimed seats are released immediately to the next eligible waitlist entry upon confirmation And if the hold expires without acceptance, all M seats are released and the offer is voided; offer links become inactive And if the user explicitly declines, all M seats are released immediately and the next eligible entry is notified And concurrent offers for the same seats are prevented; attempts are rejected with a consistent error and no double-booking occurs
Pricing, Discounts, and Policy Application on Partial Claims
Given the class has pricing rules, taxes, fees, and discount logic that may be threshold-based on seat count When a partial claim of K seats is accepted Then the total cost is calculated for exactly K seats with applicable taxes and fees And any discounts are applied based on K seats only; if thresholds are not met, discounted rates are removed with a clear price breakdown before payment And coupon codes are validated and applied once per transaction per policy And the receipt and instructor payout reflect K seats; the remainder on the waitlist has no financial hold or charge And refunds or adjustments are not triggered for unclaimed seats
Clear Multi-Channel Notifications for Partial-Claim Offers
Given a user has opted into SMS and/or email When a partial-claim offer is generated Then the notification includes class name, date/time, location or virtual link, M seats available now, N-M seats remaining on waitlist, and the expiration time in the user’s timezone And the notification contains a unique secure link to the offer and an accessible summary And duplicate notifications are de-duplicated within a defined window; a resend is available on request And upon expiry or decline, a follow-up notification confirms release and next steps
Hold-and-Offer Notifications with Deep Links
"As a waitlisted customer, I want a clear message with a one-tap link to claim my held seats so that I can check out quickly before the hold expires."
Description

Send branded SMS and email notifications when holds or split offers are created, including a secure deep link to claim seats, a visible countdown to hold expiry, and clear next steps. Provide follow-up reminders before expiration, respect user notification preferences and regional compliance, and degrade gracefully to alternate channels on delivery failure. Template content per white-label configuration and localize where available. Record delivery, open, and click events for audit and performance tracking.

Acceptance Criteria
Hold Created — KeepTogether Group Notification
Given a multi-seat booking request with “keep together” triggers a hold for N seats When the hold is created Then send notifications per each attendee’s channel preferences within 5 seconds And include white‑label branding (sender name, logo, colors) And include a secure deep link to claim all N seats And include clear next steps and the hold expiry timestamp And display a countdown timer reflecting time remaining within ±2 seconds And do not notify contacts who opted out of the channel And record a delivery attempt event per channel
Secure Deep Link to Claim Seats
Given a hold or split offer exists When a notification is generated Then embed a single‑use, signed deep link scoped to the hold/offer ID and permitted seat count And expire the link exactly at hold expiry or upon successful claim, whichever comes first And block unauthorized or over‑quota claims with an informative error state And show an expired state with option to rejoin the waitlist when invalid/expired And track clicks without exposing PII in query parameters And treat repeated clicks idempotently to prevent double‑claim
Split Offer Notification with Partial‑Claim Options
Given a request marked “okay to split” and only M (< N) seats become available When a split offer is created Then notify the requester stating clearly that M of N seats are available And present options to claim up to M seats and waitlist the remainder And include a deep link parameterized with max claimable seats = M And use configured templates with localized copy where available And record delivery, open, and click events tied to the offer
Pre‑Expiry Reminder with Visible Countdown
Given an active hold/offer with time remaining greater than the reminder threshold When time remaining equals the configured reminder offset (e.g., 10 minutes) Then send a reminder via the highest‑priority allowed channel per user preferences And suppress the reminder if the hold/offer is claimed or canceled before the offset And display remaining time with countdown accuracy within ±2 seconds And render times in the user’s or class location time zone per configuration
Preferences and Regional Compliance
Given a user’s notification preferences and region are known When preparing any hold/offer notification or reminder Then honor per‑channel opt‑in/opt‑out, quiet hours, and preferred channel order And include legally required elements per region (e.g., business name, SMS opt‑out text) And use localized templates if available, else fall back to English And throttle to configured rate limits where required by policy/regulation And log consent snapshot and compliance checks to the audit trail
Channel Fallback on Delivery Failure
Given a notification is attempted via the primary channel When the provider returns a definitive failure or no delivery confirmation within T seconds (configurable) Then attempt delivery via the next allowed channel without duplicating content And ensure at most one successful notification is received per event using an idempotency key And record failure reason, retry attempts, chosen fallback, and final status And raise an internal alert after configurable consecutive failures for the same recipient
Event Tracking and Auditability
Given notifications are sent for holds and split offers When delivery, open, and click webhooks are received Then record normalized events with timestamp, channel, template ID, user ID, and hold/offer ID And attribute clicks to subsequent claim outcomes where applicable And de‑duplicate repeated webhook events idempotently And surface events in the audit log and analytics within 60 seconds of receipt And retain events for at least 13 months And omit tracking pixels/links where prohibited or when the user opted out of analytics
Reserved Cart & Checkout Integration
"As the organizer for my group, I want to pay for our held seats in a single checkout so that I can confirm all spots before the timer runs out."
Description

Integrate seat holds into checkout so that reserved seats appear in a locked cart with an expiry timer synchronized to the allocation engine. Support paying for all seats in one transaction or, when enabled, inviting party members to pay their own shares without releasing the hold. Enforce capacity constraints, taxes, and discounts per seat, ensure idempotent payments, and auto-cancel holds on payment failure or expiry. Update class rosters in real time and trigger standard confirmations and reminders upon successful payment.

Acceptance Criteria
Locked Cart With Synchronized Hold Timer
Given the allocation engine has reserved N seats for a user with an expiry timestamp T When the user opens the cart Then the cart displays N reserved seats in a locked state with a visible countdown timer And the countdown expiry matches T within ±1 second And increasing quantity above N is disabled; decreasing quantity immediately releases the removed seats back to availability And if the timer reaches 00:00 before successful payment, the cart auto-removes the reserved seats, shows a “Hold expired” message, and the allocation engine releases the seats within 1 second
Pay Together Checkout With Capacity Enforcement
Given a user has N reserved seats and selects Pay Together When they submit valid payment details Then a single transaction is authorized/captured for the total of N seats including configured per-seat taxes, fees, and discounts And the checkout prevents increasing seat count beyond N, even if capacity changes concurrently; attempts to do so show a clear error and retain N And upon success, capacity is decremented by N atomically with the order creation to prevent overbooking And the cart is cleared and the hold is closed
Split Payment Invites Without Releasing Hold
Given split payments are enabled and a user with N reserved seats chooses to invite others to pay When the organizer assigns seat shares to invitees whose allocations sum to N and sends invites Then the system maintains the hold for all N seats until either all N seats are paid or the hold expires And as each invitee completes payment, their seat(s) are marked paid and removed from the remaining balance without releasing the unpaid seats And the organizer can pay any remaining unpaid seats in one transaction before expiry And unpaid seats under the active hold are not shown as available to the public
Per-Seat Taxes, Fees, and Discounts Applied Correctly
Given a cart with multiple seats subject to configured tax rates, fees, and discount rules (per-seat and order-level) When totals are calculated Then each seat line shows base price, itemized taxes/fees, applied discounts, and a seat subtotal And the order total equals the sum of seat subtotals, rounded to 2 decimal places using standard rounding And per-seat coupons apply once per seat; order-level discounts apply once per order; exclusions are enforced And taxes are applied after discounts when configured to do so And the same computed amounts are used for authorization/capture and appear on receipts
Idempotent Payment Processing
Given a checkout session with idempotency key K and total amount A When the payer retries payment due to refresh, duplicate submit, or network timeouts Then at most one successful charge is created for key K and amount A And subsequent identical requests return the original result without creating additional charges or orders And only one order/booking record exists; duplicates are not created even if gateway webhooks are delivered multiple times And audit logs record use of K and idempotent responses
Auto-Cancel Holds On Failure Or Expiry
Given active reserved seats in a cart When a Pay Together payment attempt fails (authorization or capture) Then all held seats are released immediately, the cart displays a clear failure message, and the allocation engine returns the seats to availability within 1 second And no roster records are created for the failed transaction When in Split Pay mode and the hold timer expires with unpaid seats remaining Then only the unpaid seats are released; paid seats remain booked; organizer and relevant invitees are notified And released seats are reintroduced to the waitlist/allocation flow immediately
Real-Time Roster Updates and Notifications On Success
Given successful payment confirmation for one or more seats (synchronous or via webhook) When the platform processes the success event Then the class roster is updated within 2 seconds with one booking per paid seat and correct attendee assignment (payer or invitee) And class capacity is decremented accordingly and the waitlist pipeline is triggered if applicable And standard confirmation emails/SMS are sent within 60 seconds and reminders are scheduled per class policy And the cart removes the paid seats; any remaining unpaid seats in Split Pay continue to show with their original timer
Instructor Controls & Overrides
"As an instructor, I want to configure and, when necessary, override group-aware waitlist behavior so that fills remain fair and aligned with my class policies."
Description

Provide admin settings at account and class levels to enable/disable KeepTogether, set default preference (keep vs split), define maximum party size, hold duration, fairness thresholds, and tie-breaker rules. Offer an instructor view of the queue that shows group size, preference, and timestamps, plus tools to manually allocate, merge, or split groups in edge cases. Include safeguards to prevent unintended over-allocation and record all overrides in an audit trail.

Acceptance Criteria
Account-Level KeepTogether Defaults
Given I am an account owner on Settings > Waitlist & Grouping When I enable KeepTogether, choose a default preference (Keep Together or Okay to Split), set Max Party Size (1–20), Hold Duration (5–120 minutes), Fairness Threshold (0–100%), and a Tie-Breaker Rule (Earliest Timestamp, Best Fit, or Random-within-threshold), and click Save Then the settings persist, are applied to all newly created classes by default, and an audit entry is recorded capturing before/after values and actor And when I reopen Settings, the saved values are displayed exactly as saved And when I create a new class, the class form is prefilled with these defaults
Class-Level Overrides Per Class
Given I am editing an existing class’s settings When I toggle KeepTogether on/off and override any default values (Default Preference, Max Party Size 1–20, Hold Duration 5–120 minutes, Fairness Threshold 0–100%, Tie-Breaker from allowed list) and click Save Then only that class uses the overrides; other classes remain unchanged And invalid inputs (non-numeric, out of bounds, empty required fields) are blocked with inline errors and Save disabled And the changes take effect for future allocations and holds; existing active holds continue until they expire or are manually cancelled And an audit entry is recorded with actor, class, fields changed, and reason if an override bypasses validation warnings
Instructor Queue View With Group Context
Given a class has a waitlist containing multiple groups with sizes, preferences, and timestamps When I open the Instructor Queue View for that class Then I see columns: Position, Group Size, Preference (Keep/Split), Joined At, Last Updated, and Eligibility Notes And the queue is ordered according to the class’s Tie-Breaker and Fairness Threshold settings And I can filter by Preference and Group Size range and search by attendee name/email And timestamps display in my local timezone with the timezone abbreviation shown And the view refreshes within 2 seconds after any allocation or hold change
Manual Allocation, Split, and Merge Tools
Given there are available seats and a populated waitlist When I select a group and choose Allocate Seats Then the confirmation dialog shows seats to be allocated, seats remaining after allocation, and any rule impacts And allocation cannot exceed class capacity; attempts are blocked with a clear error And for groups marked Keep Together, Allocate is only enabled if enough seats exist or if I explicitly choose Override and provide a reason; the override is recorded And for groups marked Okay to Split, I can choose Split Group and select a subset size up to available seats; the remainder stays in queue with updated position and timestamp rules applied And I can merge two or more Okay to Split groups into a single allocation if total seats fit capacity and do not exceed Max Party Size; otherwise the action is blocked or requires an override with reason
Over-Allocation Safeguards and Concurrency Control
Given another admin may be editing allocations simultaneously When I click Confirm on any allocation, split, or merge Then the system performs a final atomic capacity check that includes current inventory, active holds, and pending commits And if the operation would exceed capacity, the commit is rejected, no partial changes are saved, and I see a concurrency error with a prompt to refresh And the queue view refreshes to reflect the latest state within 2 seconds And a failed attempt is logged in the audit trail with outcome=failed and reason=capacity_conflict
Hold Duration Behavior for Instructor-Initiated Offers
Given Hold Duration is configured for the class When I allocate seats that generate a claim offer Then the hold timer starts immediately and expires exactly after the configured number of minutes unless the claim is completed or the hold is cancelled And upon expiry, seats are automatically returned to inventory, the queue reorders per rules, and the expired hold is marked as such in the queue And I may set a one-time custom hold duration (1–180 minutes) during allocation; invalid values are rejected and do not save And each active hold visibly displays time remaining with countdown accuracy within ±5 seconds
Audit Trail for Settings Changes and Overrides
Given any settings change, manual allocation, split, merge, or rule override occurs When the action is saved (success or failure) Then an immutable audit entry is created capturing: actor ID and role, timestamp (UTC), class ID, action type, entities affected (waitlist entry IDs), before and after values, and a required free-text reason if an override was used And audit entries are viewable by admins in a read-only log with filters for class, actor, action type, and date range, and exportable to CSV And audit entries cannot be edited or deleted; attempts are blocked and logged
Analytics, Insights, and Audit Logging
"As a studio owner, I want insights into how KeepTogether affects conversions and attendance so that I can tune settings and demonstrate ROI."
Description

Capture and surface metrics such as waitlist conversion rate by group size and preference, average hold duration, expired holds, partial-claim utilization, and impact on no-shows. Provide exportable reports and basic dashboards, and store detailed event logs (allocation decisions, notifications, claims, expiries) to support dispute resolution and continuous improvement. Expose configuration-level comparisons to help tune defaults (e.g., hold window, maximum party size).

Acceptance Criteria
Dashboard: Conversion by Group Size & Preference
Given a date range and class filter When I load the Analytics dashboard Then I see conversion rates segmented by party size and preference (Keep Together vs OK to Split) Given the displayed conversion rate When I open detail for a segment Then it is computed as claimed parties divided by offered parties in that segment and shows numerator, denominator, and percentage with one decimal Given I change filters (date range, location, class type) When results refresh Then metrics update within 2 seconds for datasets under 50k offers or show a loading state and complete within 10 seconds for larger datasets Given a segment has no data When the dashboard renders Then the cell shows 0 percent with n=0 and no error state
Export: Analytics CSV by Filters
Given a selected date range and filters for location and class type When I click Export Then a CSV is generated with columns: date, class_id, class_name, party_size, preference, offers_sent, claims, conversion_rate, avg_hold_minutes, holds_expired, partial_claims, partial_claim_utilization, no_show_rate Given a dataset up to 200k rows When the export runs Then it completes within 60 seconds and a downloadable link is provided and retained for 7 days Given filters result in zero rows When I export Then the CSV contains headers only and the export succeeds Given timestamps in the file When I open the CSV Then all timestamps are in ISO 8601 UTC and include an account_timezone column Given privacy requirements When exporting Then the file excludes message bodies and raw PII and includes only hashed user_id and anonymized party_id
Audit Log: Allocation & Hold Lifecycle
Given a waitlisted party receives an offer When the offer is created Then an audit event is written with fields: event_id, timestamp, class_id, party_id, group_size, preference, offer_type (full or partial), seats_offered, hold_expires_at, decision_basis, actor (system or staff), correlation_id Given a claim, partial claim, expiry, or cancellation occurs When the state changes Then a corresponding event is recorded with event_type, seats_claimed, payment_reference if applicable, channel (SMS, email, link), and previous_event_id linking the offer Given a notification is sent When the send occurs Then an event logs channel, template_id, send_status (queued, sent, delivered, failed), and provider_message_id Given I query audit logs by class_id or party_id When I request the log Then results are ordered by timestamp ascending and include all events with the same correlation_id Given system clock skew could affect ordering When events are written Then timestamps include milliseconds and a monotonic sequence number per correlation_id to preserve causal order
Metrics: Average Hold Duration & Expired Holds
Given a set of offers in the selected period When average hold duration is displayed Then it is computed as mean of hold_expires_at minus offer_created_at in minutes across offers that were claimed or expired and shown with one decimal Given expired holds are reported When I compare to audit events Then the count equals the number of offer cycles where no claim occurred before hold_expires_at and an expire event exists Given offers with extensions or reoffers When computing metrics Then duration uses the final effective hold window per offer cycle and expired hold count treats multiple extensions as one cycle Given I drill into this metric When I open the detail list Then I can view up to 1k offers with offer_created_at, hold_expires_at, claimed_flag, and duration and the list loads within 3 seconds
Metric: Partial-Claim Utilization
Given parties with preference OK to Split received partial offers When utilization is displayed Then partial-claim utilization equals total seats claimed from partial offers divided by total seats offered in those partial offers and is shown as a percentage with one decimal Given parties originally Keep Together were manually overridden to accept partial When segmenting Then these are shown in an override segment distinct from native OK to Split Given a class or filter set has no partial offers When the metric is displayed Then the value shows N/A with a tooltip explaining insufficient data Given I click the metric When drill-down opens Then a table lists parties, seats offered partial, seats claimed, time to claim, and channel with pagination and export to CSV
Insights: No-Show Impact by Waitlist & Preference
Given attendance data is available for the selected period When I view no-show impact Then the dashboard shows no-show rates for seats filled via waitlist segmented by Keep Together and OK to Split versus non-waitlist baseline and each rate includes a 95 percent confidence interval Given the calculation definition When values are computed Then no-show rate equals no_shows divided by seats_attended_expected and aligns with attendance records within 0.5 percentage points on a random sample of 100 bookings Given a segment has fewer than 30 seats When metrics render Then the segment is labeled low sample and confidence intervals are widened and significance badges are suppressed Given I export analytics When I include no-show metrics Then the CSV contains per class and segment no-show counts, denominators, rates, and intervals
Configuration Comparison: Hold Window & Max Party Size
Given two configuration variants or time periods are selected When I open the comparison view Then I see side-by-side metrics for conversion, expired holds, partial-claim utilization, and no-show rates per variant with filter parity enforced Given significance is requested When comparing conversion and no-show rates Then a two-proportion z-test at alpha 0.05 is computed and segments with significant differences are marked significant Given I change the compared variants or date ranges When the view refreshes Then the comparison updates within 3 seconds on datasets under 100k offers and an insight_view event is written to the audit log recording parameters Given insufficient data for a variant When the comparison loads Then significance indicators are disabled and a guidance message suggests expanding the date range or scope

ChannelSmart

Delivers claim invites via the channel each user is most likely to see—SMS, email, push, or WhatsApp—with automatic fallbacks and localization. Respects quiet hours and urgency windows, optimizing send timing and content to maximize conversions. Fewer missed messages, more seats claimed on the first try.

Requirements

Predictive Channel Selection Engine
"As a studio owner, I want invite messages sent via the channel each customer is most likely to see so that more seats are claimed on the first attempt."
Description

Determine the highest-likelihood delivery channel (SMS, email, push, WhatsApp) per recipient for claim invites using engagement history, contact availability, device tokens, deliverability signals, and recent interaction context. Scores channels in real time when a seat becomes available, honors per-channel consent, and chooses the best eligible option. Integrates with ClassTap’s user profiles, waitlist events, and booking flows, embedding a signed deep-link to the claim page. Provides idempotency to avoid duplicate notifications if a claim is completed, and logs decision reasons for auditability. Expected outcome: higher first-attempt claim conversions and fewer missed invites.

Acceptance Criteria
Real-Time Channel Scoring with Consent Enforcement
Given a seat release event for a waitlisted user with multiple contact endpoints When the engine scores channels in real time Then it computes a probability score per eligible channel using engagement history, availability, device tokens, deliverability signals, and recent interaction context within 300 ms p95 And channels without explicit consent or with an unsubscribe state are excluded from eligibility And the selected channel is the highest-scoring eligible channel; on score tie, a deterministic tiebreaker (push > SMS > WhatsApp > email) is applied and recorded And if no channels are eligible, no send occurs and a "no-eligible-channel" decision is logged
Automatic Fallback Cascade with Quiet Hours and Urgency Windows
Given the selected channel send fails with a hard failure signal When a fallback is permitted Then the next-best eligible channel is sent within 60 seconds once And total sends per invite do not exceed 2 across all channels Given the initial send has no engagement (e.g., no open/click/delivery confirm) within 10 minutes during an active urgency window When fallback is configured Then send via the next-best eligible channel and log the fallback reason Given the recipient is within configured quiet hours When the invite is non-urgent Then the send is deferred until quiet hours end and the deferral reason is logged And if urgency=true, the send proceeds with content marked as urgent while respecting channel policy
Idempotency and Claim Completion Suppression
Given multiple triggers occur for the same seatId, userId, and releaseId within the idempotency window When the engine processes the event Then only one notification is sent using an idempotency key of seatId+userId+releaseId And retries with the same key return the original result and log "idempotent-replay" Given the user completes the claim via any channel When pending or scheduled notifications exist for that invite Then all are canceled within 5 seconds and no further sends occur Given the invite expires When evaluating pending actions Then no sends are issued and expiration is logged
Signed Deep-Link Generation and Validation
Given an invite is generated When composing message content Then include a deep link with a signed token containing userId, seatId, eventId, expiry, and nonce, signed with a rotating key, with default expiry ≤ 30 minutes And the link is URL-encoded per channel and includes tracking parameters When the user opens the link Then the ClassTap claim page loads with prefilled context and proceeds if the token is valid and unexpired And if the token is expired, tampered, or already used, the claim is blocked with the appropriate error and the reason is logged And upon first successful claim, the token is invalidated for single use
Decision Reason Logging and Auditability
Given a send decision is made (send, defer, fallback, or suppress) When persisting the outcome Then an audit record is stored with correlationId, timestamps, per-channel scores, eligibility/consent states, deliverability inputs, selected channel, tiebreakers, deferrals, fallbacks, and final outcome And audit records are retrievable via admin API filtered by userId or eventId within 2 seconds p95 And audit records are retained for at least 90 days with PII redacted except stable identifiers and hashed contact points And idempotent replays return the existing audit record without creating duplicates
Localization and Channel-Specific Content Compliance
Given a recipient profile with locale and timezone When composing invite content and scheduling Then the localized template matching the locale is used and send time respects the recipient's timezone and quiet hours And if a localized template is missing, the default English template is used and a "locale-fallback" is logged And channel-specific formatting and policy checks pass (e.g., SMS length/segmentation with short link, WhatsApp approved template, email subject/preview text) And opt-out/consent language is localized per channel policy
Multi‑Channel Fallback Orchestration
"As a waitlisted student, I want to receive a claim invite via an alternative channel if the first one fails so that I don’t miss time‑sensitive openings."
Description

Automatically retry claim invites across alternative channels when the primary attempt fails, times out, or is not engaged within a configured window. Supports provider failure detection (bounces, undelivered, throttling), soft vs. hard retry policies, escalating to the next eligible channel with configurable delays and maximum attempts. Ensures deduplication and idempotent claims: suppresses further sends once the seat is claimed or the invite expires. Captures full event telemetry (sent, delivered, opened/read where available, clicked, claimed) and updates recipient contact health in ClassTap.

Acceptance Criteria
Fallback to SMS After Email Bounce
Given an invite is sent via Email as the primary channel When a hard bounce or undelivered event is received from the email provider for that invite Then schedule an SMS invite to the same recipient after the configured fallbackDelay and before urgencyWindowEnd And do not attempt Email for this invite again And ensure total attempts across all channels do not exceed maxAttemptsAcrossChannels And record telemetry events: email.sent, email.bounced (with providerMessageId), sms.scheduled And ensure the SMS content is localized to recipient.locale and contains a unique claim link bound to the invite idempotency key
Escalation on Non-Engagement Within Window
Given an invite is sent on the primary channel at T0 When no engagement event (open/read where available or click) is recorded by T0 + engagementWindow Then send the invite on the next eligible channel from the channelPriority list after the configured escalationDelay And cancel all future scheduled sends immediately if a click or claim event is recorded at any time thereafter And ensure total attempts do not exceed maxAttemptsAcrossChannels And record telemetry events for each stage: <channel>.sent, <channel>.delivered (where available), <channel>.opened (where available), <channel>.clicked, invite.claimed
Idempotent Claim Suppression Across Channels
Given multiple invite messages for the same seat and recipient exist across channels When the recipient clicks any claim link and the claim is successfully created Then mark invite status = claimed and cancel all scheduled or in-flight sends across all channels within 1s And ensure any subsequent claim attempts within idempotencyWindow return an idempotent "already claimed" response with no additional reservations or payment charges And persist exactly one claim record with a stable idempotencyKey, SeatId, and RecipientId And record telemetry: invite.claimed (once), sends.canceled (count equals number of pending jobs)
Invite Expiry Halts Sends and Claims
Given an invite has an expirationAt timestamp When now >= expirationAt Then suppress all unsent messages and cancel any scheduled retries or escalations And ensure visiting or clicking any invite link after expiration shows an "Invite expired" state and does not create a claim And record a terminal telemetry event invite.expired (exactly once) and mark outcome = expired
Soft-Retry on Provider Throttling Before Escalation
Given a send attempt returns a provider throttling signal (e.g., HTTP 429 or rate_limit) When the failure is classified as soft Then retry on the same channel up to softRetryMax times using backoffStrategy (e.g., exponential) within retriableWindow And do not escalate to the next channel unless softRetryMax is exhausted or retriableWindow is exceeded And record telemetry for each retry with attemptNumber and backoffMillis And ensure quiet hours and urgencyWindowEnd are respected when scheduling retries
Quiet Hours and Urgency Window Scheduling
Given recipient.timezone, recipient.quietHours, and invite.urgencyWindowEnd are configured When scheduling any fallback, retry, or escalation send Then do not schedule within quietHours unless the channel is marked quietHoursAllowed = true And if the next permissible time is after urgencyWindowEnd, escalate immediately to an allowed channel or stop further sending per policy And compute all schedule times in recipient.timezone and log schedule decisions for audit
Telemetry Capture and Contact Health Updates
Given any send or engagement event occurs for an invite When the event is processed by the telemetry pipeline Then persist an event with fields: inviteId, recipientId, channel, eventType, providerMessageId (if any), attemptNumber, timestamp And update contact health: increment hardBounceCount per channel on bounces, mark channelStatus = unhealthy after hardBouncesThreshold, update last30dDeliveryRate and channelWeight And expose events and contact health via the analytics API and make channel health available to routing for future channel selection
Quiet Hours and Urgency Windows
"As a customer, I want messages to respect my quiet hours but still receive urgent last‑minute openings near class time so that I am not disturbed unnecessarily yet can act when it matters."
Description

Respect tenant- and user-level quiet hours by suppressing non-urgent sends during local nighttime and only delivering within allowable windows. Detect recipient timezone from profile, class location, or last known interaction, and adjust scheduling accordingly. Support urgency windows for last‑minute seat openings (e.g., within 3 hours of class) that permit respectful overrides for explicitly opted-in users. Provide admin controls for rules, exceptions, and blackout dates, and ensure compliance with regional do‑not‑disturb regulations. All scheduling constraints are enforced across all channels and fallbacks.

Acceptance Criteria
User-Level Quiet Hours Precedence
Given tenant quiet hours 22:00–07:00 in recipient’s local timezone and user quiet hours 20:00–08:00 are configured And the recipient timezone is America/New_York And a non-urgent seat-claim invite becomes ready at 21:30 local time When the system schedules delivery Then the message is scheduled for 08:00 America/New_York And no channel (SMS, email, push, WhatsApp) delivers before 08:00 And the suppression reason "user_quiet_hours" is recorded with the message ID And the delivery window validation passes immediately before send
Timezone Resolution and Scheduling Adjustment
Given the recipient profile has no timezone And the class location timezone is America/Los_Angeles And the last interaction timezone is UTC+02:00 When the system computes the recipient timezone Then America/Los_Angeles is selected with confidence "medium" And a send requested for 10:00 local is scheduled at 10:00 America/Los_Angeles (converted to UTC for delivery) And if class location timezone is unavailable, the last interaction timezone is used And if all sources are unavailable, the tenant default timezone is used and a "low" confidence flag is set in metadata
Urgency Window Override for Opted-In Users
Given the urgency window is defined as <= 3 hours before class start And a seat opens 2 hours before class at 22:00 local time And the user has explicitly opted in to urgent notifications And quiet hours are 21:00–07:00 local When the system triggers the invite Then the message is sent immediately despite quiet hours And the audit log notes "urgent_opt_in_override" with remaining minutes in the window And regulatory do-not-disturb checks are applied before send; if non-compliant, the send is blocked and the override is not applied
Non-Opted User During Urgency Window
Given the urgency window is defined as <= 3 hours before class start And a seat opens 2 hours before class at 22:00 local time And the user has not opted in to urgent notifications And quiet hours are 21:00–07:00 local When the system evaluates delivery Then the message is suppressed until 07:00 local And if the class starts before 07:00, the invite is not sent And a "suppressed_quiet_hours" reason is logged with the message record
Admin Controls: Quiet Hours, Exceptions, and Blackout Dates
Given an admin updates tenant quiet hours to 21:00–06:00 And adds an exception on 2025-12-31 allowing sends until 23:00 local And adds a blackout date on 2025-12-25 (no sends) When the admin saves changes Then changes propagate to the scheduling service within 5 minutes And all future scheduled jobs are re-evaluated against the new rules And an immutable audit record captures actor, timestamp, and before/after values And user-level overrides remain unchanged and continue to take precedence
Regulatory Do-Not-Disturb Compliance by Locale and Channel
Given the recipient locale is India and the channel is SMS And the planned send time falls within 22:00–07:00 India Standard Time (regulated DND period) When the system evaluates compliance Then the send is blocked regardless of urgency or opt-in And the message status is set to "blocked_regulatory_dnd" with a regulation reference code And any fallback attempts are evaluated; only compliant channels at compliant times are executed, otherwise they are deferred And the compliance block is visible in reports and message logs
Channel Fallbacks Respect Constraints and Prevent Duplicates
Given the preferred channel is Push but the device token is invalid And the fallback order is SMS -> Email And the current time 22:30 local is within quiet hours for non-urgent messages When a non-urgent invite is initiated Then no delivery occurs until quiet hours end And at 07:00 local the system retries Push; if still invalid, attempts SMS, then Email, each only if compliant with quiet hours and regulations And at most one successful delivery is recorded for the invite And the attempt timeline (channel, timestamp, outcome, reason) is captured in logs
Localization and Template Personalization
"As an international customer, I want invite messages in my language and in a format that fits the channel so that I can quickly understand and claim my spot."
Description

Localize invite content and transport‑specific templates for SMS, email, push, and WhatsApp using an i18n catalog with fallback languages. Support merge fields (class name, date/time in local format, instructor, studio, location/map link), conditional content per channel (length limits, media, WhatsApp approved templates), and right‑to‑left rendering where applicable. Validate templates at publish time, automatically shorten and safely truncate SMS, and embed signed tracking links compatible with each channel. Integrates with ClassTap branding to ensure white‑label consistency on all touchpoints.

Acceptance Criteria
Locale Selection and Fallback Rendering
Given a recipient locale and a studio default locale exist in the i18n catalog When rendering an invite for any supported channel Then the system selects strings and templates matching the recipient locale And if unavailable, falls back to the studio default locale; if still unavailable, falls back to en-US And date/time and numbers are formatted using the recipient’s locale and timezone And for RTL locales, output is marked and rendered right-to-left with correct punctuation and digit shaping
Merge Fields Expansion and Timezone Formatting
Given a template uses merge fields: class_name, start_datetime, instructor_name, studio_name, location_name, map_link When rendering the invite Then each merge field is populated from authoritative event/account data And start_datetime is converted to the recipient’s timezone and formatted per the selected locale And if any required field is unavailable, rendering fails with a clear error and publish is blocked
SMS Shortening and Safe Truncation
Given the channel is SMS and max_sms_parts is configured to 2 When the merged SMS exceeds the per-part limit after merge Then the system shortens all tracking links using the platform shortener And detects encoding (GSM-7 vs UCS-2) and enforces a max combined length of 306 (GSM-7) or 134 (UCS-2) characters And if still over limit, truncates at the nearest word boundary before the limit, preserves at least one tracking link, does not split grapheme clusters, and appends an ellipsis
WhatsApp Approved Template Compliance
Given the channel is WhatsApp and the recipient is WhatsApp-capable When preparing the message Then a pre-approved WhatsApp template ID is used with a matching language code And parameter counts, types, and media exactly match the approved template And if no approved locale variant exists, the studio default locale variant is used; if none, fallback to the next-best channel per policy and record the fallback reason
Channel-Conditional Content and Media Rules
Given a template contains channel-conditional blocks and media assets When rendering per channel (Email, SMS, Push, WhatsApp) Then only blocks allowed for the target channel are included and disallowed blocks are omitted And media conforms to configured per-channel limits (type, dimensions, size, character/payload caps); violations block publish with specific errors And SMS contains no media and respects channel-specific length constraints
Template Validation, Branding Integration, and Preview at Publish Time
Given a template is saved for Publish When validation runs Then unknown/unresolved merge fields, missing i18n keys, invalid conditional syntax, disallowed HTML/CSS, or channel rule violations are flagged as Errors and block publish And ClassTap white-label branding is applied (logo, colors, from/sender names, link domains) consistently across channels And previews are generated for at least two locales (including one RTL) and for each channel, showing branding, merge expansions, and layout
Signed and Channel-Compatible Tracking Links
Given any invite includes a CTA link When rendering per channel Then links use the studio’s white-label domain and include a signature and metadata (channel, locale, template_version, campaign_id) And on click/open, the backend validates the signature, records the event with correct attribution, and redirects to the destination And links conform to channel constraints: deep links for Push, UTM parameters for Email, WhatsApp-safe URLs, and shortened links for SMS
Send‑Time Optimization
"As a studio owner, I want the system to schedule invites when recipients are most likely to engage so that claim conversions improve without manual tuning."
Description

Optimize send time for each invite by modeling historical engagement patterns, recipient availability, and proximity to class start while honoring quiet hours and urgency rules. Choose the best time within an allowed window; fall back to immediate send for high‑urgency events. Provide A/B testing and holdout groups to measure lift, and guardrails to prevent bunching or provider rate‑limit breaches. Expose per‑tenant controls to enable/disable and set optimization windows, with transparent reasoning in logs and analytics.

Acceptance Criteria
Optimize Send Within Allowed Window Respecting Quiet Hours
Given tenant has send-time optimization enabled and quiet hours set for recipient local time And an invite is created at T0 with an allowed optimization window from Wstart to Wend And the model proposes an optimal send time Topt within [Wstart, Wend] When Topt falls inside quiet hours Then the system schedules the invite at the nearest permitted time within [Wstart, Wend] outside quiet hours And the scheduled time Ts satisfies Wstart <= Ts <= Wend And an audit log entry records reason=QUIET_HOUR_ADJUSTMENT with Topt and Ts Given the allowed window [Wstart, Wend] has no time outside quiet hours When scheduling is attempted Then the system schedules at the earliest permitted time after quiet hours that is before class start And if no permitted time exists before class start, the invite is not sent and is marked status=BLOCKED_QUIET_HOURS with reason code logged
Immediate Send Override for High-Urgency Invites
Given tenant policy urgency_window_minutes = U and allow_quiet_hour_override = true And an invite is created when time_to_class_start <= U When the invite is processed Then the system sends immediately, bypassing optimization and quiet hours And the audit log records reason=URGENCY_IMMEDIATE with time_to_class_start Given allow_quiet_hour_override = false and quiet hours are active And time_to_class_start <= U When the invite is processed Then the system sends at the first minute after quiet hours or at class_start - minimal_safety_buffer, whichever is earlier and permitted And the audit log records reason=URGENCY_DEFERRED_RESPECT_QUIET_HOURS
A/B Test and Holdout Allocation for Send-Time Optimization
Given tenant has experimentation enabled with allocation: variant=V%, control=C%, holdout=H% When N >= 1000 invites are created under identical target conditions Then observed assignment proportions are within ±2 percentage points of configured V/C/H Given an invite is assigned to variant When it is sent Then analytics record group=variant, proposed_time, scheduled_time, send_time, open_time, claim_time, and model_version Given an invite is assigned to control When it is sent Then it is sent at the control policy time (e.g., immediate) and analytics record group=control Given an invite is assigned to holdout When it is processed Then it is withheld or sent at a neutral policy time per configuration and analytics record group=holdout Given the experiment is toggled off When new invites are created Then all invites follow the default policy with no experiment group assigned
Guardrails Prevent Bunching and Rate-Limit Breaches
Given provider rate limit is configured at L sends per minute per tenant And M invites are scheduled to be sent within the same minute where M > L When the scheduler runs Then sends are staggered with jitter so that the instantaneous rate never exceeds L And no invite is delayed by more than max_jitter_minutes configured for the tenant And an audit log is written for any invite whose time was adjusted with reason=RATE_LIMIT_JITTER Given multiple tenants share the same provider with a global cap G per second When aggregate scheduled sends exceed G Then the system applies fair-share throttling across tenants without any tenant exceeding its configured cap Given a send would create a burst exceeding the max_batch_size_per_second guardrail When scheduling Then the batch is split into smaller chunks not exceeding the guardrail
Per-Tenant Controls: Enable/Disable and Window Configuration
Given a tenant admin disables send-time optimization When a new invite is created Then the invite bypasses optimization and follows the tenant's default send policy And analytics mark optimization_enabled=false Given a tenant admin sets an optimization window policy (e.g., earliest=Wmin relative to now or latest=Wmax relative to class start) When a new invite is created Then the proposed send time lies within the configured window and respects quiet hours Given a tenant updates optimization settings When changes are saved Then the settings are persisted with actor, timestamp, and change diff And existing scheduled invites out of compliance are re-evaluated within 5 minutes and re-scheduled to comply
Transparent Reasoning in Logs and Analytics
Given an invite is evaluated for send time When a decision is made Then the system logs model_version, features_summary, candidate_times, constraints_applied, proposed_time, final_time, and reason_codes And the log entry is queryable by invite_id within 5 minutes Given analytics are viewed for a tenant When the Send-Time Optimization report is opened Then it displays conversion lift, open/claim rate by group, average time-to-claim, and distribution of send times with filters for date range, class, and channel Given an invite is overridden by urgency or guardrails When it is sent Then the reason codes include URGENCY_IMMEDIATE or RATE_LIMIT_JITTER (as applicable) and appear in both logs and analytics
Timezone and DST-Aware Quiet Hours and Send Scheduling
Given a recipient has a known timezone TZ and quiet hours 22:00–08:00 local When an invite is scheduled for delivery Then the scheduled time is computed in TZ and does not fall within 22:00–08:00 in TZ Given a DST start transition occurs in TZ on the scheduling day When the proposed local time does not exist Then the system schedules at the next valid local time after the transition and logs reason=DST_FORWARD_ADJUSTMENT Given a DST end transition occurs in TZ on the scheduling day When the proposed local time is ambiguous Then the system schedules at the first occurrence of that local time and logs reason=DST_FALLBACK_DISAMBIGUATION Given recipient timezone is unknown When scheduling Then the system falls back to the class location timezone or tenant default per configuration and logs reason=TIMEZONE_FALLBACK
Consent, Compliance, and Opt‑Out Management
"As a recipient, I want clear opt‑in and opt‑out controls for each channel so that I can control how I’m contacted and trust the communications I receive."
Description

Centralize per‑channel consent state, capturing double opt‑in where required (e.g., WhatsApp), honoring STOP/HELP keywords for SMS, unsubscribe links for email, and system‑level DND lists. Store auditable records (timestamp, channel, source, IP/user agent), apply regional rules (e.g., GDPR/CCPA data minimization and purpose limitation), and suppress messaging for non‑consented channels in both primary and fallback flows. Provide self‑serve preference center links in messages and in the booking UI, and expose APIs/webhooks to sync consent with external CRMs.

Acceptance Criteria
Centralized Consent & Auditable Records (GDPR/CCPA)
- Given a user updates consent via booking UI, preference center, or API, When the change is submitted, Then the centralized per-channel consent state is updated within 1 second and used immediately for send eligibility decisions. - Given any consent event occurs, When the event is stored, Then an immutable audit record is saved with fields: user_id, channel, consent_action (opt-in|opt-out|update), source (UI|API|keyword|import), timestamp (UTC ISO-8601), IP, user_agent, locale, evidence (payload/message_id), and is queryable via API and admin UI within 5 seconds. - Given a GDPR/CCPA access request, When requested, Then the system provides a machine-readable export of the user’s consent history and stored identifiers within 24 hours. - Given a GDPR/CCPA deletion request, When requested, Then personal data related to consent is erased or anonymized within 24 hours while maintaining a non-contact suppression token to prevent future messaging. - Given data minimization rules, When storing consent, Then only required attributes are persisted and no message content beyond evidence IDs is retained.
WhatsApp Double Opt-In Acquisition
- Given a user adds a WhatsApp number, When no prior verified WhatsApp consent exists, Then the system sends a localized double opt-in prompt via WhatsApp and sets channel status to pending. - Given the user replies YES or taps the approved CTA within 14 days, When the confirmation is received, Then the WhatsApp consent state is set to active, the audit record includes the confirmation message_id and timestamp, and future WhatsApp messages are eligible. - Given no confirmation within 14 days or the user replies STOP, When the window expires or STOP is received, Then the WhatsApp consent remains or becomes opted-out and the channel is excluded from primary and fallback sends.
SMS STOP/HELP Keyword Compliance
- Given any SMS is sent, When the message is delivered, Then the body includes clear, localized STOP and HELP instructions. - Given a recipient sends STOP (or recognized variants), When the keyword is received, Then the phone number is opted-out for SMS within 5 seconds, a confirmation SMS is sent, an audit record is stored, and all future SMS (including fallbacks) are suppressed. - Given a recipient sends HELP, When the keyword is received, Then an SMS is sent within 10 seconds with support contact, carrier disclosure, and opt-out instructions, and the event is logged.
Email Unsubscribe Enforcement
- Given any marketing email is sent, When the email is constructed, Then it includes a one-click HTTPS unsubscribe link and RFC 8058 List-Unsubscribe and List-Unsubscribe-Post headers (mailto and HTTPS). - Given a recipient clicks the unsubscribe link, When the request is received, Then the user’s email marketing consent is set to opted-out within 10 seconds, a confirmation page is displayed, and an audit record is stored. - Given an email address is opted-out, When ChannelSmart selects channels, Then email is excluded from primary and fallback sends for marketing messages while transactional emails remain unaffected.
System-Level DND and Cross-Flow Suppression
- Given a user is on a system-level Do Not Disturb or suppression list, When ChannelSmart evaluates send eligibility, Then all channels for that user are blocked for non-essential messaging across primary and fallback flows, with the suppression reason logged. - Given a per-channel opt-out exists, When ChannelSmart evaluates fallback options, Then that channel is excluded from the candidate list and is not used as a fallback. - Given a suppressed send attempt occurs, When the decision is made, Then no message is dispatched, the attempt is logged with channel, trigger, and reason, and the action is visible in delivery analytics.
Preference Center Access and Update
- Given a message (SMS, email, WhatsApp) is sent, When the user opens it, Then a visible, functional link to the preference center is present and loads within 2 seconds from supported regions. - Given the user opens the booking UI, When they view their account or booking confirmation, Then a link to the preference center is displayed and accessible via tokenized, time-limited access (valid 15 minutes) without requiring account creation. - Given the user changes any channel preference, When they save, Then the centralized consent state is updated immediately, an audit record is created, and the UI shows a success confirmation within 2 seconds.
Consent Sync via APIs and Webhooks
- Given an external CRM calls the Consent API, When it sends an authenticated PUT to update a channel’s consent, Then the change is idempotent, validated, applied within 1 second, and the response returns the updated state with version. - Given a consent change occurs in ClassTap, When webhook subscriptions are configured, Then an outbound webhook with event type, user_id, channel, new state, timestamp, and signature is delivered within 60 seconds; failures are retried with exponential backoff for up to 24 hours. - Given inbound events (e.g., keyword opt-outs) are received via webhook or API, When the payload is signature-verified, Then the corresponding consent is updated and audit logged; invalid or unsigned payloads are rejected with 401/400 and no state changes occur.
Delivery Analytics and Conversion Attribution
"As a studio owner, I want to see which channels and timings drive successful claims so that I can tune settings and improve results over time."
Description

Provide dashboards and exports showing end‑to‑end performance by channel and strategy: send, delivery, open/read (where available), click/tap, and claim conversion, attributed to the last or best‑performing touch. Ingest provider webhooks and receipts for SMS, email, push, and WhatsApp; reconcile with internal events to compute latency and fallout per step. Surface insights such as top channels per segment, effective fallback paths, and optimization lift. Offer per‑invite and aggregate views, with filters by class, instructor, location, and time. Expose metrics via API for BI tools.

Acceptance Criteria
Channel Funnel Dashboard (Per-Channel, Filterable)
- Given invites and events exist across SMS, email, push, and WhatsApp and a date range is selected, When the Delivery Analytics dashboard loads, Then for each channel it displays counts and rates for Sent, Delivered, Open/Read (where available), Click/Tap, and Claimed conversions. - Given filters by class, instructor, location, strategy, and time granularity (hour/day/week) are available, When I apply any combination of filters, Then charts and totals update and reflect only the filtered dataset, and totals equal the sum of visible breakdowns. - Given latency metrics are enabled, When I inspect a funnel step, Then median, p90, and p99 latencies between adjacent steps are shown based on reconciled timestamps. - Given real-time processing, When I view the dashboard, Then a Last Updated timestamp is visible and corresponds to the latest processed event. - Given there are no matching events, When I view a filtered dashboard, Then an explicit No Data state is shown with zeroed metrics and no errors.
Attribution Model Toggle (Last Touch vs Best Performing Touch)
- Given an invite has multiple touches before a claim, When I select Last Touch attribution, Then the claim is attributed to the chronologically latest touch prior to the claim time for that invite. - Given the same invite and a defined user segment, When I select Best Performing Touch attribution, Then the claim is attributed to the touch in that invite’s path with the highest 30-day rolling claim conversion rate for that segment and channel; ties are broken by the latest touch. - Given I toggle the attribution model, When I compare aggregate totals, Then aggregate conversions equal the sum of per-invite attributions for the selected model and charts/exports update consistently. - Given an invite has no claim, When attribution metrics are computed, Then the invite is excluded from conversion counts but included in exposure and interim step counts.
Provider Webhook Ingestion and Event Reconciliation
- Given providers send delivery/open/click receipts, When webhooks arrive (including duplicates and out-of-order), Then events are processed idempotently, deduplicated by provider message ID, and ordered using provider event timestamps. - Given internal send logs per touch exist, When reconciliation runs, Then each external event links to the correct invite and touch; unmatched events are logged with a resolvable error and do not corrupt aggregates. - Given reconciled events, When latencies are computed, Then per-step latency equals (event_time - previous_step_time) and is stored in milliseconds for analytics and export. - Given a provider experiences delays or outages, When metrics rely on missing receipts, Then affected widgets display a warning indicator and exclude unverifiable steps from rates while still counting sends.
Exports and Metrics API for BI Tools
- Given filters by class, instructor, location, strategy, channel, and date range are applied, When I export CSV, Then the file contains one row per invite-touch with: invite_id, user_id_hashed, class_id, instructor_id, location_id, channel, strategy_id, send_time, delivered_time, open_time, click_time, claim_time, per-step latencies, current step status, attribution_model, and attributed_touch_flag. - Given I request aggregates via the Metrics API with OAuth2, pagination, and the same filters, When I compare results, Then API aggregates match dashboard totals for the same time window and attribution model. - Given the export result exceeds 1,000,000 rows, When I request export, Then a background job is created and returns a job_id that can be polled; upon completion, the download link returns a file whose row counts and aggregates match the API for the same filters.
Segment and Fallback Insights with Optimization Lift
- Given customer segments (e.g., lifecycle, locale) are configured, When I open Insights, Then top-performing channels per segment are ranked by claim conversion rate with sample sizes displayed. - Given fallback paths (e.g., Email → SMS → WhatsApp) are utilized, When I view path performance, Then I see conversion rate, median time-to-claim, and traffic share for each path, and paths can be filtered by segment. - Given a control group is configured, When I view optimization lift, Then lift is reported as (conversion_optimized − conversion_control) / conversion_control with 95% confidence intervals and a significance flag.
Per-Invite Drilldown and Audit Trail
- Given I open a specific invite, When I view the drilldown, Then I see a chronological timeline of touches including channel, locale, content variant, send/delivery/open/click timestamps, outcome, and provider error codes if any. - Given an attribution model is selected, When I view the drilldown, Then the attributed touch is highlighted and the rationale is displayed (Last Touch or Best Performing with reference window and rate source). - Given the invite did not result in a claim, When I view the drilldown, Then I see the last step reached and the inferred dropout point (e.g., delivery failed, no open, no click) if determinable.

Demand Pulse

A live, color-coded heatmap that predicts conversion likelihood by day and hour—filter by location, room, class type, and audience. Instantly spot hot hours to double down and cold zones to avoid. See expected fill-rate lift and confidence so you can move or launch classes with clarity and fewer experiments.

Requirements

Live Heatmap Visualization
"As a studio owner, I want a live heatmap of predicted demand by hour and day so that I can quickly spot high-opportunity times and avoid low-performing slots."
Description

A responsive, color-coded grid that visualizes predicted conversion likelihood by day and hour for selected entities. Each cell displays key metrics (predicted conversion %, expected fill-rate lift vs baseline, confidence) with tooltips for historical context (bookings, cancellations, waitlist, no-shows). Supports weekly and custom date ranges, timezone awareness, and mobile/desktop layouts. Provides fast interactions (initial render under 1.5s on typical org datasets; sub-300ms filter updates with client-side virtualization). Includes accessible colorblind-safe palette, legends, keyboard navigation, and aria labels. Integrates with ClassTap’s scheduling context so clicking a cell opens detail and actions relevant to that slot.

Acceptance Criteria
Weekly Heatmap Rendering and Performance
Given a typical org dataset and the default weekly range When the user opens Demand Pulse Then initial render completes within 1.5 seconds And a 7x24 heatmap grid is displayed with day/hour headers And each visible cell displays predicted conversion %, expected fill-rate lift vs baseline, and confidence And the color legend is visible and maps monotonically to predicted conversion %
Filter Updates and Virtualization Responsiveness
Given a rendered heatmap with any filters applied When the user changes one filter (location, room, class type, or audience) Then visible cells update within 300 ms at the 95th percentile And virtualization ensures no more than 300 cell DOM nodes exist at once And scroll performance remains at or above 60 FPS while scrolling the grid
Cell Metrics Tooltip With Historical Context
Given a rendered heatmap When the user hovers (desktop) or long-presses (mobile) any cell Then a tooltip appears within 100 ms and remains anchored to the cell And the tooltip shows historical bookings, cancellations, waitlist count, and no-show rate for the selected date range And the tooltip includes the prediction date range and confidence value And the tooltip can be dismissed with Escape, tap outside, or moving the pointer away
Custom Date Ranges and Timezone Handling
Given an organization timezone is set and data exists for the selected period When the user switches between Weekly and any Custom date range and/or changes timezone Then the grid recalculates predictions for the selected range and timezone And hour labels reflect the selected timezone, including DST shifts (23 or 25-hour days) And historical metrics and predictions in cells and tooltips match the selected range
Accessibility: Keyboard Navigation, ARIA, and Colorblind-Safe Palette
Given assistive technology users navigate the heatmap When navigating with keyboard and screen reader Then all interactive elements are reachable via Tab/Shift+Tab with a visible focus indicator And arrow keys move between cells in row/column order without trapping focus And each cell has an aria-label announcing day, hour, predicted conversion %, lift, and confidence And the color palette is verified colorblind-safe (deuteranopia, protanopia, tritanopia) and legend/text meet WCAG AA contrast
Responsive Layout for Mobile and Desktop
Given different viewport sizes When the heatmap is viewed on mobile (<=640px) and desktop (>=1024px) Then on mobile, the grid is horizontally scrollable with sticky day/hour headers; tap targets are >=44x44 px; text is legible at >=12 px; tooltips are accessible via long-press And on desktop, the full week grid is visible without horizontal scroll at 1280x800; tooltips appear on hover; resizing maintains layout without overlap
Scheduling Context Integration on Cell Click
Given a rendered heatmap with scheduling context enabled When the user clicks/taps a cell Then a detail panel opens within 400 ms prefilled with the cell’s date, hour, location, room, class type, and audience And the panel shows predicted conversion %, expected fill-rate lift vs baseline, and confidence And the panel provides actions: Create Class, Move Class, View Conflicts/Waitlists And closing the panel returns focus to the originating cell
Filter & Segmentation Engine
"As a location manager, I want to filter demand by room, class type, and audience segment so that I can tailor scheduling decisions to my context."
Description

Multi-dimensional filters to refine demand signals by location, room, instructor, class type, format (in-person/hybrid), audience segment (new vs returning, member vs drop-in, age bracket), capacity band, and price tier. Supports multi-select, inclusion/exclusion, and date/season toggles (e.g., holidays). Enables saved segments with friendly names, default views per role, and deep-linkable URLs for shareable, permission-respecting views. Persists last-used filters per user. Ensures performant queries via indexed back-end endpoints and client-side caching.

Acceptance Criteria
Apply Multi-Select Inclusion/Exclusion Filters Across Dimensions
Given Demand Pulse is loaded with no filters When the user selects Include values across one or more dimensions (location, room, instructor, class type, format, audience segment, capacity band, price tier) Then results include only classes matching at least one Included value within each active dimension Given a dimension has only Exclude values selected When filters are applied Then results exclude classes matching those values while leaving all other values in that dimension eligible Given Include and Exclude values are both set within the same dimension When filters are applied Then Exclude takes precedence over Include for any overlapping values Given multiple dimensions are active When filters are applied Then the final result set equals the intersection of all active dimensions after exclusion rules Given the user clears a single dimension or clicks "Clear all" When the action is taken Then only the targeted filters are removed and results recalculate accordingly
Date Range, Time Zone, and Holiday/Season Toggles
Given the account time zone is configured When the user selects a custom date range Then events are filtered using the account time zone and daylight-saving transitions are handled correctly Given a supported holiday calendar is configured for the account region When the user enables "Exclude holidays" Then all dates that are holidays in that region within the selected range are excluded from calculations and results Given the user selects a Season preset (e.g., Summer) When the preset is applied Then the date range updates to the preset boundaries and yields the same results as entering the equivalent custom dates Given the current date/season/holiday selections When the user saves the filter state to a segment Then these selections persist with the segment and reload identically when recalled Given no holiday calendar is configured When the user opens the holiday toggle Then the control is disabled with a clear reason and applying it has no effect on results
Saved Segments CRUD With Friendly Names and Validation
Given a set of active filters When the user saves them as a new segment with a friendly name Then the segment is created, the exact filter state is stored, and the name is validated as non-empty, <= 64 characters, and unique (case-insensitive) within the organization Given an existing segment When the user renames it Then the new name is validated with the same rules and saved without altering the stored filter state Given an existing segment When the user deletes it and confirms Then the segment is removed and it no longer appears in segment lists or APIs Given a deleted or non-existent segment ID is requested When a deep link referencing it is opened Then the system responds with a not-found error and no filter state is applied
Deep-Linkable, Shareable, Permission-Respecting Views
Given a filter state or saved segment is active When the user copies a shareable link Then the URL encodes the current state (by reference or value) and opening it in a fresh browser reproduces the same filters and results for authorized users Given a recipient opens a shareable link When they lack permissions for some referenced entities (e.g., a location or instructor) Then disallowed filters are dropped and only permitted data is shown; no unauthorized data is exposed Given a recipient opens a shareable link When they lack access to the Demand Pulse feature entirely Then the request is rejected with an authorization failure and no demand data is returned Given a shareable link contains unknown or deprecated filter values When it is opened Then unknown values are safely ignored and the remaining valid filters load Given a shareable link is opened and the page is refreshed When the user reloads Then the same filter state persists in the URL and the view remains consistent
Default Views by Role and Override Behavior
Given organization role defaults are configured for Admin, Instructor, and Staff When a user with one of these roles opens Demand Pulse for the first time Then the role’s default filter set is applied Given a user has interacted with filters after the first load When they return to Demand Pulse Then their last-used filters override the role default until they select "Reset to default" Given role defaults are updated by an admin When users without personalized filters next load Demand Pulse Then the new defaults are applied for those users, and users with personalized filters remain unaffected Given no default is configured for the user’s role When the user first loads Demand Pulse Then a system fallback default is applied without error
Persist Last-Used Filters Per User Across Sessions and Devices
Given a user adjusts filters (including date/season/holiday toggles) When they sign out and later sign in on the same or a different device Then the last-used filters are restored server-side for that user Given a user selects a saved segment and makes no further changes When they return later Then the previously selected segment is reloaded rather than a raw filter snapshot Given a user’s persisted filters reference entities they no longer have access to When the filters are restored Then the inaccessible filters are dropped and the remaining filters are applied without error Given a user selects "Reset filters" When confirmed Then all last-used filters are cleared and the next load applies the appropriate default
Performance and Scalability of Filtered Queries
Given an organization with up to 100k class sessions and 1M attendance records over the last 12 months When applying any valid combination of filters Then p95 API latency for the heatmap query is ≤ 1000 ms and p99 ≤ 2000 ms under a cold cache Given repeated queries with identical filter states within 60 seconds When the client-side cache is enabled Then p95 API latency is ≤ 300 ms and server load shows a cache hit rate ≥ 80% Given users incrementally add or remove a single dimension filter (e.g., add one location) When the delta is applied Then incremental query p95 latency is ≤ 500 ms Given production-like load of 50 concurrent users per organization for 10 minutes When filters are applied and refreshed every 15 seconds Then there are 0 server 5xx responses and timeouts, and all SLAs above are maintained Given slow-query logging is enabled When EXPLAIN plans are captured for representative filter queries Then indexed access paths are used and no full table scans occur on demand-related tables
Predictive Demand Scoring Pipeline
"As an analyst, I want accurate, explainable demand scores with confidence so that I can trust the predictions and plan staffing and capacity."
Description

A scalable data pipeline that ingests historical bookings, views, waitlists, payments, cancellations, and no-shows; enriches with seasonality, time-of-day, local holidays, and optional external signals (e.g., weather). Produces per-slot predictions: conversion likelihood, expected fill-rate lift versus current schedule, confidence interval, and sample size. Nightly model retraining with incremental updates every 15 minutes; model versioning, A/B rollouts, and fallback heuristics if the model is unavailable. Monitors drift and accuracy with alerts. Ensures privacy via aggregation, pseudonymization, and compliance with applicable data regulations.

Acceptance Criteria
Data Ingestion & Freshness SLA
- Given all source connectors are healthy, when the 15-minute ingestion cycle runs, then events from bookings, views, waitlists, payments, cancellations, and no-shows are ingested exactly-once with duplicate rate <= 0.01% over 24h. - Given valid input records, when schema validation executes, then >= 99.9% of records pass required-field checks; failing records are quarantined with reason codes within 5 minutes. - Given events arrive continuously, when measuring freshness, then P95 feature-store lag <= 15 minutes and P99 <= 30 minutes over rolling 7 days. - Given the pipeline operates over 30 days, when monitoring job runs, then ingestion success rate >= 99.5% with automatic retry (up to 3 attempts) on transient failures.
Feature Enrichment with Seasonality & External Signals
- Given a slot with location and local time, when enrichment runs, then seasonality features (hour-of-day, day-of-week, month-of-year), local holiday flag/name, and time-of-day features are populated for >= 99% of scored slots. - Given geocoded locations and provider availability, when weather enrichment runs, then weather features (temperature, precipitation probability, condition) are populated for >= 95% of relevant slots; otherwise neutral defaults are applied and logged. - Given repeated runs with identical inputs, when enrichment re-executes, then outputs are deterministic and byte-identical. - Given enrichment caches, when accessing holidays and historical weather, then cache TTLs prevent provider calls (holidays: 365 days; historical weather: 24 hours; forecast: 1 hour), and cache hit rate >= 90% in production. - Given enrichment is enabled, when measuring scoring throughput, then enrichment overhead adds <= 15% latency versus baseline P95.
Per-Slot Prediction Output Contract
- Given a valid schedule slot, when scoring completes, then the output contains: slot_id, location_id, room_id, class_type_id, audience_id, start_time (ISO-8601, local TZ), model_version, generated_at (UTC), conversion_likelihood (0.0–1.0), expected_fill_rate_lift_percent (-100 to +100), confidence_interval_low/high (0.0–1.0, low <= high), sample_size (integer >= 0). - Given numeric outputs, when validating ranges, then no field is null/NaN/Inf and all values fall within specified bounds; invalid rows are rejected and logged at <= 0.1% rate. - Given the heatmap API requests predictions, when querying up to 5,000 slots, then the API returns the full payload within 700 ms P95 and 1,200 ms P99 with schema version preserved (backward compatible changes only). - Given schema evolution, when introducing a breaking change, then a new output schema_version is published and the previous version remains supported for >= 90 days.
Nightly Retraining & 15-Minute Incremental Updates
- Given the daily schedule, when the nightly training job starts at 02:00 UTC, then it completes within 90 minutes P95 and 120 minutes P99, using data up to 23:59:59 UTC of the previous day. - Given training runs, when artifacts are produced, then model binary, feature mappings, training parameters, metrics, and data lineage checksums are stored and versioned together. - Given quarter-hour boundaries, when incremental scoring is scheduled, then jobs start at mm in {00,15,30,45} and complete within 5 minutes P95 and 10 minutes P99, updating predictions for newly affected slots. - Given a failure, when nightly training or incremental job fails, then auto-retry up to 3 times occurs and an alert is emitted on each final failure.
Model Versioning, A/B Rollouts & Fallback Heuristics
- Given a successful train, when promoting a model, then an immutable semantic model_version (e.g., vYYYYMMDD-hhmm+commit) is assigned and recorded with reproducible lineage. - Given rollout configuration, when enabling an A/B test, then requests are split by tenant with sticky assignment according to the configured percentages (e.g., 10% new / 90% current) and logged. - Given a rollback command or health check failure (error rate > 5% over 1 minute or p95 latency > 1s), when triggered, then traffic reverts to the previous stable model within 5 minutes. - Given the scoring service is degraded (5xx > 5% for > 30s or timeout > 30s), when failover triggers, then fallback heuristics generate predictions within 1 minute, tagged strategy="heuristic", and service continues without missing more than one scoring cycle; auto-recovery restores model predictions and tagging.
Drift, Accuracy Monitoring & Alerting
- Given daily monitoring, when computing feature drift (PSI) against a 30-day baseline, then warnings are sent for PSI in [0.2, 0.3) on 2 consecutive days and critical alerts for PSI >= 0.3; notifications reach Slack and email within 5 minutes. - Given delayed ground truth availability (e.g., 7 days), when evaluating forecast performance, then AUROC drop > 5% from 30-day baseline raises a warning and > 10% raises a critical; Brier score increase > 0.02 raises a warning and > 0.05 critical. - Given online calibration checks, when comparing predicted conversion deciles to realized rates, then each decile’s absolute calibration error <= 5 percentage points over rolling 30 days; breaches create alerts and a retraining recommendation ticket. - Given alerting, when an alert is emitted, then it is deduplicated, includes runbook links and model_version, and appears on the monitoring dashboard within 2 minutes.
Privacy, Aggregation & Pseudonymization Compliance
- Given ingestion of user-level data, when persisting to the feature store or logs, then PII (names, emails, phone numbers) is removed or pseudonymized (salted hash) and an automated PII scan reports zero offending fields on each deploy. - Given cohort outputs and analytics, when generating reports, then k-anonymity k >= 20 is enforced (no cohort with < 20 distinct users) and slot-level predictions never include direct identifiers. - Given security controls, when data is transmitted or stored, then TLS 1.2+ is used in transit and AES-256 at rest; access is limited via RBAC to least-privilege roles; access logs are immutable and retained 365 days. - Given retention policies, when retention jobs run, then raw personal data is purged after 90 days, derived features after 180 days, and model logs after 365 days with 100% successful deletion logged; GDPR/CCPA DSRs can be fulfilled within 30 days with evidence.
What‑if Scheduling Simulator
"As an instructor, I want to simulate moving or launching a class at different times so that I can choose the slot with the best attendance and minimal conflicts."
Description

Interactive tool to test moving or launching a class into candidate time slots and compare predicted outcomes side-by-side. Shows projected enrollment curve, revenue impact, capacity utilization, likelihood of cannibalization of adjacent classes, waitlist absorption, and no-show risk. Performs conflict checks (instructor, room, location) and honors scheduling constraints (buffers, setup/teardown). Exposes assumptions and sensitivity toggles (price, capacity, promo). Supports exporting scenarios and saving drafts tied to specific classes or templates.

Acceptance Criteria
Side-by-Side Comparison of Candidate Time Slots (Move vs Launch)
Given a selected class or template And the user chooses mode "Move" or "Launch New" When the user adds 2–5 candidate time slots Then the simulator displays a side-by-side comparison with for each slot: projected enrollment curve (daily cumulative), expected revenue, capacity utilization %, expected fill-rate lift vs baseline, and a confidence score And in "Move" mode, baseline is the current scheduled time; deltas reflect replacing the original slot And in "Launch New" mode, baseline remains unchanged; deltas reflect adding an additional instance And the user can set one scenario as baseline, sort by any metric, and remove scenarios individually
Conflict and Constraint Validation
Given selected instructor(s), room, and location with configured buffers and setup/teardown times When a candidate time violates any constraint or conflicts with an existing booking Then the simulator marks the scenario as Invalid, lists each violation (instructor, room, location, buffer, setup/teardown), and disables Save/Export for that scenario And when all constraints are satisfied Then the scenario is marked Schedulable and the visual timeline includes buffers and setup/teardown blocks
Cannibalization Risk Identification and Quantification
Given existing classes targeting the same audience within a configurable window (default ±3 hours on the same day) When simulating a candidate time slot Then the simulator estimates cannibalization likelihood (%) with confidence and lists impacted classes by name and time And if risk exceeds a user-defined threshold (default 20%) Then a warning badge appears and net enrollment change (gain/loss) is shown after cannibalization
Waitlist Absorption and No-Show Risk Projections
Given active waitlists and historical no-show rates for comparable classes When the simulation runs Then the simulator outputs expected waitlist absorption count, residual waitlist, and no-show risk (%) for each scenario And updating capacity or price toggles immediately updates these projections
Sensitivity Controls and Assumptions Transparency
Given default model assumptions are loaded When the user adjusts price, capacity, or promotion intensity Then all metrics recompute within 2 seconds and are annotated with the changed parameters And an Assumptions panel displays data sources, seasonality, confidence method, cancellation/no-show baselines, and last updated timestamps
Save Draft Scenarios Linked to Classes/Templates
Given the user has edit permissions When the user saves a scenario draft Then it is stored with ID, title, linked class/template, selected candidate slots, mode, assumptions, toggles, and timestamp And reopening the draft restores the full comparison state including baseline selection and warnings And drafts are filterable by class/template and searchable by title
Export Scenario Comparisons
Given at least one schedulable scenario When the user exports Then the system generates PDF and CSV files containing comparison tables, enrollment curves, key assumptions, and any conflicts/warnings, branded with organization name/logo and timezone And filenames follow {ClassOrTemplateName}_{YYYY-MM-DD}_v{n}.{ext} And exports can be downloaded and optionally emailed to specified recipients
Actionable Recommendations & One‑Click Scheduling
"As a scheduler, I want actionable time-slot recommendations and one-click class creation or moves so that I can act on insights without duplicating effort."
Description

Surfaced recommendations for top time slots within the current filter context, each with expected fill-rate lift, confidence, and rationale. For existing classes, suggests move options with automated conflict resolution and impact preview; for new classes, offers pre-filled creation flows. Provides one-click actions that create or reschedule classes, place tentative holds on rooms, update reminders, notify enrolled attendees of changes, and roll back on failure. Tracks outcomes of accepted/ignored recommendations to continuously improve the model.

Acceptance Criteria
Surface Ranked Recommendations Within Current Filters
Given active filters for location, room, class type, and audience are applied in Demand Pulse When I open the Recommendations panel Then I see between 3 and 10 recommendations ranked by expected fill‑rate lift descending And each recommendation displays: start date/time (with timezone), location, room, class type, action type (Create or Move), expected fill‑rate lift as a percentage, confidence (numeric 0–1 and label Low/Med/High), and a rationale summary of 200 characters or fewer And all recommendations strictly match the active filters And changing the sort to Time or Confidence reorders the list accordingly within 300 ms
One‑Click Create New Class From Recommendation
Given a recommendation of type Create is visible When I click One‑Click Create on that recommendation Then a pre‑filled creation modal opens with time, duration, location, room, class type, default capacity, pricing, and instructor derived from the recommendation and current context And when I click Confirm, the class is created and appears on the schedule within 3 seconds And a tentative hold is placed on the room for the class duration and is visible on the room calendar And default SMS/email reminders are scheduled per organization templates And a success confirmation is shown and an audit log entry with action id and recommendation id is recorded And repeated clicks within 10 seconds do not create duplicates And on any failure, no partial side effects persist and a clear error with retry option is shown
One‑Click Reschedule Existing Class With Auto‑Conflict Resolution
Given a recommendation of type Move for an existing class is visible When I click One‑Click Move and review the impact preview Then the preview shows predicted fill‑rate delta (%), confidence, count of enrolled attendees, and waitlist impact And the system pre‑holds the target room/time to prevent race conditions; the temporary hold auto‑expires after 10 minutes or on cancel And upon Confirm, the system validates instructor, room, and overlapping class conflicts and applies an automatic resolution from ranked alternatives without creating double‑bookings And the class is rescheduled; the original slot is freed; calendars are updated And enrolled attendees receive change notifications via SMS/email within 2 minutes; reminders are updated to the new time And on any downstream failure, the original schedule is restored and all temporary holds are released
Atomic Rollback and Idempotency Across One‑Click Actions
Given any one‑click action (Create or Move) is initiated When a downstream step fails (e.g., calendar sync or notification dispatch) Then all changes are rolled back: schedule restored, room holds released, reminders unscheduled, notifications canceled And the user is shown an error within 2 seconds of failure detection including a correlation id And retrying the same action within 60 seconds results in at most one class creation/reschedule (idempotency enforced) And an error event with full context is captured in the audit log
Attendee Notifications and Reminder Updates
Given a class is created or rescheduled via a one‑click action When the action completes successfully Then enrolled attendees and assigned staff are notified through configured channels (SMS/email) within 2 minutes And messages include class name, location, date/time with timezone, and an action link; opt‑outs are respected And undeliverable messages are retried up to 3 times with exponential backoff And outdated reminders are canceled and new reminders are scheduled for the updated time And delivery status for each recipient is recorded and visible in the activity log
Recommendation Outcome Tracking and Model Feedback
Given a recommendation is displayed to a user When it is accepted, dismissed, or remains ignored for 7 days Then an outcome record is stored with recommendation id, user action (accepted/dismissed/ignored), timestamp, active filters, predicted lift and confidence, and realized fill‑rate at class start And the record is queryable via analytics API and exportable as CSV within 24 hours And the dashboard shows acceptance rate and realized lift distribution segmented by confidence bucket And aggregated outcomes are incorporated into the next model training at least weekly, with model version and training date recorded and visible
Role‑Based Access, Privacy & Audit
"As an org admin, I want role-based access, privacy controls, and audit logs so that sensitive data is protected and changes are traceable."
Description

Enforces granular permissions so org admins can view all demand data, location managers are scoped to their sites, and instructors see only their classes and rooms. Masks or aggregates sensitive metrics for restricted roles. Controls export/share abilities (CSV, PDF, image) with branding options and watermarks. Logs viewing, exporting, scenario creation, and scheduling actions with timestamp, actor, and context for traceability; provides retention and export of audit logs. Integrates with existing ClassTap authentication, supports SSO and optional 2FA.

Acceptance Criteria
Org Admin Full Demand Visibility
Given an authenticated Org Admin When they open Demand Pulse without filters Then the heatmap includes data for all locations, rooms, class types, and audiences within the org with exact values for conversion likelihood, expected fill-rate lift, and confidence And when they apply any combination of filters (location, room, class type, audience) Then the results include all matching entities across the org And export buttons for CSV, PDF, and Image are visible and enabled And no values are masked or aggregated for this role
Location Manager Scoped Demand Visibility
Given a Location Manager assigned to Locations A and B and none others When they open Demand Pulse without filters Then the heatmap contains only data for Locations A and B (all rooms/class types/audiences within those locations) And when they attempt to access other locations via UI deep link or API Then the response is 403 Forbidden and no data is returned And when exporting from the heatmap Then the exported file contains only data scoped to Locations A and B And no values are masked for this role
Instructor Restricted View and Metric Masking
Given an Instructor assigned to Rooms R1 and R2 and Classes C1 and C2 When they open Demand Pulse Then only R1, R2, C1, and C2 appear in filters and results; all other locations/rooms/classes/audiences are hidden And conversion likelihood and expected fill-rate lift are displayed as ranges (e.g., 40–50%) and confidence is rounded to the nearest 10% And any cohort or time slot with fewer than 5 historical sessions or fewer than 20 unique attendees displays “Insufficient data” instead of values And export actions are not visible and export API calls by this user return 403 Forbidden
Export Controls, Branding, and Watermarking
Given a user with export permission (Org Admin; Location Manager only if org setting export_enabled_for_location_managers=true) When they export the current Demand Pulse view Then available formats reflect role policy (Admin: CSV/PDF/Image; Location Manager per org setting; Instructor: none) And exported data exactly matches the current filters and role-based scope And PDF/Image exports include org name, logo, brand colors, filter summary, generated_at_utc timestamp, and a diagonal watermark containing the user’s full name, role, and email on every page/image And CSV exports include a metadata header row with generated_at_utc, generated_by, role, org_name, and filter_summary; metric masking rules are preserved in the dataset And watermark/branding cannot be disabled by the client and is present in the generated files
Audit Logging of Views, Exports, Scenarios, and Scheduling
Given a user performs any of the following: heatmap_view, export_created, scenario_created, class_scheduled_or_moved Then the system writes exactly one append-only audit log entry per action within 2 seconds And each entry contains: event_type, event_id (UUIDv4), timestamp_utc (ISO 8601), actor_user_id, actor_role, org_id, location_ids, room_ids, applied_filters (JSON), export_format (if any), scenario_id (if any), class_id (if any), previous_value, new_value, and request_id And Org Admins can search and view all audit logs; Location Managers can search logs scoped to their locations; Instructors cannot access audit logs (403) And all access to the audit log endpoints is itself logged with event_type=audit_log_access
Audit Log Retention and Export
Given the default audit log retention is 180 days When an Org Admin updates retention to 365 days Then logs are retained for 365 days and purge schedules update accordingly And a daily purge runs at 02:00 UTC and deletes entries older than the configured retention And when an Org Admin exports audit logs for a specified time range with up to 1,000,000 records Then the CSV file is generated within 60 seconds and contains all standard fields with a header row and filter summary metadata And the export event is logged with event_type=export_created and includes the export parameters and resulting row_count
SSO, Role Mapping, and 2FA Enforcement for Demand Pulse Access
Given an org has SSO configured via SAML 2.0 or OIDC with group-to-role mappings (admin, location_manager, instructor) When a user signs in via SSO and is a member of the mapped group Then the assigned role is applied to the session and Demand Pulse authorizations reflect that role And when an org enforces 2FA for non-SSO accounts Then users must complete 2FA before accessing Demand Pulse; failure results in 401 Unauthorized and no data rendered And when a user’s role is removed in the IdP Then within 5 minutes of next login or token refresh, access to previously allowed resources is revoked and subsequent requests return 403 And authentication context (sso_provider, mfa_method) is included in audit logs for all Demand Pulse accesses

SlotSense

When creating or moving a class, get the top suggested time slots ranked by predicted utilization. SlotSense weighs instructor availability, room conflicts, seasonality, past waitlists, and commute patterns, then applies your pick in one tap—publishing a schedule that fills faster with less back-and-forth.

Requirements

Signal Aggregation & Data Pipeline
"As a studio owner, I want SlotSense to consider all relevant signals (availability, rooms, history, seasonality, commute) so that suggested time slots reflect real-world constraints and demand."
Description

Build a real-time, multi-source data pipeline that unifies instructor availability (ClassTap profiles and connected calendars), room resources, historical bookings, waitlists, seasonality patterns (weekday/term/holiday effects), location data, and inferred commute times between consecutive classes. Implement ETL to normalize time zones, class durations, capacities, buffers, and resource identifiers; deduplicate and reconcile conflicts; maintain freshness SLA < 5 minutes for availability/resource changes and daily refresh for historical features. Provide a feature store for model inputs (e.g., trailing fill rates, lead-time-to-fill, no-show probabilities, demand by hour/weekday/season, room utilization, instructor travel time constraints). Enforce privacy by limiting geodata to corridor-level travel times, hashing PII, and honoring account data-retention settings. Expose a query service for the SlotSense engine and cache hot signals for <200ms reads.

Acceptance Criteria
Real-time Availability Update SLA
Given an instructor updates availability or a connected calendar adds/modifies/cancels an event for a tracked resource When the event is ingested by the pipeline Then the unified availability and resource state reflect the change within 5 minutes of the event’s source timestamp And any SlotSense query for an affected slot returns the updated state
Daily Refresh of Historical Features
Given new bookings, cancellations, and waitlist changes occur during the day When the daily historical refresh completes Then trailing fill rates, lead-time-to-fill, no-show probabilities, demand by hour/weekday/season, and room utilization are recomputed to include events up to the prior day And the feature store metadata shows an update time within the last 24 hours And SlotSense queries return the refreshed values without application changes
ETL Normalization Across Sources
Given incoming records with mixed time zones, class durations, capacities, buffers, and resource identifiers from multiple sources When ETL processes the records Then timestamps are stored in UTC with preserved IANA time zone identifiers And class durations, capacities, and buffers are normalized to canonical units and types And resource identifiers are mapped to canonical IDs with referential integrity checks passing
Deduplication and Conflict Reconciliation
Given duplicate or conflicting events for the same instructor, room, and time window from different sources When the unification step runs Then duplicates are collapsed into a single canonical record And conflicts are resolved by a deterministic reconciliation policy producing one final state per resource-time And the result prevents double-bookings in the unified calendar And an auditable log entry records the inputs and chosen outcome
Feature Store Completeness and Contracts
Given a query context with instructor, room, location corridor, and candidate date-time When fetching model inputs from the feature store Then the following features are available and non-null: trailing fill rates, lead-time-to-fill, no-show probabilities, demand by hour/weekday/season, room utilization, instructor travel time constraints And features adhere to documented schema versions and value ranges And feature retrieval for a single context completes successfully via the query service
Privacy and Data Retention Compliance
Given PII fields and geolocation inputs are ingested When data is stored and served Then PII is hashed or tokenized at rest and never exposed in query responses And geodata is limited to corridor-level travel times without storing precise origin/destination coordinates And account-specific data-retention settings are honored, with expired records excluded from query responses and removed or anonymized from storage
Query Service Latency and Hot Signal Cache
Given a set of frequently requested signal bundles for SlotSense When identical queries are issued after an initial warm-up Then responses for hot keys return within 200 ms And responses include the latest available state per freshness SLAs And cold-start queries still return correct data via the underlying store
Conflict-Aware Slot Generation
"As an instructor, I want SlotSense to only show conflict-free times that respect my buffers and travel needs so that I can schedule confidently without manual checking."
Description

Generate feasible candidate time slots for new or moved classes by intersecting instructor availability, room calendars, business hours, blackout dates, and class constraints (duration, capacity, setup/teardown buffers, recurrence rules). Respect travel and turnaround time between an instructor’s adjacent classes across locations; detect and exclude slots that cause resource or overlap conflicts. Handle edge cases: daylight saving changes, cross-time-zone instructors, hybrid classes with room + virtual resource, and minimum lead time before start. Produce a ranked-ready candidate set per location with metadata (resources required, applied constraints, conflict reasons if excluded) within 500ms for a typical week view.

Acceptance Criteria
Feasible Slot Intersection Within Business Constraints
Given a request to generate candidate slots for a new class at location L for week W with duration D, setup buffer S, teardown buffer T, minimum room capacity C, and optional recurrence rule R And instructor I has availability windows A And location L has business hours B and blackout dates X And room calendars include existing bookings and holds When candidate slots are generated Then every returned slot starts and ends within B and not on X And each slot’s window [start - S, end + T] does not overlap any room booking or hold And the slot duration equals D exactly And only rooms with capacity >= C are used And each slot lies entirely within instructor availability A (converted as needed) And, if R is provided, only occurrences that satisfy all above constraints are produced
Instructor Travel and Turnaround Across Locations
Given instructor I may have adjacent confirmed classes with end time t_prev at location L_prev and start time t_next at location L_next And a turnaround buffer Tt and a travel time function travel_time(from, to, departure_time, instructor) When generating candidate slots at location L for I Then no slot is returned with start_time < t_prev + travel_time(L_prev, L, t_prev, I) + Tt And no slot is returned with t_next < slot_end + travel_time(L, L_next, slot_end, I) + Tt And slots violating these conditions are excluded with conflict reason codes TravelTime or Turnaround
Hybrid Class Resource Locking
Given a class requires both a physical room R and a virtual resource V And buffers S and T apply to room R And virtual resource V has a concurrency limit of 1 per time window When generating candidate slots Then a slot is feasible only if some room R with capacity >= required capacity is free for [start - S, end + T] And virtual resource V is free for [start, end] And feasibility checks consider both resources atomically to prevent overlap conflicts And exclusions indicate which resource caused the conflict with reason codes RoomConflict or VirtualResourceConflict
Daylight Saving Time Transitions
Given week W for location L includes a daylight saving time change When generating candidate slots Then start/end times are computed in L’s time zone and preserve wall-clock duration D And no slot is proposed that spans a nonexistent hour (spring forward) or causes duplicate-hour overlap (fall back) And setup/teardown buffers S and T are applied in wall-clock time consistent with L’s DST rules
Cross-Time-Zone Instructor Availability
Given instructor I defines availability windows in time zone TZ_I and the class location uses TZ_L When generating candidate slots for dates D Then availability windows are converted from TZ_I to TZ_L per date D considering DST in both zones And no slot is returned that falls outside the converted availability windows in TZ_L And exclusions due to conversion appear with reason code OutsideInstructorAvailability
Minimum Lead Time Enforcement
Given a minimum lead time ML from now to class start When generating candidate slots at current time now Then any slot with start_time < now + ML is excluded with reason code MinLeadTime And for recurring series, the first occurrence must satisfy ML; each subsequent occurrence must individually satisfy ML at generation time
Ranked Candidate Set, Metadata, and Performance SLA
Given a week-view request for location L When candidate slots are generated Then the response includes a ranked list of feasible candidates ordered by predicted utilization score descending And each candidate includes metadata: required resources, selected room id and capacity, applied constraints, and a stable unique identifier And for excluded candidates, structured conflict reason codes with implicated resource ids are available And server-side generation time for the Standard Week View dataset is <= 500 ms for at least 95% of requests
Utilization Prediction & Ranking Engine
"As a studio manager, I want the best time slots ranked by predicted fill so that our schedule fills faster with less back-and-forth."
Description

Develop a scoring service that predicts expected utilization for each candidate slot and returns a ranked list. Features include historical fill curves, waitlist pressure, seasonality (weekday, month, school term, holidays), price elasticity signals, neighborhood demand by time-of-day, instructor popularity, and cold-start priors by category. Incorporate constraints as penalties (e.g., short lead time, atypical hours) and allow business-weighted objectives (maximize utilization, minimize idle room gaps, reduce instructor travel). Provide an API returning top N suggestions with score, confidence, and key drivers. Latency target < 300ms for 200 candidates. Fallback to rule-based heuristics when history is sparse.

Acceptance Criteria
API Ranking Response Format and Ordering
Given a request to POST /slotsense/suggest with candidate_slots=200, top_n=10, and objective="maximize_utilization" When the service processes the request Then HTTP status is 200 and response.data.suggestions has length 10 And each suggestion includes fields: slot_id (string), score (0..1 float), confidence (0..1 float or null in heuristic mode), key_drivers (array length >= 3), penalties_applied (array), objective_used (string), and mode in {"model","heuristic"} And suggestions are strictly sorted by score descending with ties broken by ascending slot_id And response.metadata includes model_version, latency_ms, candidate_count, and generated_at (ISO-8601)
Latency Target Under Load
Given 200 candidates per request and a steady load of 10 requests per second for 60 seconds When measuring end-to-end latency at the API boundary Then p95 latency <= 300 ms and p99 latency <= 400 ms And timeout or 5xx error rate <= 0.1% And no single request exceeds 600 ms
Prediction Quality Backtest Thresholds
Given a 90-day holdout dataset across top categories and locations When running offline backtests of predicted utilization vs realized fill curves Then NDCG@10 >= 0.85 and Kendall Tau-b >= 0.60 on per-class rankings of candidate slots And calibration Brier score for confidence <= 0.18 And thresholds are met in each segment: weekday vs weekend, morning vs evening, and low vs high price quartiles
Constraint Penalties and Hard Exclusions
Given candidate slots with room conflicts or instructor unavailability When scoring Then hard-conflict slots are excluded from returned suggestions And soft constraints (e.g., short lead time < 24h, atypical hours) apply configured penalty weights and appear in penalties_applied and key_drivers And for a test slot with base_score=0.72 and penalties=[0.15,0.05], the final score is between 0.51 and 0.53 and the corresponding drivers show negative direction
Business Objective Weighting Effects
Given objective="minimize_idle_gaps" with weight >= 0.6 and a sample day schedule When suggestions are generated Then the average idle gap between selected class times decreases by >= 20% compared to objective="maximize_utilization" on the same inputs And response.metadata.objective_used equals "minimize_idle_gaps" And changing the objective to "maximize_utilization" reorders the top-3 suggestions to align with utilization scores primarily
Cold-Start and Sparse History Fallback
Given a location and category with fewer than 10 historical sessions and no waitlist data When scoring is requested Then the service returns mode="heuristic" with reason_codes including "cold_start" and "history_sparse" And suggestions are based on category and seasonality priors and neighborhood time-of-day demand And p95 latency in heuristic mode for 200 candidates <= 350 ms
Explainability: Key Drivers Coverage and Limits
Given any returned suggestion in model mode When inspecting key_drivers Then there are between 3 and 8 drivers including at least one positive and one negative when applicable And drivers cover applicable factors: seasonality, waitlist pressure, instructor popularity, neighborhood demand, price elasticity, and penalties And each driver contains name, direction in {+,-}, magnitude in [0,1], and message length <= 120 characters
One-Tap Apply & Publish
"As a community center coordinator, I want to apply the chosen slot in one tap and publish immediately so that I can finalize schedules without extra admin steps."
Description

Implement UI and backend actions to apply a selected suggestion in one tap: create or update the class session(s), reserve room resources, update instructor calendars, and publish to the branded booking page. For moves, surface impact preview (enrollments affected, waitlist transfers, outstanding payments) and support automated notifications via SMS/email with templated messaging. Respect ClassTap’s cancellation/reschedule policies, require confirmations where configured, and provide atomic rollback on failure. Support single, series, or partial-series application with bulk updates. Ensure end-to-end completion under 2 seconds for single-class operations.

Acceptance Criteria
One-Tap Apply for Single-Class Suggestion
Given a suggested time slot is selected and "Apply & Publish" is tapped When the operation executes for a single class Then the system creates or updates the class session, reserves the room, updates the instructor calendar, and publishes to the branded booking page in one transaction And the total time from tap to success response is <= 2000 ms measured at the API gateway And the operation returns a success toast with the new date/time and a link to view the booking page
Publish and Booking Page Update for Applied Change
Given a class is created or updated via One-Tap Apply When an anonymous user opens the branded booking page Then the class appears with correct title, date/time, capacity, pricing, modality (in-person/hybrid), and enrollment policy And previous schedule for the class is no longer visible And CDN/cache is invalidated so that changes are visible within 5 seconds globally
Move With Impact Preview and Confirmation
Given the user initiates a move via SlotSense suggestions When a suggested slot is tapped Then an impact preview modal displays counts: enrolled affected, waitlist candidates to transfer, and outstanding payments And if org policy requires confirmation, the primary button is disabled until the user confirms via checkbox or secondary confirm dialog And the apply action is blocked if reschedule or cancellation violates configured blackout or cutoff rules unless the user has override permission and explicitly confirms
Automated SMS/Email Notifications on Move
Given a class move is successfully applied When notifications are enabled for the org Then SMS and email are sent using the selected template to all affected students and instructors within 60 seconds And messages include old and new time, location or join link, and action links to confirm or decline if policy allows And recipients who have opted out are excluded; bounces and failures are logged and surfaced in the activity feed
Resource Reservation and Conflict Prevention
Given the class room and instructor are associated to the session When One-Tap Apply is executed Then the room and instructor calendars are reserved for the new time without double-booking And if a conflict exists at apply time, the system blocks the apply, surfaces the conflicting resource and time, and presents alternative suggestions And no conflicting reservations are created
Series and Partial-Series Bulk Apply
Given a recurring class series with N sessions and a set of target sessions is selected When One-Tap Apply is executed for series or partial-series Then the system updates only the selected sessions, preserving unselected ones And all updated sessions are published and resource reservations applied consistently And the operation supports idempotency such that reapplying the same change produces no duplicate sessions or reservations And progress feedback is shown during bulk updates and completion state summarizes successes and failures
Atomic Rollback on Failure
Given One-Tap Apply has begun When any step in create or update, reservation, calendar update, publish, or notification fails Then all prior changes for this operation are rolled back with no partial state persisted And the user sees a single error message with a correlation ID and retry option And audit logs capture the attempted changes and failure reason
Explainability & Confidence Indicators
"As a small studio owner, I want to see why a time is recommended so that I can trust the suggestion and explain it to my team."
Description

Display transparent rationale for each suggested slot to build trust and speed decisions: show top drivers (e.g., “High local demand Tue 6–7pm,” “Short commute from prior class,” “No room conflicts”), predicted utilization range with confidence, and trade-offs (e.g., “10% lower utilization but keeps instructor free Friday”). Provide quick filters (avoid late nights, prefer studio A, 60–90 min only) that re-rank suggestions. Include a “Why not?” view for excluded times with specific conflict reasons. Persist user dismiss/accept feedback for learning without exposing PII.

Acceptance Criteria
Top Drivers Display for Suggested Slot
Given SlotSense has generated at least one suggested time slot for a class When the suggestions list is rendered Then each suggested slot displays a Top drivers section containing 2–5 ranked drivers And each driver shows a human-readable label and a short explanation tooltip And at least one driver reflects each applicable factor among: instructor availability, room conflicts, seasonality, past waitlists, commute patterns And if fewer than 2 drivers are available, the slot shows Limited data and still renders all available drivers
Predicted Utilization Range and Confidence
Given a suggested slot is visible When utilization predictions are available Then the slot displays a utilization percentage range (e.g., 62–74%) And a confidence indicator labeled Low, Medium, or High with a tooltip showing the confidence interval (e.g., 90% CI) And the displayed range width does not exceed 20 percentage points for Medium/High confidence and may exceed for Low confidence And if predictions are unavailable, the slot displays No prediction and confidence is not shown
Trade-offs Explanation per Slot
Given at least two candidate slots exist for the class When a slot is rendered Then the slot displays 1–3 trade-off statements comparing it to the top-ranked alternative And each trade-off quantifies the impact where applicable (e.g., percentage difference in predicted utilization or constraint satisfied/violated) And trade-offs contain no PII and are phrased in plain language
Quick Filters Re-rank Suggestions
Given the user opens SlotSense suggestions When the user applies filters (avoid late nights, prefer studio A, 60–90 min only) Then the suggestions list refreshes and re-ranks to reflect the active filters within 2 seconds end-to-end for the 95th percentile And only slots satisfying all active filters are displayed unless Show excluded is toggled And each remaining slot’s drivers, utilization range, confidence, and trade-offs update to the filtered context And an Active filters chip list is visible and can be cleared in one action
Why Not? View for Excluded Times
Given a time range is not shown in suggestions due to constraints When the user selects Why not? for that time range Then a panel lists specific conflict reasons with entity and time details (e.g., Room A booked 6:00–7:00pm, Instructor unavailable Tuesdays) And each reason maps to a defined category: instructor availability, room conflict, policy filter, commute constraint, low predicted utilization And if multiple reasons apply, they are shown in priority order with all applicable reasons visible And the panel provides a link or hint to adjust relevant filters where applicable
Feedback Persistence Without PII
Given the user accepts or dismisses a suggested slot When the action is taken Then the feedback is persisted and used to adjust future rankings for this tenant/account And no PII (names, emails, phone numbers, exact addresses) is written to logs, analytics, or the model store; only pseudonymous IDs are stored And feedback effects are observable on the next suggestions refresh within the same tenant And users can undo the last accept/dismiss within the current session
Continuous Learning & Performance Measurement
"As a product owner, I want SlotSense to learn from outcomes and show measurable impact so that we can continuously improve utilization and reduce no-shows."
Description

Instrument end-to-end metrics and a feedback loop: suggestion acceptance rate, time-to-fill, show-up rate, waitlist conversion, schedule changes avoided, and utilization delta vs manual scheduling. Enable offline model retraining pipelines and periodic evaluation with backtests and A/B tests (e.g., top-3 SlotSense vs control). Provide dashboards per account and global, plus alerting when performance regresses. Capture labeled outcomes from user overrides, rejections, and successful fills to update features and improve cold-start priors.

Acceptance Criteria
Account and Global Metrics Instrumentation
Given production telemetry is enabled and events are flowing When a class is created, a SlotSense suggestion is shown, accepted, rejected, overridden, or the class fills/starts Then metrics for suggestion acceptance rate, time-to-fill, show-up rate, waitlist conversion, schedule changes avoided, and utilization delta vs manual are emitted with account_id and global scope within 5 minutes And aggregates are updated idempotently with no more than 1 duplicate per event And missing-data states are surfaced without errors for new accounts
Per-Account and Global Performance Dashboards
Given an authenticated org admin or ClassTap staff user When they open the performance dashboard Then they can view account-level and global metrics with selectable date ranges (Last 7/28/90 days, custom), filters (instructor, class type, location, in-person/hybrid), and time zone aligned to the account And each metric tile displays definition and formula on hover/tap And users can export CSV for any view within 30 seconds for up to 100k rows And unauthorized users are denied access (HTTP 403)
Regression Alerting on Performance
Given baseline metrics computed over the trailing 28 days per account and globally When any monitored metric deviates beyond configured thresholds (e.g., acceptance rate −10% absolute or >2σ) for 2 consecutive hours Then an alert is sent to configured channels (email/Slack) containing metric name, scope, baseline, current value, time window, and a deep link to the dashboard And alerts are suppressed during defined maintenance windows and deduplicated to at most 1 alert per metric per scope per hour And a recovery notification is sent when the metric returns within thresholds
A/B Testing: Top-3 SlotSense vs Control
Given the experiment is enabled When eligible scheduling sessions occur Then sessions are randomized 50/50 into Treatment (show top-3 SlotSense suggestions) and Control (no suggestions) with account-level stratification and no cross-over for a 14-day lookback And exposure, assignment, and outcomes are logged per session When the precomputed sample size is reached or 14 days elapse Then the system computes primary effects on time-to-fill, utilization delta, and acceptance rate with p-values and 95% CIs (alpha=0.05, multiple-testing corrected) and marks the result Pass/Fail And if guardrail metrics (no-show rate, cancellations) degrade by >5% absolute, the experiment auto-pauses and alerts fire
Offline Model Retraining and Backtesting
Given a weekly retraining schedule When the pipeline runs Then it loads versioned features and labeled outcomes for the last 26 weeks, trains a candidate model, records training/validation metrics, artifact hashes, and registers the model with semantic versioning When backtesting the candidate on the last 12 weeks with cross-validation Then the candidate must achieve ≥+2% predicted utilization uplift with ≤3% regression on acceptance rate vs current; otherwise it is not promoted When promoted to staging Then a canary serves 10% of traffic for 48 hours with automatic rollback on any triggered regression alert
Outcome and Override Feedback Capture
Given a SlotSense suggestion is displayed When a user accepts, rejects, or overrides the suggested time Then the system records the action with timestamp, user_id, suggestion_id, optional reason, and context (instructor, room, class type) When attendance and waitlist data are finalized for the class Then the show-up rate and waitlist conversion labels are updated and linked to the originating suggestion And ingestion deduplicates by composite key (account_id, suggestion_id, action, timestamp bucket) and preserves an audit trail of changes And cold-start accounts (<50 labeled events) default to cohort priors that update as outcomes accrue
Utilization Delta vs Manual Scheduling Measurement
Given a defined baseline (pre-SlotSense period or matched control cohort) When computing utilization delta for classes scheduled with SlotSense Then the system applies difference-in-differences adjustments for seasonality and instructor, and outputs per-class and aggregate deltas with 95% CIs and sample sizes When the dashboard is filtered to a class, instructor, or date range Then the utilization delta, assumptions, and any data insufficiency caveats are displayed and excluded from aggregates if thresholds are not met
Admin Controls, Constraints & Permissions
"As an account admin, I want to configure constraints and permissions for SlotSense so that suggestions align with our policies and only authorized staff can publish changes."
Description

Provide account-level settings and per-instructor controls to guide SlotSense: business hours by location, blackout dates, minimum lead time, max classes per day, required buffers, preferred rooms, commuting mode assumptions, target utilization thresholds, and objective weighting (fill-rate vs travel vs gaps). Respect roles and permissions so only authorized users can apply schedule changes; log all suggestions viewed/applied with audit trails. Offer API and UI to pin or block times, set recurring constraints, and export suggested schedule drafts for review.

Acceptance Criteria
Business Hours and Blackout Dates Enforcement
Given a location has business hours set to Mon–Fri 08:00–20:00 local time and blackout dates on 2025-10-10 to 2025-10-12 When SlotSense generates suggested time slots for that location Then no suggested slot starts before 08:00 or after 20:00 on any weekday And no suggested slot falls on a blackout date And any attempt to apply or move a class to a disallowed time via UI is blocked with an inline error stating the violated constraint And any attempt via API is rejected with HTTP 422 and a machine-readable error code (e.g., constraint_violation.business_hours or constraint_violation.blackout) And upon updating business hours or blackout dates, subsequent suggestion requests reflect the change immediately without requiring a service restart
Minimum Lead Time and Buffer Enforcement
Given account-level minimum lead time is 24 hours and required buffer between classes is 15 minutes for the same instructor and the same room When generating suggestions at 2025-09-15T10:00:00Z Then no suggested slot starts earlier than 2025-09-16T10:00:00Z And any two back-to-back suggestions for the same instructor or same room are separated by at least 15 minutes And attempts to apply a class that violates lead time or buffer are prevented in UI with a clear message and in API with HTTP 422 and error codes constraint_violation.lead_time or constraint_violation.buffer And moving an existing class triggers the same validations before commit
Max Classes Per Day Per Instructor
Given Instructor A has a maximum of 3 classes per day configured When SlotSense generates suggestions for a day where Instructor A already has 2 confirmed classes Then no more than 1 additional suggestion is returned for Instructor A on that day And attempts to apply a 4th class for Instructor A on any day are blocked in UI and rejected via API with HTTP 422 and error code constraint_violation.max_per_day And the reason for exclusion (max_per_day) is available on filtered-out candidates when requesting suggestions with diagnostics=true
Pin/Block Times and Recurring Constraints via UI and API
Given an admin pins Tuesdays 17:00–18:00 for Class X (weekly recurrence) and blocks Fridays 09:00–12:00 for Location L (weekly recurrence) When creating, updating, or deleting these constraints via UI or API Then the system persists the recurrence rule (RRULE) and returns the canonicalized series details And pins cannot overlap blocks; attempts to do so are rejected with HTTP 409 conflict and error code constraint_conflict.pin_vs_block And SlotSense suggestions exclude blocked windows and reserve pinned windows for the associated class/instructor And deleting or disabling a constraint updates subsequent suggestion results on the next request
Objective Weighting, Commuting Assumptions, and Target Utilization
Given objective weights are set to fill_rate=0.6, travel_time=0.3, gap_minimization=0.1; commuting mode=bike with average speed 15 km/h; and target utilization threshold=75% When SlotSense returns N suggestions for a class Then each suggestion includes a score_breakdown with factors matching configured weights and an overall_score in [0,1] And suggestions with predicted utilization < 75% are excluded by default, unless include_below_threshold=true is specified And changing weights or commuting mode results in a deterministically different ranking order reflecting the new configuration on the next request And the top-ranked suggestion has the highest overall_score among returned suggestions
Preferred Rooms and Room Conflict Avoidance
Given Instructor B has preferred rooms [Studio 1, Studio 3] and required buffers are configured for rooms When SlotSense generates suggestions Then no suggestion uses a room that is already booked during the proposed time or that would violate room buffer constraints And suggestions using preferred rooms are ranked above otherwise identical suggestions in non-preferred rooms And each suggestion includes room_id and a preference_flag indicating preferred vs non-preferred
Role-Based Permissions, Audit Trails, and Draft Export
Given roles are defined as Admin, Scheduler, and Viewer with only Admin/Scheduler allowed to apply schedule changes When a Viewer attempts to apply a SlotSense suggestion Then the UI does not display the Apply action and API attempts are rejected with HTTP 403 forbidden and error code permission_denied.apply When an Admin or Scheduler applies a suggestion Then an audit record is written capturing user_id, role, timestamp (UTC ISO 8601), suggestion_id, action (applied), before_time, after_time, location_id, instructor_id, constraint_snapshot_version, and objective_weights And viewing suggestions writes an audit record with action=viewed and suggestion_id list And audit records are immutable and queryable by date range and user, and exportable as CSV and JSON with consistent headers and row counts And users can export a suggested schedule draft (selected suggestions) via UI and API, producing a versioned draft artifact that can be downloaded and later applied (subject to permissions), with both export and apply actions logged

EventAware

Enrich forecasts with local signals—school breaks, holidays, major events, even weather alerts. EventAware flags risky hours before you publish, proposes safer alternatives, and auto-adjusts confidence bands so your calendar rides demand waves instead of fighting them.

Requirements

Multi-Source Event Data Ingestion
"As an operations manager, I want ClassTap to automatically pull reliable local event data so that I don’t have to manually track holidays and city events that affect my classes."
Description

Build and operate connectors that continuously ingest and normalize local signals (public holidays, school district calendars, major city events, venue schedules, and weather alerts) into a unified EventAware schema. Implement scheduled fetches, webhook listeners where available, deduplication, and data quality checks, with fields for event category, location geometry, start/end, expected attendance/severity, and source provenance. Cache results with TTLs, handle API rate limits and failures with retries/fallbacks, and align ingested events to ClassTap tenants and locations by geo metadata. Expose a standardized internal event feed for downstream scoring and forecasting pipelines.

Acceptance Criteria
Scheduled Fetch and Webhook Ingestion Reliability
- Given connectors configured with per-source fetch intervals, when the scheduler runs, then each source is fetched within its configured interval with jitter not exceeding ±5 minutes for 99% of runs in a 24-hour window. - Given a source provides webhooks, when a webhook is received, then the payload is validated and persisted to the ingestion queue within 2 seconds for 99% of requests and a 2xx response is returned. - Given transient network failures, when a fetch attempt fails, then the system retries up to 3 times with exponential backoff (1s, 4s, 16s) and jitter, and surfaces a failure metric if all retries fail. - Given a source-specific maintenance blackout window, when the scheduler reaches that window, then no fetch is attempted and the next run is scheduled after the window ends. - Given duplicate webhook deliveries, when an identical delivery_id or signature is seen within 24 hours, then the payload is idempotently ignored and acknowledged without side effects.
Schema Normalization and Required Fields Mapping
- Given an event is ingested, when it is normalized, then the unified schema includes: event_id (UUID), category (one of [public_holiday, school_break, city_event, venue_event, weather_alert]), title, description (optional), location_geometry (GeoJSON Point/Polygon/MultiPolygon), start_time_utc, end_time_utc, expected_attendance (integer, optional), severity (enum: info, minor, major, critical; optional), source_provenance {source_id, source_type, fetch_method, fetched_at_utc, source_event_id, version_tag}. - Given an event spans times, when start and end are parsed, then start_time_utc < end_time_utc and both are stored in UTC with original timezone preserved as metadata. - Given a location text address or venue name, when geocoding occurs, then location_geometry must be present with precision radius ≤ 250m for points, or polygon area > 0 for polygons; failures are quarantined and not emitted downstream. - Given missing optional fields, when normalization runs, then fields are set to null (not empty strings) and required fields are present; otherwise the record is rejected with a machine-readable error code. - Given text fields, when normalization runs, then titles are trimmed, normalized to NFC, and limited to 140 chars; descriptions limited to 2,000 chars.
Cross-Source Deduplication and Versioning
- Given two or more source events, when their normalized titles are a fuzzy match (Levenshtein distance ≤ 3 or Jaro-Winkler ≥ 0.92), their time windows overlap by ≥ 50%, and their centroids are within 500 meters, then they are clustered into a single canonical event. - Given a canonical event is created, when additional duplicates are found, then the canonical record aggregates all source_event_ids and preserves the most recent version_tag per source with a deterministic winning strategy (newest fetched_at_utc, then highest severity, then largest expected_attendance). - Given a source updates or cancels an event, when a new version is ingested, then the canonical event version increments and downstream consumers receive an upsert with action = update or cancel within 60 seconds for 99% of cases. - Given conflicting fields across sources, when canonicalization occurs, then provenance is retained for each chosen field along with a confidence score in [0,1]. - Given near-duplicates below thresholds, when evaluated, then they remain separate and are not merged, with rationale stored for audit.
Geo Alignment to Tenants and Locations
- Given a tenant with registered locations (points with service radii) and service areas (polygons), when an event is normalized, then it is associated to the tenant if its geometry intersects any tenant polygon or lies within any location radius. - Given multiple tenants in the same metro, when an event is aligned, then no association is leaked across tenants; associations are tenant-scoped and verifiable by tenant_id. - Given an event with large area (citywide/region), when aligned, then it is associated to all tenants whose polygons intersect the event geometry; the association strength is computed as intersection_area / tenant_area in [0,1]. - Given 10k events to align, when batch alignment runs, then throughput is ≥ 1,000 events/second with p95 alignment latency ≤ 50 ms per event on provisioned hardware. - Given geometry parsing errors, when alignment is attempted, then the event is quarantined and excluded from downstream feeds for that tenant.
Resilience to Rate Limits, Timeouts, and Source Outages
- Given a source advertises a rate limit (e.g., 60 requests/min), when fetching, then the connector honors the limit with token bucket enforcement and maintains a 0% 429 rate over a 15-minute sliding window. - Given timeouts or 5xx responses, when fetching, then the connector retries with exponential backoff and circuit breaker; once the breaker is open, subsequent calls are suppressed for a cooldown and metrics/alerts are emitted. - Given an outage extends beyond 15 minutes, when fallback is configured, then the system switches to a secondary mirror or cached snapshot and marks events with provenance.fallback = true. - Given persistent failures, when dead-letter thresholds are exceeded (e.g., 5 consecutive failures), then the item is routed to a DLQ with correlation ids and is visible in ops dashboards within 1 minute. - Given recoveries, when the source is healthy again, then backlog is drained without exceeding the rate limit and data completeness for the affected window reaches ≥ 99% within 2 hours.
Caching, TTLs, and Invalidation Semantics
- Given normalized events, when stored in cache per tenant and category, then TTLs are applied per category: public_holiday=30d, school_break=30d, city_event=7d, venue_event=3d, weather_alert=2h. - Given a webhook update for an event, when received, then the corresponding cache entries are invalidated within 5 seconds and rehydrated on next read. - Given cache misses, when a read occurs, then the system fetches from the authoritative store and populates cache, achieving ≥ 80% cache hit ratio over a 24-hour period. - Given concurrent updates, when cache write occurs, then updates are atomic and readers never observe partial records (read-your-writes within the tenant scope). - Given TTL expiry, when data is stale, then stale records are not served beyond TTL; a background refresh replenishes near-expiry keys proactively for hot keys.
Standardized Internal Event Feed Contract
- Given downstream consumers, when they query the internal feed, then they can filter by tenant_id, location_id, time window, category, and bounding box, and receive results paginated or streamed with a continuation cursor. - Given the feed API, when called, then p95 response latency is ≤ 300 ms for queries returning ≤ 500 events and stream init time ≤ 1 second. - Given schema evolution, when a new minor version is released, then it is backward compatible; deprecations follow a 2-release grace period with dual-write and dual-read support. - Given consumer checkpoints, when events are consumed, then high-watermark cursors allow exactly-once or at-least-once semantics documented per transport (HTTP pull vs. Kafka topic), with idempotent upserts guaranteed by event_id. - Given authorization, when a request is made, then only events associated to the caller’s tenant_id are returned; unauthorized access is denied with 403 and audited.
Geospatial Matching & Impact Scoring
"As a studio owner, I want nearby events evaluated for their likely impact on my class times so that I can schedule around disruptions and capitalize on demand spikes."
Description

Match ingested events to classes using geospatial proximity, travel-time estimates, and temporal overlap, then compute an impact score representing expected demand uplift or suppression. Incorporate distance decay, event scale, category (e.g., marathon, school break, storm), historical correlations, day-of-week effects, and timing (lead time vs. last-minute) into a tunable scoring model. Provide configurable thresholds per tenant/location and return reason codes and confidence for transparency. Deliver a low-latency scoring service with batch and on-demand modes, backed by tests and monitoring.

Acceptance Criteria
Geospatial-Temporal Match Determination
Given tenant config distanceThresholdKm=5, travelTimeThresholdMin=20, and minTemporalOverlap=0.25 And a class at location A scheduled 18:00–19:00 local And an event at location B 3.2 km away with travel time 12 min scheduled 17:30–18:30 When the matching service evaluates the pair Then match=true And overlapRatio=0.5 ± 0.01 And reasonCodes includes ["PROXIMITY_MATCH","TEMPORAL_OVERLAP"] And confidence >= 0.70 Given the same class and an event 8.1 km away with travel time 28 min scheduled 17:00–17:20 When the matching service evaluates the pair Then match=false And reasonCodes includes at least one of ["DISTANCE_EXCEEDED","TRAVEL_TIME_EXCEEDED","NO_OVERLAP"] And confidence <= 0.40
Travel-Time Estimation & Fallback Behavior
Given routing API is available and mode=driving at 18:00 local for locations A and B When the service computes travelTimeMin Then travelTimeMin matches API ETA within ±10% And reasonCodes includes "TRAVEL_TIME_API" Given routing API returns a timeout after 500 ms (configured) When the service computes travelTimeMin Then the service falls back to haversine distance with speedProfile=30 km/h ±10% And reasonCodes includes "TRAVEL_TIME_FALLBACK" And a metric travel_time_fallback_count increments by 1
Impact Scoring with Distance Decay and Event Scale
Given scoring config with distanceDecay enabled and normalized impactScore range [0,1] And two matched pairs identical except for distance: d1=0.5 km, d2=5 km; eventScale constant=5,000 attendees When scores are computed Then impactScore(d1) >= impactScore(d2) + 0.20 And reasonCodes includes ["DISTANCE_DECAY","EVENT_SCALE"] Given two matched pairs identical except for eventScale: s1=1,000 attendees, s2=10,000 attendees; distance constant=2 km When scores are computed Then impactScore(s2) >= impactScore(s1) + 0.10 And all impactScore values are within [0,1]
Category, Day-of-Week, and Historical Correlation Adjustments
Given tenant T has historical correlation config: school_break → +30% uplift for kids classes on weekdays; marathon → −40% for driving-dependent classes on Sundays When scoring a kids class on a Wednesday matched to a school_break event Then a categoryAdjustment between +25% and +35% is applied to the base score And reasonCodes includes ["CATEGORY_HISTORICAL_CORRELATION","DOW_EFFECT"] When scoring a driving-dependent class on a Sunday matched to a marathon event within 1 km Then a categoryAdjustment between −35% and −45% is applied to the base score And reasonCodes includes ["CATEGORY_HISTORICAL_CORRELATION","DOW_EFFECT"]
Lead-Time Sensitivity in Impact Score
Given leadTimeWeighting enabled with stronger weighting for lead time < 72 hours And a matched uplift-category event occurring in 2 days vs 30 days (all else equal) When scores are computed Then impactScore(2 days) >= impactScore(30 days) + 0.15 And reasonCodes includes "LEAD_TIME_EFFECT" Given a matched suppression-category event (e.g., storm) occurring in 6 hours vs 72 hours (all else equal) When scores are computed Then suppression magnitude at 6 hours is at least 30% stronger than at 72 hours (impactScore difference ≥ 0.30 in the suppression direction) And reasonCodes includes "LEAD_TIME_EFFECT"
Per-Tenant Thresholds, Reason Codes, and Confidence Output
Given tenant T config: minImpactScoreForFlag=0.35 and minConfidence=0.60 When the service returns a result for any matched pair Then response includes fields: matched (boolean), impactScore (0–1), confidence (0–1), reasonCodes (array ≥1), thresholdsApplied (object) And flag=true only if impactScore ≥ 0.35 AND confidence ≥ 0.60; otherwise flag=false And for identical inputs under the same config across 5 repeated calls, variance(impactScore) = 0 and variance(confidence) = 0
On-Demand and Batch Scoring SLAs, Monitoring & Alerts
Given the on-demand API with warm cache When processing 10,000 valid requests sequentially Then p50 latency ≤ 60 ms, p95 ≤ 150 ms, p99 ≤ 300 ms, and HTTP error rate < 0.1% And metrics emitted include: request_count, latency_ms (p50/p95/p99), error_count, match_rate, avg_impact_score, reason_code_distribution And SLO alerts exist: 5-min p95 latency > 150 ms pages on-call; 10-min error rate > 1% pages on-call Given a batch job of 1,000 classes × 500 events (500,000 pairs) on a worker pool of size 20 When executed Then job completes within 10 minutes wall-clock, throughput ≥ 50,000 pairs/min, and emits processed/succeeded/failed/retried counts And failed items are retried up to 3 times with exponential backoff and un-recovered items are written to a dead-letter queue
Forecast Adjustment & Confidence Band Tuning
"As a growth analyst, I want forecasts that reflect local events so that our staffing and capacity decisions align with real-world demand patterns."
Description

Integrate event impact scores into the existing demand forecasting service to adjust point forecasts and widen or tighten confidence intervals accordingly. Apply uplift/suppression factors with guardrails (caps, minimum confidence) and produce backtestable outputs. Log attribution per adjustment, support fallback to baseline forecasts when signals are weak, and expose an API returning adjusted forecasts plus variance bands for each class slot. Provide offline evaluation scripts and dashboards to track accuracy and the incremental value of EventAware adjustments.

Acceptance Criteria
API Returns Adjusted Forecasts with Confidence Bands
Given a set of class slots with baseline forecasts and event impact scores And the API is called with confidence_level=0.95 and a valid time horizon When the request is processed Then the response includes for each slot: slot_id, baseline, adjusted_point, lower, upper, variance, confidence_level, model_version, generated_at, request_id And lower <= adjusted_point <= upper And band_width = upper - lower >= min_relative_band_width * baseline and <= max_relative_band_width * baseline (per current config) And all numeric values are non-negative and rounded to the configured precision (default 2 decimals) And the HTTP status is 200 and p95 latency <= 400ms for requests with <= 100 slots
Guardrails on Uplift/Suppression Factors
Given configuration uplift_cap=0.5, suppression_cap=0.5, min_relative_band_width=0.1, max_relative_band_width=1.0 When an event impact implies +0.8 uplift Then the applied_adjustment is capped at +0.5 and guardrail_applied=true with reason="uplift_cap" When an event impact implies -0.7 suppression Then the applied_adjustment is capped at -0.5 and guardrail_applied=true with reason="suppression_cap" And the confidence band width respects min_relative_band_width and max_relative_band_width And per-slot guardrail metadata is included in the API response and in logs
Weak-Signal Fallback to Baseline
Given signal_strength_threshold=0.2 and impact_magnitude_threshold=0.05 (configurable) And a slot where aggregate_event_confidence < 0.2 or abs(impact_score) < 0.05 When the API processes the slot Then adjusted_point == baseline and used_baseline=true and fallback_reason in ["weak_signal","low_confidence"] And confidence bands equal the baseline bands (no widening/tightening) And attribution shows an empty adjustments list
Attribution Logging and Traceability
Given adjustments are applied to one or more slots When processing completes Then for each slot a structured log exists with correlation_id=request_id including: slot_id, baseline, adjusted_point, events:[{event_id,type,start,end,source,raw_score,weight,applied_factor,capped:boolean,cap_type}], guardrails:[{type,value}], config_version, model_version, latency_ms And logs are persisted with retention >= 90 days and queryable by slot_id and date range And logging sampling is 100% in non-prod and >=10% in prod (configurable) And PII is excluded or redacted per policy
Offline Backtesting and Accuracy Dashboard
Given historical bookings and archived event signals for a selected date range When the backtest CLI is executed comparing baseline vs EventAware-adjusted forecasts Then it produces metrics: MAE, MAPE, RMSE, coverage for 50%/90%/95% bands, WIS, and uplift_vs_baseline for both point and interval forecasts, plus a paired t-test or bootstrap significance result And artifacts (metrics.json, plots, config snapshot) are written to a timestamped folder and published to a dashboard with filters by location, class_type, and event_type And runs are reproducible: same inputs and seed yield identical metrics And the CLI exits non-zero if data integrity checks fail (missing events, leakage, or timezone mismatches)
API Contract, Versioning, and Error Handling
Given an OpenAPI 3.0 spec exposed at /openapi.json with version "v1" When clients send invalid inputs (e.g., missing slot_id, malformed dates, unsupported confidence_level) Then the API returns 400 for validation errors, 422 for semantic errors, 429 with Retry-After for rate limits, and 500 with a request_id for unexpected errors And default rate limit is 100 RPS per API key; responses include standard error codes and machine-readable messages And minor changes are backward-compatible; breaking changes require a version increment to v2 with side-by-side support And p99 latency <= 800ms for requests up to 1000 slots and monthly availability >= 99.9%
Publishing Guardrails & Safer Alternatives
"As an instructor, I want to be warned about risky class hours and be offered better alternatives so that I can avoid low attendance and cancellations."
Description

Intercept schedule publishing (dashboard and API) to flag risky class times based on impact scores and confidence, showing clear reason codes (e.g., weather alert, road closures, school break). Generate and rank safer alternative time slots that respect instructor availability, room capacity, existing bookings, and travel buffers. Provide one-click replacement or bulk-apply, plus the ability to override with acknowledgment. Maintain low-latency evaluations to keep the publish flow responsive and record accepted/ignored suggestions for learning.

Acceptance Criteria
Dashboard Publish Intercept with Risk Flags and Override
Given a dashboard user attempts to publish a class with impact_score >= 0.70 and confidence >= 0.60, When they click Publish, Then the publish is intercepted and a modal appears showing at least one reason_code with human-readable descriptions and sources. Given multiple reasons exist, When the modal renders, Then reasons are sorted by descending impact contribution and capped at 5 items. Given the modal is open, When the user checks an acknowledgment checkbox and clicks Override & Publish, Then the class is published without schedule change and an audit record captures user_id, timestamp, impact_score, confidence, reason_codes, and acknowledgment_text. Given the modal is open, When the user closes it without override or alternative selection, Then no publish occurs and the class remains in Draft.
Ranked Safer Alternatives Respecting Constraints
Given a risky class time is intercepted, When alternatives are requested, Then at least 3 candidates are returned within the configurable search window (default ±72h) if feasible, else all feasible candidates. Then each candidate satisfies: instructor availability windows, room capacity >= class capacity, no conflicts with existing bookings, and travel buffers of at least 30 minutes before/after adjacent commitments for both instructor and room. Then candidates are ranked by ascending risk_score; ties broken by proximity to original start time, then historical fill rate for that hour/day. Then each candidate includes start, end, risk_score, confidence, reason_summaries, and an expected_attendance_delta estimate. Given a candidate is selected and confirmed, Then the class start/end is updated, enrollments are preserved, reminders rescheduled, and no double-bookings are introduced.
Bulk Apply and Single-Click Replacement
Given N flagged classes in the current publish batch, When the user selects Apply Top Alternative and confirms, Then the system previews the proposed replacements and applies them in one operation. Then the operation is atomic by default: either all replacements succeed or none; if allow_partial=true, only failed items are skipped with reasons. Then the result summary shows counts for replaced, overridden, skipped (with reason), and total notification updates queued. Then all affected waitlists, SMS/email reminders, and calendar entries are updated within 60 seconds; no duplicate notifications are sent. Then any conflict detected during apply aborts that item with a specific error (e.g., capacity_conflict, buffer_violation) and leaves it unchanged.
API Publish Intercept and Response Contract
Given a client POSTs /v1/classes/publish containing one or more risky class times, When evaluated, Then the API responds 409 Conflict with a payload per class_id including: impact_score, confidence, reason_codes[], and alternatives[] (start, end, risk_score, confidence, is_top). Given the same request is retried with an identical Idempotency-Key, Then the response is identical byte-for-byte. Given the client resubmits with override=true and acknowledgment (non-empty string), Then the API publishes those classes and returns 201 Created with warnings[] for each overridden item. Given no risky times are detected, Then the API returns 201 Created and publishes normally. Then all timestamps are ISO 8601 with timezone offsets; all numeric scores are 0.00–1.00 inclusive.
Low-Latency Guardrail Evaluation SLA
Given a dashboard publish action triggers evaluation, When the decision UI appears, Then time-to-first-decision is <= 500ms P95 and <= 900ms P99 measured server-side; client total blocking time adds <= 100ms P95. Given an API publish request with up to 50 classes, When evaluation runs, Then end-to-end latency (request to response) is <= 700ms P95 and <= 1200ms P99. Given repeated evaluation of the same class time within 10 minutes, When caching is in effect, Then cache hit rate is >= 80% and median response <= 200ms. Then performance SLAs are observable via metrics dashboards with alerts firing if P95 exceeds SLA for 5 consecutive minutes.
Graceful Degradation on Event Signal Failures
Given one or more event-signal providers fail or exceed a 300ms fetch timeout, When evaluation runs, Then evaluation_status="degraded" is set, missing_providers are listed, and confidence is reduced by at least 0.20 (not below 0.00). Then alternatives generation proceeds using available signals; if no signals are available, Then alternatives[] is empty with reason="insufficient_data" and the override path remains enabled. Then the UI/API presents a clear notice of degraded signals and allows proceed with override. Then provider failures are logged with provider_id, error_type, duration_ms, and correlation_id.
Decision Logging and Learning Telemetry
Given any intercepted publish results in an action (alternative applied, override, or cancel), When the action completes, Then a decision event is recorded with: decision_id, class_id, original_start, chosen_action, selected_alternative_start (if any), user_id or api_client_id, source (dashboard/api), reasons_shown[], impact_score, confidence, latency_ms, bulk_operation_id (if any), and timestamp (ISO 8601). Then events are delivered to the analytics stream with at-least-once semantics and deduplicated by decision_id; 99.9% of events are available for querying within 2 minutes. Then an admin report endpoint /v1/reports/eventaware/decisions supports filtering by date range, action, and source and returns accept_rate and override_rate that reconcile within ±1% of raw event counts. Then all stored telemetry complies with data retention and PII policies, masking email/phone by default in exports.
Event Overlay & Explainability UI
"As a scheduler, I want a clear visual overlay of events and their effects so that I can quickly understand why a time slot is flagged and decide what to do."
Description

Enhance the calendar and forecasting views with an event overlay that visualizes nearby events, predicted impact intensity, and time windows. Offer filters by event type and severity, tooltips with source and distance, and a before/after forecast preview for each class slot. Provide an adjustable sensitivity control and a what-if sandbox to compare schedule options. Ensure responsive, accessible UI that localizes timezones and formats, includes loading/error states, and surfaces links to original sources for verification.

Acceptance Criteria
Calendar Overlay: Local Events and Impact Heat
Given I am viewing the calendar in Day or Week view with a venue location configured and EventAware enabled When I toggle the Event Overlay on Then events within the configured detection radius and time horizon are visualized aligned to their start and end windows on the timeline And each event displays predicted impact intensity using a color scale with a visible legend And overlapping events aggregate intensity without exceeding the maximum scale And class slots falling within event windows display a risk badge with the highest impacting event type When I toggle the Event Overlay off Then all event visuals and risk badges are removed and my toggle preference is persisted for my user
Filter Controls: Event Type and Severity
Given the Event Overlay is on and the filter panel is open When I multi-select event types (e.g., School Breaks, Holidays, Sports, Weather Alerts) and set a minimum severity threshold Then only events matching the selected types and at or above the threshold remain visible And the calendar refreshes the overlay within 300 ms of my last change And a No matching events state appears when no events meet the filter And my last-used filter selections persist for my user across sessions
Tooltips: Source, Distance, and Details
Given an event chip has pointer hover or keyboard focus When the tooltip opens Then it shows event name, type, start and end times localized to the venue timezone and formatted to my locale, predicted impact range (%), confidence level, and distance from venue in km/mi And it includes the source name and a View source link And activating the source link opens it in a new tab with tracking parameters and does not navigate away from ClassTap And the tooltip supports keyboard open/close (Enter/Escape), is focusable, and conveys content to screen readers with appropriate ARIA roles/labels
Forecast Preview: Before/After per Class Slot
Given a class slot is selected on the calendar When I open Event Impact Preview for that slot Then I see baseline forecast versus event-adjusted forecast with upper and lower confidence bands And I see numerical deltas for projected attendance, revenue, and no-show risk And I can Accept adjustment for this class or Dismiss without changes And no schedule or pricing changes are committed until I explicitly Save changes And closing the preview restores the prior calendar state
Sensitivity Control: Adjust Impact and Flagging
Given a sensitivity slider with presets (Low, Medium, High) is visible When I adjust the sensitivity value or select a preset Then the number of flagged class slots and the impact intensity on the overlay update in real time And the Event Impact Preview recalculates adjusted forecasts using the selected sensitivity And my selected sensitivity persists per user and can be reset to system default
What‑If Sandbox: Compare Schedule Options
Given I enter What‑If mode from the calendar When I duplicate a class or drag it to an alternative time Then a comparison panel shows side-by-side metrics for each option (attendance, revenue, no-show risk) with the recommended option highlighted And I can Commit selected option to the live schedule or Discard all sandbox changes And sandbox changes are non-destructive and clearly labeled until committed And exiting What‑If mode restores the original schedule if no commit was made
Responsiveness, Accessibility, Localization, and States
Given I use a phone (>=360px width), tablet, or desktop Then the overlay, filters, tooltips, and previews are usable with touch and keyboard, maintain readable layout, and interactions render at or above 60 FPS during scroll and drag Given my user locale and the venue timezone may differ Then event times display in venue timezone with my local time available on hover/focus, and date/number formats follow my locale Given data is loading Then skeleton loaders appear within 200 ms and are replaced by content when ready Given an error occurs fetching events or sources Then a non-blocking error banner shows a human-readable message, a retry action, and a diagnostic code, and retry refreshes only the overlay data And each event includes a link to the original source for verification And all interactive elements meet WCAG 2.1 AA for contrast, focus order, and keyboard operability
Event-Aware Notifications & Reminders
"As a participant, I want timely reminders that account for local disruptions so that I arrive prepared and on time."
Description

Modify SMS/email reminder timings and content when impactful events are present (e.g., send earlier reminders before road closures or storms, include travel advisories or parking notes). Respect user communication preferences and compliance (opt-outs, regional regulations). Automatically update reminders if a class is rescheduled due to EventAware suggestions, include dynamic tokens referencing relevant events, and provide preview/testing tools. Track delivery and engagement metrics to assess effectiveness.

Acceptance Criteria
Event-adaptive reminder timing
Given a class is scheduled at T0 with configured reminders at T0-24h and T0-3h And EventAware flags a high-impact event within 5 miles overlapping the travel window (T0-3h to T0+1h) When reminders are scheduled Then the first reminder is moved to T0-36h and the second to T0-15h, without sending before the attendee’s booking timestamp And no attendee receives more than one reminder within any rolling 6-hour window And a change log entry records the original and adjusted times per attendee
Contextual advisory content injection
Given EventAware identifies a road closure affecting the venue within 5 miles during the travel window When an SMS reminder is generated Then the SMS includes an advisory block containing {event_name}, {impact_summary} (<=120 chars), and a shortened URL And the total SMS length is <=320 characters unless the organization setting "allow_long_sms" is true And the email reminder includes the advisory block in the header with a warning icon and the same tokens And if multiple events exist, the highest-impact event is included and others summarized as "+N more events" with a link to details
Compliance and communication preferences
Given an attendee has opted out of SMS but allowed email When reminders are dispatched Then no SMS is sent and email is sent for that attendee Given an attendee replied STOP to a prior SMS When future reminders are queued Then SMS reminders are suppressed across all classes within 60 seconds of the STOP event And all SMS include compliant opt-out language for the attendee’s region And reminders are not sent during configured regional quiet hours; if a reminder falls inside quiet hours, it is deferred to the next allowed window before class and the deferral is logged
Auto-update and cancellation on EventAware-driven reschedule
Given EventAware proposes a reschedule and the organizer accepts, changing start time from 18:00 to 17:00 local And attendees have pending reminders for the original start time When the reschedule is saved Then all pending reminders tied to the original start time are canceled And new reminders are scheduled relative to the new start time using event-aware adjustments And attendees with a reminder due within the next 6 hours receive a one-time time-change notification via their opted-in channels And an immutable audit log records cancellations and new schedules with timestamps and actor
Dynamic event tokens with resilient fallbacks
Given a reminder template includes tokens {{event_name}}, {{event_window}}, {{advisory_text}}, {{parking_note}}, {{weather_severity}}, and {{event_count}} When an impactful event exists for the class Then all tokens are populated with localized values and rendered without placeholders And if any token value is unavailable, the token is omitted cleanly with no dangling braces or extra whitespace And if no impactful events exist, the EventAware advisory section is not rendered at all And if multiple events exist, tokens resolve to the highest-impact event and {{event_count}} reflects the total number of detected events
Preview and test-send tools
Given an organizer opens the reminder preview for a class with an EventAware event detected When viewing SMS and email previews Then token substitution is shown, SMS character and segment counts are displayed, and scheduled send times reflect event-adjusted timing And clicking "Send Test" delivers to verified test recipients only, prefixes subjects/bodies with "[TEST]", and does not send to live attendees And the preview allows selecting which event to highlight when multiple events are detected
Delivery and engagement effectiveness tracking
Given reminders are sent with and without EventAware advisories When delivery and engagement data are collected Then per message the system records delivery status, bounce reason (if any), email opens, link clicks, SMS replies, and opt-out events And dashboards and exports/API allow filtering by class, date range, channel (SMS/email), and EventAware flag (Yes/No) And metrics become visible within 60 seconds of provider webhook receipt and are retained for at least 90 days And the system computes comparative metrics (e.g., open/click/confirmation uplift) between EventAware and non-EventAware reminders
Admin Controls, Overrides & Audit
"As an account admin, I want to configure EventAware to our context and override edge cases so that we maintain control while benefiting from automation."
Description

Provide tenant-level settings to enable/disable specific data sources, configure sensitivity thresholds, catchment radius, blackout dates, and event categories. Allow manual event creation/edits and per-event or per-class overrides of impact scores with expiration. Gate actions with role-based permissions and write an immutable audit trail of ingestions, score changes, overrides, publish decisions, and accepted suggestions to support supportability and compliance.

Acceptance Criteria
Tenant Data Source Toggles
Given I am a Tenant Admin for tenant T When I navigate to Admin > EventAware > Data Sources and disable "Weather Alerts" and enable "School Breaks" and "Holidays" And I save changes Then subsequent ingestion runs for tenant T only create/update events from enabled sources And no new events from "Weather Alerts" are created after the saved-at timestamp And the UI shows the effective source state with the actor and timestamp And an audit entry is appended capturing old/new source sets, actor, tenant, and timestamp
Configure Sensitivity Thresholds and Catchment Radius
Given I am a Tenant Admin for tenant T When I set Risk Flag Threshold to 75 (valid range 0–100) and Catchment Radius to 12 with unit = km and save Then validation enforces bounds and required unit selection and prevents save if invalid And the saved values are applied to the next scoring job for tenant T And only events within 12 km of tenant locations are considered in impact scoring; events beyond are excluded And risk flags only trigger when calculated impact >= 75 And an audit entry is appended with old/new values, actor, tenant, and timestamp
Manage Blackout Dates and Event Categories
Given I am a Tenant Admin for tenant T When I add a blackout window from 2025-10-31 00:00 to 2025-11-01 23:59 in tenant timezone and select categories = [Parade, Marathon] Then any publish action overlapping the blackout window is blocked with a clear message referencing the blackout And suggested safer times do not fall within the blackout window And during the blackout window, events in selected categories are excluded from demand adjustments And the blackout window appears on the EventAware calendar with label and scope And an audit entry is appended with window, categories, actor, and timestamp
Manual Event Creation and Editing
Given I am a Tenant Admin with permission Manage Events When I create a manual event with title, start, end, location, category, and impact score Then the event is saved with source = Manual and participates in scoring for affected classes within the catchment radius And editing any field creates a new audit entry with old/new values And if a potential duplicate external event exists (same start ±15 min, same location, same category), I am warned and can link or proceed And deleting the event requires confirmation and appends a tombstone audit record with reason
Per-Event/Class Impact Override with Expiration
Given an event E influences class C When I apply an override impact score = 20% for class C only with expiration = 2025-12-31 23:59 UTC and a reason Then risk calculations and publish checks for class C use 20% until expiration And after expiration the system reverts to the calculated impact automatically at the next scoring cycle And precedence is enforced: per-class override > per-event override > calculated score And the UI shows an Override badge with expiry details on class C And all changes and the automatic expiry are appended to the audit trail
Role-Based Permissions for Admin Controls and Overrides
Given roles exist: Tenant Admin, Scheduler, Instructor, Viewer When users with each role access Admin > EventAware > Settings and Overrides Then Tenant Admin can modify all settings, manual events, overrides, and export audit logs And Scheduler can create per-class overrides and view settings but cannot change tenant-level defaults or data source toggles And Instructor can view impacts on their own classes and request (but not apply) overrides And Viewer can only view And unauthorized actions return HTTP 403 via API and disabled controls in UI And role changes take effect on the next request and are logged in the audit trail
Immutable Audit Trail Coverage and Integrity
Given auditing is enabled for tenant T When any of the following occurs: ingestion, scoring parameter change, manual event create/edit/delete, override create/edit/expire/delete, publish decision, suggestion accepted/declined Then an audit record is appended with tenantId, actor (or system), action type, entity id, UTC timestamp, old/new values (if applicable), reason/comment, and requestId And audit records are append-only; attempts to modify or delete are blocked and logged And the audit viewer supports filtering by time range and action type and export to CSV/JSON with configured PII redaction And the audit API is accessible only to Tenant Admin and enforces pagination and rate limits And a daily integrity check verifies audit continuity and reports anomalies to Tenant Admin

MicroShift

Small start-time tweaks, big impact. MicroShift recommends 5–20 minute adjustments to align with school drop-offs, transit arrivals, and neighboring class end times. Preview the expected lift in on-time arrivals and reduced no-shows before you commit.

Requirements

Context-Aware Data Feeds
"As a studio owner, I want ClassTap to consider nearby transit and school schedules so that recommended start times match when my clients can actually arrive."
Description

Implements connectors to ingest and normalize local time-based signals—public transit GTFS arrivals near each venue, school drop-off/pick-up windows by district, and adjacent ClassTap class end times within the same location—mapped to venue geofences and time zones. Provides a resilient availability signal service with configurable refresh cadence, caching, and graceful degradation when external feeds are stale, enabling MicroShift to generate reliable recommendations without blocking core scheduling flows.

Acceptance Criteria
GTFS Arrivals Ingestion and Normalization within Venue Geofence
Given a venue with a defined geofence polygon and time zone And configured GTFS static and real-time feed endpoints (with credentials where required) When the ingestion job runs at its configured cadence Then the service fetches GTFS data within the configured timeout per feed And retains only stop-times whose stop coordinates intersect the venue geofence And normalizes records into the internal schema: stop_id, stop_name, route_id, direction_id, arrival_time_local, arrival_time_utc, delay_seconds, source_timestamp, source_name And stores timestamps in both UTC and the venue's local time with offset And marks the snapshot with freshness_age <= the configured staleness threshold And exposes the normalized arrivals for the venue via the Availability Signal API
School Drop-off/Pick-up Windows Mapped to Venues
Given a venue with latitude/longitude falling within one or more school district boundaries And a configured provider for district bell schedules When the daily refresh runs Then the service resolves the venue to a single district using configured priority or nearest-boundary rules And ingests drop-off and pick-up windows for the next configured horizon (default 14 days) And normalizes windows to start_time_local, end_time_local, start_time_utc, end_time_utc, district_id, source_timestamp And exposes the windows via the Availability Signal API for the venue And if no provider data is available, returns last-known-good windows flagged stale=true with staleness_age populated
Adjacent Class End Times Signal per Location
Given a location with multiple scheduled classes in ClassTap When the adjacent-class signal refresh runs Then the service collects end times for all classes at the same location_id excluding the target class And aggregates end events into 5-minute buckets within a ±60-minute window of any requested time And reflects edits/cancellations within 60 seconds of change And de-duplicates multi-session or recurring instances referencing the same physical end time And exposes counts and precise end timestamps via the Availability Signal API
Availability Signal Service Caching and TTL
Given the Availability Signal API is queried for a venue When a cached, non-expired snapshot exists for the requested signal type Then the API responds with p95 latency <= 200 ms And cache TTLs default to: GTFS <= 2 minutes, Adjacent Classes <= 1 minute, School Windows <= 24 hours, and are configurable per deployment And on cache miss the service serves the latest persisted snapshot and backfills the cache And cache invalidation occurs immediately upon successful refresh
Graceful Degradation on Stale or Failed External Feeds
Given an external feed request results in timeout, 4xx/5xx, or data older than the configured staleness threshold When MicroShift or other clients request availability signals Then the API returns HTTP 200 with last-known-good snapshot where available And includes stale=true, staleness_age (seconds), and a confidence_score per signal And omits only the unavailable signal while continuing to serve other signals And core scheduling endpoints remain non-blocking and unaffected by feed errors And a health event is emitted for monitoring with feed name, error type, and first_seen timestamp
Configurable Refresh Cadence per Feed Type and Venue
Given an operator updates the refresh cadence for a feed type globally or overrides it for a specific venue When the configuration is saved Then the scheduler applies the new cadence within 60 seconds And respects provider rate limits using exponential backoff and jitter on 429/5xx And enforces at most one concurrent refresh per venue per feed type And precomputes school windows for the configured horizon (default 14 days) at each daily run
Time Zone and DST Normalization Across Signals
Given a venue in a time zone that observes daylight saving time When a DST transition occurs Then all signal timestamps (GTFS arrivals, school windows, adjacent class end times) returned for that venue are correct in local time and UTC with explicit offsets And no duplicated or missing windows are returned during the transition hour And cross-venue queries return each venue's local times alongside UTC consistently
MicroShift Optimization Engine
"As an instructor, I want smart start-time tweaks that won’t create conflicts so that I can improve punctuality without manual trial and error."
Description

Delivers a rules- and model-driven engine that proposes 5–20 minute start-time adjustments for eligible classes, respecting instructor and room availability, buffer policies, existing reservations, and blackout constraints. Produces a ranked set of candidates with predicted impact on on-time arrivals and no-shows, including confidence intervals and rationale codes, and exposes APIs for the UI to fetch recommendations, validate constraints, and commit selected changes.

Acceptance Criteria
Ranked MicroShift Recommendations for a Single Class Session
Given an upcoming class session with a scheduled start time, When the engine generates MicroShift recommendations, Then it returns a list of candidate start times each offset by between 5 and 20 minutes (inclusive) earlier or later than the original start time, and no two candidates share the same new start time. Given the list of candidates, When the engine returns them, Then the list is sorted in descending order by a numeric score representing predicted impact, and the first item has the highest score. Given each returned candidate, When examining its payload, Then it includes fields new_start_time (ISO 8601), offset_minutes (integer 5–20), direction ("earlier" or "later"), score (number), predicted_on_time_arrival_lift (number), predicted_no_show_reduction (number), confidence_interval (object), and rationale_codes (array of strings).
Hard Constraint Compliance for Recommendations
Given instructor availability, room availability, buffer policies, existing reservations, and blackout constraints, When MicroShift candidates are produced, Then no candidate violates any of these constraints and no candidate causes overlapping bookings or buffer violations for the instructor or the room. Given the class session attributes, When candidates are generated, Then class duration, capacity, and instructor assignment remain unchanged for all candidates. Given any hypothetical candidate that would violate a constraint, When the engine evaluates it, Then that candidate is excluded from the returned results.
Predicted Impact and Confidence Intervals Included
Given a candidate recommendation, When it is returned by the engine, Then it includes predicted_on_time_arrival_lift and predicted_no_show_reduction as numeric values relative to the current schedule baseline. Given each prediction, When validating the payload, Then it includes a confidence_interval with lower and upper numeric bounds and a level field, and lower is less than or equal to upper. Given multiple candidates in a response, When comparing prediction fields, Then units and polarity are consistent across candidates and positive values indicate improvement.
Rationale Codes Provided per Candidate
Given a candidate recommendation, When it is returned by the engine, Then it includes at least one rationale code from the controlled set {align_school_dropoff, align_transit_arrival, align_neighbor_end, demand_peak_alignment, blackout_avoidance, buffer_compliance, instructor_availability, room_availability}. Given the rationale codes array, When validating its contents, Then each code is a lowercase snake_case string and no unknown codes are present. Given a candidate’s score, When tracing its inputs, Then each rationale code maps deterministically to an observable factor used in scoring for that candidate.
Recommendations Fetch API
Given an authenticated request with a valid class_id to GET /microshift/v1/recommendations, When the class exists and is eligible, Then the API responds 200 with a JSON body containing a non-null candidates array sorted by score and baseline metrics for the current schedule. Given a request for a non-existent class_id, When the API is called, Then it responds 404 with an error code and message. Given a class that is ineligible or yields no valid candidates, When the API is called, Then it responds 200 with candidates: [] and a non-empty reasons array explaining ineligibility or emptiness. Given an unauthenticated request, When the API is called, Then it responds 401 and no recommendation data is returned.
Validate and Commit Selected MicroShift
Given a selected candidate new_start_time and class_id, When POST /microshift/v1/commit is called with an idempotency key, Then the engine revalidates all constraints at commit time and updates the class start time atomically if still valid. Given a successful commit, When the API responds, Then it returns 200 with the updated class record, committed_change_id, and the actual new_start_time, and adjacent buffer compliance is preserved. Given a new conflict detected during commit (e.g., intervening booking), When the API processes the request, Then it applies no changes and responds 409 with a conflict reason code. Given a retried commit with the same idempotency key, When the API processes the request, Then it returns the original result without duplicating the change.
Deterministic Ranking and Reproducibility
Given identical inputs (class context, constraints, and data snapshot) and the same model_version, When the engine generates recommendations multiple times, Then the ranking order, scores, predictions, and rationale codes are identical across runs. Given a recommendation response, When inspecting metadata, Then it includes model_version and a run_id to support audit and comparison across versions. Given a change in model_version, When comparing responses across versions, Then differences in scores or ordering are attributable to the version change and documented via the included metadata.
Impact Preview & Simulation UI
"As a studio manager, I want to preview the impact of a suggested shift so that I can choose the option with the biggest upside and least disruption."
Description

Adds an interactive preview panel on schedule and class edit screens to compare baseline versus proposed start times, visualizing expected lift in on-time arrivals, reduced no-shows, affected attendees, and any conflicts. Supports side-by-side alternative comparison, risk warnings for low-confidence predictions, and mobile-responsive, accessible controls to aid quick, informed decision-making before committing a shift.

Acceptance Criteria
Preview Panel and Baseline vs Proposed Metrics
Given I am on the schedule or class edit screen for a class with a defined location and instructor When I adjust the proposed start time by between 5 and 20 minutes from the current start time Then an Impact Preview panel becomes visible without navigating away And it displays side-by-side Baseline vs Proposed metrics including: predicted on-time arrival rate (%), predicted no-shows (% and count), affected attendees (count), and detected conflicts (count) And percentages are rounded to one decimal place and counts are whole numbers And the panel shows a Last updated timestamp
Compare Up to Three Alternative Start Times
Given a class supports MicroShift adjustments of 5–20 minutes When I add alternative start times within ±20 minutes of the current start time (maximum of 3 alternatives) Then up to 3 alternatives are displayed side-by-side with their metrics And alternatives outside the 5–20 minute adjustment range are rejected with inline validation messaging And I can set one alternative as the selected proposal
Low-Confidence Prediction Warning
Given predictions include a confidence score per alternative When the confidence score for any displayed alternative is below 0.70 or the model has fewer than 6 comparable past sessions Then the preview shows a low-confidence warning with an icon and label on that alternative And a tooltip or info link reveals the drivers of low confidence And the Commit action remains enabled
Conflict Detection and Commit Guardrail
Given the proposed start time overlaps a class using the same room or instructor or violates resource availability When the preview is generated Then the Conflicts metric shows the count of blocking conflicts And a View conflicts action lists each conflicting item with time, resource, and a link to open it And the Commit button is disabled while conflicts > 0 and displays a tooltip explaining why
Attendee Impact and Notification Summary
Given the class has current bookings and/or waitlisted attendees When an alternative start time is previewed Then the preview displays the count of affected booked attendees and waitlisted attendees separately And the commit confirmation dialog summarizes how many attendees will receive notifications if the change is committed
Non-Persistence Until Commit
Given I have previewed one or more alternatives When I navigate away, close the panel, or click Cancel without committing Then no schedule changes are saved and no attendee notifications are sent And if unsaved changes exist, I am prompted with a discard-confirmation dialog And when I click Commit, the change is saved, the chosen start time is applied, and an audit log entry records old time, new time, user, and prediction snapshot
Mobile-Responsive and Accessible Controls
Given I use the preview on a mobile device at 320px width or larger When viewing the Impact Preview and comparison controls Then the layout adapts to a single-column stack on small screens and side-by-side at ≥768px And all interactive targets are at least 44x44px, keyboard navigable, and have visible focus And color contrast meets WCAG 2.1 AA and all metrics, warnings, and buttons have accessible names/labels announced by screen readers
Safe Rescheduling & Notifications
"As a coordinator, I want MicroShift changes to be applied safely with automatic notifications so that attendees aren’t surprised and operations stay conflict-free."
Description

Implements a one-click commit workflow that applies the selected MicroShift, updates internal calendars, enforces conflict checks to prevent double-bookings, and triggers branded SMS/email notifications with clear change details and updated calendar attachments for attendees and instructors. Includes configurable lead-time thresholds, automatic fee waivers for rescheduled sessions, waitlist revalidation, and a reversible rollback within a defined window to minimize operational risk.

Acceptance Criteria
One-Click Commit With Conflict Prevention
Given a session has a selected MicroShift and no hard conflicts exist for instructor, room, or linked resources at the proposed time And the change is outside the hard lead-time block When the organizer clicks Commit MicroShift Then the session start and end times are updated atomically in the internal calendar And no double-booking is introduced for any protected resource And the operation completes in <= 3 seconds at the 95th percentile And an audit log entry is created capturing user, timestamp, old/new times, reason, and conflict-check results
Branded Notifications With Updated Calendar Attachments
Given a MicroShift commit succeeds When notifications are sent Then emails to attendees and instructors use the organization’s branding (logo, colors, from-name) And the body clearly states old time → new time, date, timezone, location, class title, and instructor name And each email includes a single .ics attachment that updates the existing event using the same UID with SEQUENCE incremented And SMS (only to opted-in recipients) includes a concise change summary and a link to the event details and downloadable calendar file And notifications are dispatched within 2 minutes of commit And any pre-scheduled reminders are rescheduled to align with the new start time, avoiding duplicate sends
Lead-Time Threshold Enforcement
Given lead-time thresholds are configured with values for hard_block_hours and soft_warn_hours When attempting to commit a MicroShift that moves the start time Then if the new start is within hard_block_hours, the commit is blocked with an explanatory error and no changes are persisted And if within soft_warn_hours but outside hard_block_hours, a confirmation modal is shown and the commit proceeds only after explicit confirmation And if outside soft_warn_hours, the commit proceeds without warning And threshold overrides at the class template level are honored And all outcomes are recorded in the audit log
Automatic Fee Waivers On Reschedule
Given a session has been rescheduled via MicroShift When an attendee cancels within the late-cancel window or is marked no-show for that rescheduled instance Then all late-cancel and no-show penalties are automatically waived for that instance And any penalty pre-authorizations are voided within 15 minutes And waiver flags are visible on the booking record and exportable in payout/finance reports And attendees are informed of the waiver in the reschedule and reminder communications
Waitlist Revalidation Post-Reschedule
Given a rescheduled session with waitlist enabled When the commit completes Then the system re-evaluates roster and waitlist according to policy And if “Require reconfirmation on time change” is enabled, confirmed attendees receive a reconfirmation request with a configurable expiry; non-reconfirmed spots are released at expiry And released spots are offered to waitlisted users in priority order; promotions occur automatically until capacity is met And promoted users receive confirmations; users with conflicts at the new time are skipped and preserved on the waitlist And all promotions, drops, and skips are logged
Rollback Within Defined Window
Given a rollback window (e.g., 30 minutes) is configured and no blocking conflicts exist at the original time When the organizer triggers Rollback for the MicroShift within the window Then the session reverts to its original time atomically And correction notifications are sent with updated .ics (same UID, SEQUENCE incremented) and SMS/email content indicating the reversion And reminder schedules are restored; fee waiver flags tied solely to the reschedule are removed; waitlist changes are reversed where possible And any exceptions (e.g., irreversible promotions) are surfaced with recommended actions And the audit log links the rollback to the original commit
External Calendar Sync And Double-Booking Guard
Given instructors and rooms have connected external calendars When a MicroShift commit is attempted Then conflict checks include external calendars and block the commit if the new time overlaps by more than the configured overlap threshold And upon a successful commit, the existing external calendar event is updated (not duplicated) within 5 minutes to reflect the new time And sync failures are retried with exponential backoff and surfaced in activity logs
A/B Experimentation & Outcome Measurement
"As a product admin, I want to measure the real-world impact of MicroShift so that we can prove value and tune the recommendation strategy."
Description

Introduces lightweight experimentation controls to run MicroShift versus control across comparable classes or time periods, capturing on-time arrivals, no-shows, late check-ins, and cancellations. Attributes lift to shift magnitude and contextual signals, and surfaces results in analytics with cohort filtering and export to refine recommendation strategies and prove ROI.

Acceptance Criteria
Create and Launch MicroShift A/B Experiment
Given an instructor selects a set of comparable classes or time periods for experimentation And a randomization ratio (default 50/50) and minimum sample size per arm (default 30 bookings) are configured When the instructor defines the variant shift magnitude between 5 and 20 minutes and the control as 0 minutes Then the system validates no overlapping experiments exist on the same class/time window And blocks launch if venue/instructor/resource constraints would be violated by the variant scheduling And displays an estimated experiment duration to reach the minimum sample size based on recent booking velocity When the instructor launches the experiment Then assignments are randomly allocated at the chosen ratio, locked, and visible on the schedule and experiment dashboard
Capture Outcome Metrics per Arm
Given a running experiment with control and variant arms When students book, cancel, or check in Then the system records for each booking: arm, shift_minutes, scheduled_start (UTC and local), check_in_time, cancellation_time, and contextual signals (day of week, time-of-day bucket, location, modality, instructor, class type) And classifies outcomes using standardized definitions: on-time (check-in from 0 to +5 min), late (check-in > +5 to +20 min), no-show (no check-in by class end), cancellation (cancel before start) And attributes each outcome to the correct arm and shift magnitude And ensures data completeness rate >= 99% for required fields; missing fields are flagged with reason codes
Compute and Display Lift and Confidence
Given at least 30 bookings per arm or experiment duration exceeds 14 days When analytics are viewed Then the dashboard displays, for on-time rate, no-show rate, late-check-in rate, and cancellation rate: absolute difference, relative lift, 95% confidence interval, and p-value (two-sided) of variant vs control And highlights statistical significance at alpha = 0.05 And shows sample size progress, power estimate, and "Underpowered" badge if either arm < 30 bookings And computes and segments lift by shift magnitude buckets (5–9, 10–14, 15–20 minutes) and by contextual signals
Cohort Filtering in Experiment Analytics
Given experiment results exist When the user applies cohort filters (location, instructor, class type, modality, day of week, time-of-day, new vs returning students) Then all metrics, samples, and lifts recompute consistently across both arms And filtered counts reconcile to totals within ±1 due to rounding only And the filter state is reflected in the URL for shareable deep links And clearing filters restores the default view with totals
CSV Export of Experiment Results
Given a filtered or unfiltered experiment view When the user requests CSV export Then a CSV is generated within 60 seconds containing both summary (one row per arm) and granular data (one row per booking or session) according to selected granularity And includes headers: experiment_id, arm, class_id, session_id, scheduled_start_local, scheduled_start_utc, shift_minutes, booking_id, student_id (hashed), on_time_flag, late_minutes, no_show_flag, cancel_flag, check_in_time, cancellation_time, location_id, instructor_id, class_type, modality, cohort_flags, contextual_signals And timestamps are ISO 8601 with timezone info; numeric fields use dot decimal; nulls are empty And the export respects active filters and date range And a secure download link is available for 24 hours and audit-logged
Integrity: Scheduling and Confound Prevention
Given an active experiment When a conflicting schedule change is attempted (e.g., instructor unavailability, venue blackout, overlapping experiments) Then the system blocks the change for affected sessions in both arms and surfaces a corrective action And offers to pause the experiment; if paused, future assignments stop while existing data remains available and labeled as "Paused" And any excluded session or booking is tagged with exclusion_reason and excluded from analytics by default, with an option to include for sensitivity analysis And an immutable audit trail captures user, timestamp, and change details for all experiment configuration and assignment events
Permissions & Audit Trail
"As an owner, I want controls and traceability over schedule changes so that our team stays accountable and we can resolve disputes."
Description

Enforces role-based permissions so only authorized users can view recommendations or apply shifts, and records an immutable audit trail of proposed shifts, approvals, rejections, notifications sent, and rollbacks with timestamps and actor identity. Displays audit entries in class history and provides export for compliance and support inquiries.

Acceptance Criteria
Authorized Viewing of MicroShift Recommendations
Given a user with permission "MicroShift.View" for Class X When they open the Class X details or schedule panel Then the MicroShift recommendations list is displayed And the recommendations endpoint GET /classes/{classId}/microshift responds 200 with at least one item when data exists Given a user without permission "MicroShift.View" for Class X When they open the Class X details or schedule panel Then the MicroShift recommendations list is not displayed And the recommendations endpoint GET /classes/{classId}/microshift responds 403
Controlled Application and Approval of MicroShift Shifts
Given a user with permission "MicroShift.Apply" for Class X selects a recommendation that requires approval When they click Apply Then a proposal record is created with status "pending_approval" And the class start time remains unchanged until an approver acts And an audit entry with event_type "proposed" is recorded including actor and timestamp Given a user with permission "MicroShift.Approve" for Class X opens the pending proposal When they approve it Then the class start time updates to the proposed time And an audit entry with event_type "approved" is recorded And attendees are queued for notifications Given a user without permission "MicroShift.Apply" for Class X When they attempt to apply any recommendation Then the action is blocked and the API responds 403 And the class start time remains unchanged
Audit Entry Completeness and Format
For each MicroShift lifecycle event (proposed, approved, rejected, notified, rolled_back): - The audit record includes: audit_id (UUID), event_type (one of [proposed, approved, rejected, notified, rolled_back]), class_id, actor_id, actor_role, timestamp (ISO 8601, UTC, ms), old_start_time (ISO 8601 with TZ), new_start_time (ISO 8601 with TZ), reason (optional), channels_notified (array), counts_notified (per channel), correlation_id (UUID), integrity_hash (SHA-256) - Required fields are non-null except reason and notification fields for non-notification events - Timestamps for related events are non-decreasing across the lifecycle (proposed <= approved/rejected <= notified <= rolled_back) - Attempts to create an audit entry missing any required field result in HTTP 400 and no record is written
Audit Trail Immutability and Tamper Prevention
Given any user (including admins) attempts to edit or delete an existing audit entry via UI or API When they send PUT/PATCH/DELETE to /audit/{auditId} Then the request is rejected with 405 Method Not Allowed (or 403 per policy) and no changes are made And subsequent GET /audit/{auditId} returns the original entry unchanged And GET /classes/{classId}/audit returns the same total count and digest value before and after the attempt
Audit Trail Display in Class History
Given a class with at least 5 audit entries spanning multiple event types When a user with permission "ClassHistory.View" opens the Class History tab Then the audit list shows entries with columns: Event, Old Start, New Start, Actor, Timestamp, Notes, Channel/Counts (where applicable) And entries are sorted by Timestamp descending by default And the user can filter by Event Type, Actor, and Date Range, and the resulting list updates within 1 second for up to 200 entries And pagination shows 50 entries per page with Next/Previous controls
Audit Export for Compliance and Support
Given a user with permission "Audit.Export" selects a class and date range When they export to CSV and JSON Then the system generates each file within 60 seconds for up to 10,000 entries And files are UTF-8 encoded with headers and include: all visible fields plus audit_id and correlation_id and integrity_hash And the exported row count matches the number of entries returned by the same filters in the UI/API And only users with "Audit.Export" can download the files; others receive 403 And the export action is logged with actor, timestamp, entry_count, and format
MicroShift Rollback with Notifications and Guardrails
Given a class with an approved MicroShift applied within the rollback window When a user with permission "MicroShift.Rollback" triggers a rollback Then the class start time reverts to the immediately previous start time And audit entries with event_type "rolled_back" and "notified" (per channel) are recorded with actor and timestamps And attendees are notified according to the class notification policy And if the rollback would cause a double-booking, the API responds 409 and no changes are made

Cadence Fit

Optimize recurring series for retention, not just first-class fills. Cadence Fit models attendance decay and cohort behavior to propose the best weekly rhythm (e.g., Tue/Thu 6:10pm for 6 weeks), then auto-suggests next-term times that keep momentum—minimizing churn between cycles.

Requirements

Attendance Decay Modeling Engine
"As a studio owner, I want reliable predictions of attendance decay for different schedule options so that I can choose cadences that maximize retention and revenue."
Description

Implements a predictive engine that models week-over-week attendance decay for recurring class series using historical booking, attendance, cancellation, and no-show data from ClassTap. The engine ingests signals such as day/time, class type, instructor, location, capacity, waitlist length, seasonality, and cohort size to forecast retention curves for candidate schedules. It supports cold-start scenarios via configurable heuristics and global priors when data is sparse, and continuously retrains with new outcomes to improve accuracy. Outputs include projected attendance per week, expected churn, confidence intervals, and overall retention score for each cadence. Integrates with the scheduling service, inventory/capacity module, and analytics pipeline, with guardrails for data privacy and opt-outs at the account level.

Acceptance Criteria
Forecast Output Completeness and Calibration
Given a forecast request for a recurring series with length L weeks and capacity C When the engine returns results Then for each candidate cadence and each week i=1..L the response includes projected_attendance_i (integer 0..C), expected_churn_i (0..1), ci80_i [lo, hi], ci95_i [lo, hi], and retention_score (0..100) And projected_attendance_i is between 0 and C inclusive for all i And retention_score equals the mean weekly retention over L scaled to 0..100 within ±0.5 of spec And prediction interval coverage on a rolling 12-week holdout is 75%–85% for 80% CIs and 90%–98% for 95% CIs And the response validates against schema version v1.0; invalid requests receive HTTP 400 with a schema error code
Signal Ingestion and Feature Coverage
Given the feature assembly for training and serving When the engine ingests signals Then the following signals are present or defaulted without failure: day_of_week, start_time, class_type_id, instructor_id, location_id, capacity, waitlist_length, seasonality_index, cohort_size And historical data window covers up to the last 18 months where available And cancellations are excluded from attendance labels while no-shows are included as such per spec And end-to-end event-to-feature freshness is ≤ 24 hours
Cold-Start and Sparse Data Fallback
Given a candidate cadence where any of {class_type_id, instructor_id, location_id} has < 5 historical series or < 200 attendee-weeks total When a forecast is requested Then the engine applies global priors plus heuristics for day_of_week and start_time And widens all week-level prediction intervals by ≥ 20% versus data-rich baselines And sets low_confidence=true in the response And produces outputs within the same latency SLA as data-rich paths
Continuous Retraining, Versioning, and Promotion
Given new attendance outcomes are available When the nightly retraining job runs at 02:00 UTC and ≥ 200 new attended sessions were logged since the last run Then a candidate model is evaluated on an 8-week holdout and is promoted only if MAPE for weekly attendance ≤ 18% and log-loss improves by ≥ 2% versus current And if criteria are not met, the current model remains in production And all forecasts include model_version and training_timestamp And predictions are deterministic for identical inputs and model_version And a pager alert triggers if 7-day rolling live MAPE > 22% for any top-10 accounts by volume for ≥ 3 consecutive days
Scheduling API Integration, Ranking, and Latency
Given the scheduling service calls POST /v1/retention/forecast with up to 100 candidate cadences (term length L ≤ 12, capacity C ≤ 60) When the request is processed Then p95 latency ≤ 800 ms and p99 latency ≤ 1500 ms And the response ranks candidates by retention_score descending, breaking ties by lower expected_churn then earlier start_time And the API honors an Idempotency-Key or request_id to guarantee identical results for identical inputs and model_version And on upstream timeouts the service returns HTTP 503 within 2 seconds without partials unless partial_results=true; if partials are returned, at least 80% of candidates include results and partial=true is set
Capacity and Waitlist Constraint Awareness
Given weekly capacity C_i and current enrollments E_i and waitlist_length W_i per week i are provided When forecasts are generated Then projected_attendance_i ≤ max(C_i − E_i, 0) for all i And if W_i > 0, then projected_attendance_{i+1} includes an uplift U bounded by 0 ≤ U ≤ min(W_i, 0.15 × C_{i+1}) And blackout dates/holidays supplied are assigned attendance 0 with rationale="blackout" And varying room capacities by week are honored without violating constraints
Privacy, Opt-Outs, and Data Governance
Given account A has opt_out=true When training and serving occur Then A’s historical data are excluded from future training within 24 hours and from cross-account priors at serving immediately And no PII (names, emails, phone numbers) is present in features or logs; only hashed IDs are used And account-level deletion requests are completed within 30 days and verified absent from training snapshots and the feature store And global priors are computed using only opted-in accounts and are versioned with an audit record of contributing account count
Cohort Affinity & Segmentation
"As an instructor, I want the system to recognize when my students tend to attend on specific days and times so that proposed schedules match their real availability and habits."
Description

Clusters students into behavioral cohorts based on attendance history, preferred days/times, commute radius, time zone, and class type affinity to identify "sticky" rhythms (e.g., Tue/Thu evenings). Builds a calendar-affinity profile per class and per instructor to detect patterns such as school-term alignment or holiday dips. Exposes cohort-level constraints (e.g., cannot attend Fridays after 6pm) and feeds these as features to the modeling engine and optimizer. Integrates with existing user profiles, tags, and CRM exports without storing external PII beyond what ClassTap already maintains. Provides configurable thresholds for minimum cohort size to avoid overfitting.

Acceptance Criteria
Behavioral Cohort Clustering Pipeline
Given historical attendance, preferred days/times, commute radius, time zone, and class type affinity are available in the data store When the nightly clustering job runs Then each active learner is assigned to exactly one cohort per class type with an assignment timestamp And the job completes within 15 minutes for 100k active learners and 12 weeks of history And results are deterministic given identical inputs and clustering seed And any cohort with size below the configured minimum threshold is labeled "insufficient" and excluded from downstream feeds
Cohort-Level Constraint Extraction (e.g., No Fridays After 6pm)
Given at least 12 weeks of attendance and booking attempt data per cohort When the constraint extraction process runs Then time-window constraints are emitted where at least 80% of cohort members show zero attendance for that window across the lookback period And each constraint includes cohort_id, rule_expression (e.g., day=Fri AND start_time>=18:00), timezone, lookback_window, support_count, and support_rate And constraints are exposed via API and reporting with the same values And constraints are versioned and traceable to the source run id
Calendar-Affinity Profiles for Classes and Instructors
Given at least 8 completed sessions for a class or instructor When the affinity profiler runs Then it produces a ranked list of the top 5 recurring rhythms (day-of-week and 30-minute time buckets) with an affinity score between 0 and 1 And it flags school-term alignment when attendance rises ≥15% for ≥4 consecutive weeks after typical school start dates in the class locale And it flags holiday dips when attendance falls ≥20% during defined holiday periods relative to the prior 4-week baseline And the profile is stored and retrievable via API with class_id/instructor_id, rhythms[], scores[], and detected_patterns[]
Privacy Guardrails for Feature Generation and CRM Integration
Given a CRM export is ingested for enrichment When mapping occurs Then only fields already maintained by ClassTap are persisted and all other PII fields are discarded at ingestion And no new PII columns are created in any persistent store And emitted features contain only aggregated or bucketized values (e.g., commute_radius_bucket, time_bucket, day_of_week) with no raw addresses, emails, or phone numbers And a governance log records discarded field names and counts per run And an automated scan of feature payloads finds zero occurrences of email, phone, or street address patterns
Configurable Minimum Cohort Size to Avoid Overfitting
Given a platform default minimum cohort size of 20 and a tenant-level override setting When clustering completes Then cohorts with member_count < threshold are labeled "insufficient" and excluded from feature exports to the modeling engine and optimizer And an admin can set the threshold between 5 and 100 via settings, with validation preventing out-of-range values And threshold changes are audited and take effect on the next scheduled run
Feature Feed Contract to Modeling Engine and Optimizer
Given valid cohort features, constraints, and affinity profiles are available When the feature export job runs Then the modeling engine receives a payload matching schema_version X.Y with mandatory fields: cohort_id, class_type, rhythm_scores[], constraints[], locale_timezone, member_count And the export returns HTTP 2xx with an ack_id on success And if any mandatory field is missing, the export fails with HTTP 4xx/5xx, emits an alert, and does not send partial data And the optimizer can fetch cohort constraints via GET /cohorts/{id}/constraints with p95 latency ≤200ms at 100 concurrent requests
Time Zone Normalization and Daylight Saving Handling
Given learners and classes span multiple IANA time zones with DST transitions When computing preferred days/times, rhythms, and constraints Then all calculations are performed in the class’s local IANA time zone And outputs store times as local time with accompanying IANA zone id and UTC offset at event time And unit tests for the DST transition week show no off-by-one-day or one-hour errors in rhythms or constraints
Schedule Optimizer & Proposal Generator
"As a program director, I want a ranked list of feasible schedules with clear retention and revenue scores so that I can confidently pick the best cadence without manual spreadsheet work."
Description

Generates and ranks candidate recurring schedules (e.g., 2x per week for 6 weeks at 6:10pm) under real-world constraints. Consumes instructor/room availability, blackout dates, holidays, capacity, and cohort affinity to produce the top N proposals with predicted retention, fill rate, and revenue impact. Each proposal includes an explanation of key drivers (e.g., Tuesday/Thursday pattern aligns with cohort affinity; avoids holiday week) and conflict checks against existing bookings to prevent double-booking. Exposes an API and admin UI endpoint to request proposals for a class template and write back the selected schedule to publish.

Acceptance Criteria
Top-N Ranked Proposals with Metrics and Explanations
Given a class template with valid availability, capacity, date range parameters, and N=5 When the optimizer generates proposals Then it returns 1 to 5 proposals sorted by predicted retention (descending) And each proposal includes cadence pattern (e.g., 2x/week for 6 weeks), start date, end date, number of sessions, session weekdays and start times, time zone, predicted retention (%), predicted fill rate (%), predicted revenue (currency), and an explanation of key drivers And ties in retention are broken by higher predicted revenue, then higher predicted fill rate, then earliest start date And for identical inputs the ranking and scores are deterministic
Hard Constraint Compliance (Availability, Holidays, Blackouts, Capacity)
Given instructor and room availability windows, capacity limits, holiday calendar, and blackout dates When generating proposals Then no session is scheduled outside any availability window And no session falls on a blackout date or organization holiday And multi-week cadences skip holiday weeks without reducing the total number of sessions unless skipping would violate the date range, in which case the schedule is excluded And predicted fill and revenue never assume attendance greater than defined capacity per session
Conflict Detection and Double‑Booking Prevention
Given existing published sessions and holds for the same instructor or room When generating proposals Then any candidate with a time overlap conflict is excluded When publishing a selected proposal Then a conflict check is performed atomically against the latest calendar; if a conflict exists the publish fails with HTTP 409 and no sessions are created And the API/UI returns the conflicting resource, date, and time range And an audit log entry is recorded for the failed attempt
Proposal Generation API Behavior
Given an authenticated client with scope proposal:write When it POSTs /api/v1/proposals with template_id, date_range, cadence_options, timezone, and N within 1..10 Then the response is HTTP 200 within 3 seconds for workloads evaluating ≤2000 candidates And the response JSON includes request_id, generated_at (ISO-8601), and proposals[] where each item has id, cadence pattern, dates, metrics (retention, fill_rate, revenue), drivers[], and conflicts[] And providing an Idempotency-Key header returns identical proposal ids and scores for repeated identical requests within 24 hours And invalid or missing fields return HTTP 400 with machine-readable error codes; missing/invalid auth returns 401/403
Admin UI Proposal Review and Selection
Given an admin selects a class template in Cadence Fit When they click Generate Proposals and choose N Then a results list appears sorted by predicted retention (descending) with columns: cadence, start/end, sessions, retention, fill rate, revenue, drivers summary And the admin can sort by retention, fill rate, or revenue and filter by weekdays and time window And selecting a proposal highlights it and enables Publish; only one proposal can be selected at a time
Write‑Back and Publish Selected Schedule
Given a selected proposal with no conflicts at publish time When the admin clicks Publish Then the system writes a recurring series and session instances to the calendar, reserves the instructor and room, and marks the schedule status as Published And the operation is atomic; on any error all changes are rolled back And the API returns HTTP 201 with series_id and session_ids; the UI displays a success state And an audit log records user, timestamp, proposal_id, and created series/session identifiers
No Feasible Schedule Response
Given constraints that yield zero feasible candidates When generating proposals Then the API returns HTTP 200 with proposals=[] and reasons[] listing at least one actionable cause (e.g., no overlapping availability, holidays cover entire range) And the UI displays a zero-state with the same reasons and a link to adjust constraints And no calendar writes occur
Next-Term Auto-Rollover Suggestions
"As a studio manager, I want smart next-term suggestions that keep my cohorts together so that I reduce churn between cycles and simplify scheduling."
Description

Monitors active series approaching completion and automatically proposes next-term start dates and times that maintain cohort momentum based on predicted retention and calendar continuity. Supports pre-enrollment with seat priority for current attendees, optional auto-creation of the next series draft, and configurable lead-time windows for notifications. Integrates with payment plans and credits to carry over unused passes where permitted, and with reminders to nudge cohorts to rebook. Provides toggles for one-click publish or manual approval, with audit logs for all rollover actions.

Acceptance Criteria
Automatic Rollover Proposal Generation
Given an active series with at least 1 upcoming session and an organization lead-time window is configured When the series enters the lead-time window Then the system generates at least 2 next-term schedule suggestions containing: start date, start time, cadence length (e.g., weeks), number of sessions, and a predicted retention score And each suggestion is validated to avoid instructor, room, and location conflicts and respects org/site blackouts and holidays And suggestions preserve the current weekday pattern and start-time within ±15 minutes unless the model predicts ≥5% retention improvement by deviating And suggestions are sorted by predicted retention score descending and labeled with a rationale summary string of 240 characters or less And a notification to the series owner is queued within 5 minutes of suggestion generation
Pre-Enrollment Priority for Current Cohort
Given pre-enrollment is enabled with a hold duration configured and a next-term suggestion is approved or a draft exists When pre-enrollment opens for the next-term series Then seat holds are created for all current attendees up to series capacity and holds do not charge until checkout And cohort members can rebook via personalized links without re-entering profile data or payment method (unless expired) And holds expire exactly at the configured duration from the first notification timestamp and are then released to the public/waitlist And if current attendees exceed capacity, remaining cohort members are placed at the top of the next-term waitlist ahead of the public And all hold and release events are visible in the roster with timestamps and actor where applicable
Auto-Create Next Series Draft
Given auto-create next-term drafts is enabled for the series When rollover suggestions are generated Then the highest-ranked suggestion is instantiated as a Draft series that inherits instructor(s), location/room, capacity, pricing, tax, cancellation policy, and enrollment settings from the current series And the draft is not publicly discoverable until published and is clearly labeled as Draft in the admin And time/resource conflicts on the draft are flagged and block One-Click Publish until resolved, while still allowing manual edits And admins can edit schedule, pricing, and capacity on the draft prior to publishing And disabling the auto-create setting prevents draft creation on subsequent suggestion runs
Lead-Time Configuration and Notifications
Given an organization default lead-time (e.g., 14 days) and a series-level override (e.g., 10 days) When the series reaches the override threshold before its last scheduled session Then suggestion generation is triggered at 10 days (series-level override takes precedence over org default) And notifications are sent to the series owner and assigned instructors via their preferred channels (email/SMS) with the top 3 suggestions and retention scores And notifications include action controls: Approve Draft, One-Click Publish, View All Suggestions, and Dismiss And all timestamps displayed and used for scheduling honor the series timezone And duplicate notifications for the same trigger are suppressed for 24 hours
Payment Plans and Credit Carryover
Given carryover of unused passes is permitted by the organization policy and a student has remaining credits on the current term When the student rebooks into the next-term series via the rollover flow Then eligible unused credits are automatically applied to the new term at checkout per policy limits (quantity and expiry) And the invoice line items clearly itemize carryover credits, proration (if any), and remaining balance And if carryover is not permitted or credits are expired, the system presents a clear policy message and offers standard payment options And accounting/ledger entries reflect credit application with reference IDs linking old and new series And no carryover is applied if the next-term start date violates the policy gap limit between terms
Rebooking Nudges and Reminders
Given rebooking reminders are enabled for rollover When suggestions are generated and pre-enrollment opens Then reminders are scheduled to non-rebooked current attendees at T-7, T-3, and T-1 days before the next-term start And reminders cease immediately after the attendee rebooks or opts out And each reminder contains a personalized deep link to the pre-filled checkout and displays remaining seat-hold time if applicable And SMS reminders are sent only to contacts with SMS consent; otherwise email is used And click-through and conversion events are tracked and attributed to the series and message instance
One-Click Publish and Audit Logging
Given the admin has publish permission and a rollover suggestion or draft exists When the admin selects One-Click Publish Then the next-term series is published immediately, enrollment opens, and the series appears on the branded booking page And if Manual Approval is selected, the series remains in Draft until explicitly approved And all rollover actions (suggestion generation, draft creation, pre-enrollment open, publish, dismiss) are recorded in an immutable audit log with actor, timestamp (UTC and local), entity IDs, and old/new state And audit records are filterable by series and action type and can be exported to CSV And an audit entry is written within 1 second of each action completion
Demand Signals Integration (Waitlist & No-Show)
"As a data-informed owner, I want the optimizer to account for waitlists and no-show patterns so that proposed schedules reflect true demand and reliability."
Description

Augments predictions by incorporating real-time demand signals such as waitlist length, late cancellations, and no-show rates per day/time to adjust expected fill and retention. Applies decay-weighted recency to emphasize recent trends and introduces caps and smoothing to avoid overreacting to anomalies. Provides a data contract with the waitlist and attendance subsystems and surfaces the contribution of each signal in the proposal explanations. Includes monitoring dashboards and alerting when signal feeds degrade or drift.

Acceptance Criteria
Waitlist Signal Ingestion and Decay Weighting
Given the waitlist service publishes events with fields {class_id, session_datetime, waitlist_count, event_timestamp} When the events are produced in real time Then ≥99.5% of events are ingested within 2 minutes of publish time over any rolling 24h window Given waitlist counts with timestamps are stored When computing the waitlist signal score for a day/time bucket Then exponential decay weighting is applied with a configurable half-life (default 14 days) and a test dataset yields weights at t0, t0+14d, t0+28d of [1.0, 0.5±0.05, 0.25±0.05] Given events arrive late due to a temporary outage (≤24h) When backfilled Then the recomputed waitlist signal score matches real-time processing within ±1% absolute difference
Attendance No-Show and Late Cancellation Signals
Given the attendance service emits session outcomes with fields {class_id, session_datetime, user_id_hash, outcome, event_timestamp} When outcomes are ingested Then the system computes decay-weighted (half-life 14 days) no-show rate and late-cancel rate per weekday-time-slot using a minimum sample size of 30 sessions; if fewer than 30, it falls back to a 12-week window Given a new week of data is added When rates are recomputed from raw history Then the stored rates differ by ≤2% absolute from a full recomputation (idempotence check) Given payloads may include extra fields When persisting Then no PII beyond user_id_hash is stored, and payloads missing required fields are rejected with a 400 error and logged
Caps and Smoothing to Prevent Overreaction
Given a single-day spike where a signal value exceeds the 99th percentile of the past 90 days When computing the composite adjustment Then the spike is winsorized at the 95th percentile before decay weighting Given composite adjustments are calculated per slot weekly When applying adjustments to expected fill and retention Then adjustments are bounded within [-15%, +20%] per recomputation Given a synthetic anomaly dataset with a 1-day spike When running the pipeline end-to-end Then the proposed slot's expected fill change is ≤5% versus baseline without the spike
Explainable Contributions in Proposal
Given a Cadence Fit proposal is generated for a term When viewing a suggested slot Then the UI shows base demand and additive contributions from waitlist, no-show, and late-cancel signals, plus the net adjustment, and the contributions sum to the total adjustment within ±0.5% Given a user hovers the explanation tooltip or calls the explanations API When data is returned Then each signal includes last_updated_timestamp, sample_size, decay_half_life_days, and any caps applied Given the explanations API is queried at /cadence-fit/explanations?slot_id=... When under normal load Then p95 latency ≤300ms and the response adheres to the published schema
Signal Feed Health Monitoring and Alerts
Given monitoring is configured for waitlist and attendance feeds When observing in dashboards Then freshness lag, throughput, parse error rate, and missing-field rate are shown with p50/p90/p99 for the last 24h Given feed freshness lag exceeds 10 minutes for two consecutive 5-minute intervals When detected Then an alert is sent within 5 minutes to Slack #cadence-signals and on-call email with a runbook link Given distribution drift where KL divergence on waitlist_count or no_show_rate exceeds 0.1 over a 3-day window When detected Then an alert is generated and the affected signal is marked "degraded" in the dashboard
Degradation, Fallback, and Recovery Behavior
Given a signal feed is marked degraded by monitoring When generating proposals Then the system uses the last-known-good snapshot for that signal and reduces its weight by 50%, and a UI banner indicates degraded signals are in effect Given both waitlist and attendance feeds are unavailable for >24h When generating proposals Then the system reverts to the base demand model, flags proposal quality as "low", and blocks auto-publish Given feeds recover after an outage with delayed events When replay processing completes Then composite scores reconcile to within ±1% of a full-history recomputation and the degraded status is cleared automatically
Versioned Data Contracts and Schema Validation
Given data contracts for waitlist and attendance are published as JSON Schema/OpenAPI with required fields and types When events are received Then runtime validation accepts additive, backward-compatible changes and rejects breaking changes with explicit error codes and metrics Given incoming events include a contract_version field When observed in the pipeline Then dashboards show version distribution by feed and an alert triggers on unknown versions Given a schema change proposal is introduced When contract_version is bumped Then downstream consumers continue to function without manual intervention for additive changes
Compare View & One-Click Apply
"As an administrator, I want to quickly compare schedule options and publish the winner with one click so that I can move from insight to action without extra tools."
Description

Delivers an admin interface to compare up to five schedule proposals side-by-side, showing key metrics (predicted retention, expected revenue, average class fill, conflicts avoided, confidence). Supports what-if adjustments (e.g., shift start time by 10 minutes, swap day) with instant re-scoring. Enables one-click apply to create the selected recurring series, reserve resources, generate the branded booking page, and optionally trigger pre-enrollment invitations. Includes role-based permissions, audit trail, and export of proposals to CSV/PDF for stakeholder review.

Acceptance Criteria
Compare Up to Five Proposals with Key Metrics
Given I have 1–5 schedule proposals generated by Cadence Fit When I open Compare View Then I see each proposal displayed side-by-side in columns (maximum 5) without horizontal scrolling on a 1440px wide viewport And each proposal column shows predicted retention (%), expected revenue (currency), average class fill (%), conflicts avoided (count), and confidence (Low/Med/High with %) And values match the latest model output within ±0.1 percentage points for percentages and ±0.50 in the account currency for revenue And proposals are ordered by the selected sort (default: expected revenue descending) And attempting to add a sixth proposal displays a non-blocking error "Maximum 5 proposals" and does not add it And initial render completes within 1.5 seconds at p95 with 12 months of data and 50 classes
What-If Adjustments with Instant Re-Scoring
Given Compare View is open with at least one proposal selected When I shift a proposal's start time by 10 minutes earlier Then the proposal's metrics re-score and update within 2 seconds p95 without a page reload And the change is visually indicated with delta badges and a Revert control And clicking Revert restores original times and metrics within 1 second When I swap a class day (e.g., Tue to Wed) Then conflicts avoided and average class fill recompute and reflect new values And unsaved what-if changes do not persist after navigating away unless I click Save Variant
One-Click Apply Creates Series and Artifacts
Given I have a selected proposal or saved what-if variant in Compare View When I click Apply Then the system creates a recurring series with the specified cadence, start/end dates, and time slots And reserves required resources (room, instructor, capacity) for all instances, failing atomically if any conflict emerges And generates a branded booking page with the organization's theme and a shareable URL And if "Send pre-enrollment invitations" is toggled on, queues SMS and email invitations to the prior cohort within 2 minutes And I receive a success confirmation with links to the series, booking page, and invitation queue And an audit trail entry is recorded with user, timestamp, proposal ID/variant ID, and artifacts created And the apply operation completes within 10 seconds p95 or surfaces a retryable error without partial side effects
Role-Based Permissions for Compare and Apply
Given a user with role Owner or Scheduler When they open Compare View Then they can view metrics, run what-if adjustments, export, and apply proposals Given a user with role Instructor When they open Compare View Then they can view metrics but cannot apply proposals or export; Apply and Export controls are disabled with a tooltip "Insufficient permissions" Given a user without access to the location or program When they attempt to access Compare View via deep link Then they receive a 403 error page and no data is leaked And all permission checks are enforced server-side and captured in audit logs
Export Proposals to CSV and PDF
Given up to five proposals are visible in Compare View When I click Export and choose CSV Then a CSV downloads containing one row per proposal with columns: proposal ID, name, predicted retention, expected revenue, average class fill, conflicts avoided, confidence, assumptions, timestamp And numeric values match the UI within ±0.1 percentage points for percentages and exact cents for currency using the organization's locale and timezone And the file name follows the pattern proposals_YYYY-MM-DD_HH-mm_orgslug.csv When I choose PDF Then a paginated PDF renders a side-by-side comparison matching on-screen ordering and includes the organization logo and footer And both exports complete within 5 seconds p95 and pass basic accessibility checks (tagged PDF, CSV header row)
Audit Trail Coverage for Compare Actions
Given a user views Compare View When they perform actions (view, filter, sort, what-if adjust, export, apply) Then an audit record is created for each action including user ID, role, timestamp (UTC), proposal IDs affected, before/after values where applicable, and IP/device fingerprint And audit records are immutable, queryable by date range and proposal ID, and exportable to CSV And audit entries for failed Apply operations include error codes and rollback confirmation

Instructor Match

Personalized heatmaps per instructor reveal when each teacher overperforms the average. Instructor Match pairs the right instructor with the right hour based on historical pull, ratings, and audience segments—lifting conversion without adding more classes.

Requirements

Unified Performance Data Pipeline
"As an operations manager, I want a reliable, up‑to‑date dataset of instructor and time‑slot performance so that recommendations and heatmaps are accurate and trustworthy."
Description

Build a robust data pipeline that aggregates and normalizes historical and real‑time signals needed for Instructor Match, including bookings, conversions, ratings, attendance, cancellations/no‑shows, waitlist sizes, class types, locations, modalities, price points, promotions, and audience segments. Implement de‑duplication, schema versioning, and quality checks (freshness, completeness, outlier detection). Compute derived features such as slot performance vs studio baseline, instructor lift by segment, and time‑of‑day seasonality. Backfill at least 12 months of data where available and update incrementally near real‑time. Enforce PII minimization and anonymization for attendee data while preserving analytic utility. Expose standardized feature tables for downstream analytics, heatmaps, and the recommendation engine.

Acceptance Criteria
Unified Ingestion, Normalization, and De-duplication
Given events from all sources (bookings, conversions, ratings, attendance, cancellations/no-shows, waitlists, class types, locations, modalities, price points, promotions, audience segments), when ingested, then 100% of records include source_id, event_time, entity_keys, and schema_version. Given records land in bronze, when normalized to a canonical silver schema, then 100% conform to schema with zero invalid types; any non-conforming records are quarantined with error_code and counted. Given duplicate events (same natural key or idempotency_key within a 24h window), when processed, then exactly one record is present in silver/gold and duplicates are logged. Given ingestion for a 24h period, when counting primary keys in silver/gold, then zero duplicate primary keys are found. Given unmapped enumeration values in modalities/class_types/promotions, when processed, then they are quarantined and a mapping gap alert is sent within 5 minutes. Given lineage requirements, when a batch completes, then metadata includes source system, load_time, record_counts, and checksum per partition.
Historical Backfill and Near Real-Time Incremental Updates
Given access to historical sources, when backfill runs, then at least 12 months of data per source are loaded within 24 hours and reconcile within 1% of source counts. Given a re-run of backfill for the same date range, when executed, then row counts and checksums match prior run within 0.1% and no duplicates are introduced. Given streaming/new events, when produced by sources, then they appear in gold feature tables within 10 minutes at p95 and 20 minutes at p99. Given late-arriving events (up to 7 days late), when received, then upserts correct affected aggregates and features within 2 hours. Given failure during incremental load, when retried, then the pipeline resumes from the last checkpoint with exactly-once semantics.
Data Quality: Freshness, Completeness, and Outlier Detection with Alerting
Given freshness SLOs of 10 minutes for streaming tables and 24 hours for daily tables, when monitored, then no breach occurs in 99% of intervals; breaches trigger alerts to the on-call channel within 5 minutes. Given completeness rules, when daily audits run, then non-nullable columns have 0 nulls and row-count variance vs source is within ±1%; outside thresholds block promotion to gold. Given volume anomalies, when load volume deviates by >3 standard deviations from the 8-week baseline, then the anomaly is flagged and gold publish is paused until manual approval. Given numeric fields (price, attendance, waitlist_size), when values are negative or exceed the 99.9th percentile bound, then records are quarantined and counts reported. Given quality reporting, when a load completes, then a report with pass/fail per check is written to a quality_log and retained for ≥180 days.
Derived Features: Slot vs Baseline, Instructor Lift by Segment, Time-of-Day Seasonality
Given sufficient data (≥30 classes and ≥50 bookings in trailing 90 days), when computing slot performance vs studio baseline, then performance_ratio is produced for all qualifying slot-studio pairs; otherwise value is null with reason_code=INSUFFICIENT_DATA. Given audience segments with ≥100 bookings per instructor in trailing 180 days, when computing instructor_lift_by_segment, then lift and 95% CI are produced; otherwise the segment is suppressed. Given time-of-day seasonality per instructor, when computed on rolling 26 weeks, then seasonality indices are normalized (mean=1.0) and bounded between 0.5 and 1.5 unless overridden by rules. Given a deterministic re-computation for the same snapshot date, when the feature job re-runs, then values match the previous run within absolute tolerance 0.001. Given late-arriving events, when reprocessing occurs, then impacted features are updated and downstream tables reflect new values within 2 hours.
PII Minimization and Anonymization with Utility Preservation
Given attendee data, when written to silver/gold and feature tables, then no direct identifiers (name, email, phone) are present and attendee_id is a salted hash rotated at least every 90 days. Given segment publishing, when any segment breakdown has cohort size <10, then values are suppressed or aggregated to meet k-anonymity ≥10. Given joinability risk checks, when evaluating quasi-identifiers, then no combination yields groups <10; automated checks run pre-publish and block on failure. Given access policies, when a user without PII clearance queries feature tables, then no PII columns are returned and access is audited. Given schema changes, when a new column is proposed, then data classification tags are required and PII-tagged columns are blocked from gold without approval.
Standardized Feature Tables and Data Contracts for Consumers
Given downstream needs, when publishing, then the following feature tables exist with documented schemas: feature_slot_baseline, feature_instructor_segment_lift, feature_time_of_day_seasonality, and feature_booking_signals. Given data contracts, when schema changes are proposed, then semantic versioning is applied; backward-compatible changes bump minor, breaking changes bump major and provide a 60-day deprecation window with dual-write. Given SLAs, when measured monthly, then feature tables meet ≥99.9% availability and freshness SLOs in ≥99% of intervals. Given documentation, when the release is complete, then machine-readable schemas, column definitions, sample queries, and lineage are published in the data catalog and pass automated link checks. Given service accounts for heatmaps and the recommendation engine, when querying, then read access is granted and standard-filter queries complete within 2 seconds at p95.
Schema Versioning and Backward Compatibility Governance
Given raw, canonical, and feature schemas, when versioned, then each table includes schema_version and a change_log with author, date, and rationale. Given deployment of a new schema version, when executed in staging, then migration tests pass with zero data loss, zero duplication, and transform parity ≥99.9% on sampled records. Given consumers on prior versions, when a major change is released, then two versions run in parallel for ≥60 days and compatibility tests validate equivalence on overlapped fields. Given rollback procedures, when a release is reverted, then the pipeline returns to the previous stable version within 30 minutes without manual data deletion. Given CI governance, when pull requests modify schemas, then version bump rules are enforced and unversioned schema changes are blocked.
Instructor Heatmap Analytics
"As a studio owner, I want to see when each instructor overperforms the average so that I can schedule them during high‑impact hours."
Description

Provide a per‑instructor heatmap that visualizes relative performance across days and time slots, highlighting over/under‑performance versus selected baselines (studio average, instructor personal average, class‑type average). Include filters for location, class type, modality (in‑person/hybrid), audience segment, and date range. Offer tooltips with key metrics (conversion rate, attendance, revenue per slot, waitlist depth) and confidence indicators. Support quick export (CSV/PNG), responsive layout, keyboard navigation, and accessible color palettes. Integrate seamlessly within the ClassTap analytics area and link out to scheduling actions for discovered high‑impact windows.

Acceptance Criteria
Heatmap Relative Performance vs Baseline
Given I am in Analytics > Instructor Heatmap and I select an instructor and baseline "Studio Average" for the last 90 days When the heatmap renders Then each day-hour cell shows uplift = ((instructor conversion rate − baseline conversion rate) / baseline conversion rate) × 100, rounded to the nearest whole percent And cells with uplift > 0 use the overperformance color scale and uplift < 0 use the underperformance color scale with 0% as a neutral midpoint shown in the legend And changing the baseline to "Instructor Personal Average" or "Class-Type Average" recalculates and updates all cells within 300 ms And cells with sample size n < 10 display a low-confidence indicator and reduced saturation
Filter Interaction: Location, Class Type, Modality, Audience Segment, Date Range
Given an instructor heatmap is loaded When I apply filters Location, Class Type, Modality, Audience Segment, and Date Range (e.g., Last 30 Days) Then the heatmap, legend, and tooltips reflect only classes matching all selected filters And each filter can be cleared individually and selections persist across page reloads in the same session And Date Range supports presets Last 7/30/90 days and a custom range up to 365 days
Tooltips Show Key Metrics and Confidence
Given the heatmap is visible When I hover a cell (desktop), focus a cell (keyboard), or tap a cell (mobile) Then a tooltip opens within 150 ms showing: Conversion Rate (% with 1 decimal), Attendance Rate (% with 1 decimal), Revenue per Slot (localized currency, ≤2 decimals), Waitlist Depth (integer), Sample Size (n), Uplift vs baseline (%), and Confidence (e.g., 95% CI range) And the tooltip stays within the viewport, is dismissible via Esc or tapping/clicking outside, and updates when a different cell is focused
Quick Export CSV and PNG with Context
Given the heatmap has active filters and a selected baseline When I click Export CSV Then a file InstructorHeatmap_<InstructorName>_<YYYYMMDD>_<baseline>.csv downloads within 3 seconds containing one row per day-hour with columns: date, day_of_week, hour, uplift_percent, baseline_type, conversion_rate, attendance_rate, revenue_per_slot, waitlist_depth, sample_size, confidence_level, location, class_type, modality, audience_segment And when I click Export PNG Then a PNG downloads within 3 seconds at 2× the current viewport resolution including the heatmap, legend, title, and applied filters/baseline
Responsive Layout Across Devices
Given I view the heatmap on a device width < 600 px When the page loads Then the heatmap supports horizontal and vertical scrolling with sticky day and hour labels and each interactive cell has a minimum 44×44 px touch target And at widths ≥ 1024 px, all 7 days × 24 hours display without horizontal scroll and labels remain readable And initial render completes under 1.5 s for up to 10k cells on typical broadband with Cumulative Layout Shift ≤ 0.1
Keyboard Navigation and Accessible Color Palettes
Given I navigate the heatmap using keyboard and a screen reader When focus enters the grid Then arrow keys move cell-by-cell, Home/End jump to row start/end, PageUp/PageDown jump by day, Enter/Space open the tooltip, and Esc closes it And each cell has an accessible name announcing day, hour, uplift %, baseline, conversion %, attendance %, revenue, waitlist, sample size, and confidence And color palettes meet WCAG 2.1 AA contrast, are colorblind-safe, and include non-color cues for over/under-performance and low confidence
Scheduling Linkout from High-Impact Window
Given a cell indicates high impact (uplift ≥ 10% with high confidence) When I click "Schedule at this time" Then I am taken to the scheduling flow with date, time, instructor, location, class type, and modality prefilled from the cell and current filters And the linkout is shown only to users with scheduling permission; otherwise it is hidden And an analytics event "heatmap_schedule_link_clicked" is recorded with instructor ID, datetime, filters, baseline, and uplift value
Matchmaking Recommendation Engine
"As a scheduler, I want ranked instructor suggestions for a given time slot so that I can improve bookings without adding more classes."
Description

Develop a recommendation service that scores and ranks instructor–time‑slot pairings by predicted conversion uplift and attendance, using historical performance, ratings, audience segments, seasonality, and real‑time demand signals (e.g., waitlists). Provide reason codes and confidence scores for transparency. Support cold‑start handling for new instructors and sparse data scenarios via similarity and heuristic backoffs. Expose a low‑latency REST API (<500 ms p95) for fetching ranked recommendations per slot or generating an optimized weekly schedule. Allow configurable weighting of objectives (conversion, revenue, no‑shows) at the studio level.

Acceptance Criteria
Ranked Recommendations for a Single Time Slot (REST API)
Given studio S with Instructor Match enabled and available instructors for slot T And current demand signals include waitlist counts for class type C at location L When GET /v1/recommendations?studioId=S&slot=T&classType=C&location=L&limit=N is called Then the response status is 200 And the body contains exactly N recommendation items sorted by score in descending order And each item includes instructorId, slotId, score (0..1), predictedConversionUplift (0..1), predictedAttendance (integer >= 0), reasonCodes (array 1..5), and confidence (0..1) And no instructorId repeats within the list And if waitlist > 0 for C at L, at least one item includes the "real_time_waitlist" reason code And scores are non-increasing across the list
Optimized Weekly Schedule Generation (REST API)
Given studio S provides a week range, instructor availability, room capacities, and hours of operation And objective weights are supplied in the request body When POST /v1/schedule/optimize is called with weekStart, weekEnd, constraints, and weights Then the response status is 200 And the body includes assignments[] each with instructorId, slotId, classType, score, reasonCodes[], and confidence And no instructor has overlapping assignments outside their availability And no room capacity is exceeded and hours of operation are respected And a globalObjectiveScore is returned for the full schedule And repeated calls with identical inputs and seed produce identical schedules
Transparency: Reason Codes and Confidence Scores
Given any recommendation item or schedule assignment in API responses Then each item contains reasonCodes of length 1..5 with unique values drawn from ["historical_pull","rating","audience_match","seasonality","real_time_waitlist","similarity_backoff","heuristic_backoff"] And reasonCodes are ordered by descending contribution And confidence is a numeric value between 0 and 1 inclusive, rounded to two decimals And when waitlist > 0 contributed to the score, "real_time_waitlist" appears in reasonCodes for at least one of the top-3 items And when cold-start or sparse data logic is used, reasonCodes includes "similarity_backoff" or "heuristic_backoff"
Performance SLA: p95 Latency Under Load
Given a production-like dataset with at least 10,000 instructors and 5,000 time slots and cold caches And a steady load of 100 RPS for GET /v1/recommendations and 10 RPS for POST /v1/schedule/optimize over 10 minutes When measuring end-to-end API response times Then p95 latency for GET /v1/recommendations is <= 500 ms with error rate < 0.5% And p95 latency for POST /v1/schedule/optimize is <= 500 ms with error rate < 1.0% And no 5xx responses exceed 1% in any 1-minute window
Studio-Level Objective Weighting Configuration
Given studio S has default objective weights {conversion:0.5, revenue:0.3, noShows:0.2} When PUT /v1/studios/S/settings/objectives is called with {conversion:0.2, revenue:0.6, noShows:0.2} Then GET /v1/studios/S/settings/objectives returns the updated weights And subsequent GET /v1/recommendations with fixed inputs yields a top-5 set whose aggregate expected revenue increases by >= 5% versus baseline while predicted no-shows do not increase by > 10% And when weights are reverted to baseline, the ranking similarity to baseline for the top-5 is high (Kendall tau >= 0.8) And requests from other studios are unaffected (their weights remain unchanged)
Cold-Start Recommendations for New Instructors
Given a new instructor I with no historical bookings and no ratings in studio S When GET /v1/recommendations is called for a slot compatible with I Then I may appear in the results with a finite score derived from similarity and/or heuristic priors And reasonCodes includes "similarity_backoff" or "heuristic_backoff" And confidence for I is <= 0.60 And if no similar instructors exist, I still receives a non-null score and the request succeeds without error
Sparse Data and Heuristic Backoff Behavior
Given missing or incomplete audience segment and seasonality data for studio S When GET /v1/recommendations or POST /v1/schedule/optimize is called Then the response status is 200 and payload contains no null/NaN fields And reasonCodes include "heuristic_backoff" where fallbacks were applied And results are stable across repeated calls with identical inputs (top-5 identical on two consecutive requests)
Constraints and Fairness Rules Engine
"As a studio coordinator, I want the system to honor availability and fairness rules so that recommended schedules are practical and equitable for our instructors."
Description

Implement a constraints framework that ensures recommendations are feasible and equitable: instructor availability, certifications/class‑type eligibility, maximum weekly hours, required rest/travel buffers between locations, pay band constraints, class caps, and conflict avoidance with existing bookings. Support both hard constraints (must not violate) and soft constraints (penalties in optimization), with studio‑level configuration. Include fairness balancing to prevent over‑allocating popular slots to a small subset of instructors and provide override capabilities with justification and audit logging.

Acceptance Criteria
Enforce Hard Constraints on Instructor Match Recommendations
Given Studio X has configured instructor availability, class-type certifications/eligibility, maximum weekly hours, pay band constraints, class capacity caps, and a booking calendar with existing reservations And the dataset contains at least 10 candidate instructors per class slot When the engine generates Instructor Match recommendations for a 7-day window Then every recommended assignment satisfies instructor availability windows And matches required certifications/eligibility for the class type And does not exceed the instructor's configured maximum weekly hours And complies with pay band constraints for the class And does not conflict with existing bookings And does not exceed class capacity caps And any candidate violating a hard constraint is excluded and logged with reason code HARD_CONSTRAINT_VIOLATION including constraint type and identifier
Respect Rest and Travel Buffers Between Classes
Given Studio X has configured a minimum rest buffer of 30 minutes and a travel buffer computed as 10 minutes per mile between locations And instructor location history and class locations are known When generating assignments for consecutive classes for the same instructor on the same day Then any recommended back-to-back assignments are separated by at least 30 minutes rest And for assignments at different locations, the estimated travel time is greater than or equal to the required travel buffer And candidates failing rest or travel buffers are excluded and logged with reason code BUF_VIOLATION including from/to class IDs and computed gap
Apply Soft Constraints with Configurable Penalties
Given Studio X has configured soft constraint weights: historical pull (+3), rating (+2), audience segment match (+2), and commute preference (-1) And a deterministic seed is set for the optimizer When generating recommendations for Prime Time slots Then the engine maximizes total objective score subject to hard constraints And the top 5 recommendations include a per-item score breakdown for each soft constraint component and the total And toggling off audience segment match reorders at least one item within the top 5 results on the same dataset and updates score breakdowns accordingly And increasing the historical pull weight by 10% changes the total score values consistently and reorders at least one pair where historical pull differs
Studio-Level Configuration of Constraints and Weights
Given a tenant with Studio A and Studio B And Studio A updates max weekly hours to 20 and fairness min share to 10%, max share to 25% When recommendations are generated for both studios Then Studio A's recommendations reflect the updated settings And Studio B's recommendations remain unaffected And missing settings for either studio fall back to documented platform defaults And all configuration changes are versioned with user, timestamp, and diff, and are viewable in the audit log And publishing configuration changes triggers recomputation for affected future slots within 60 seconds
Fairness Balancing for Popular Time Slots
Given fairness thresholds are configured as min share 10% and max share 25% of prime-time slots over a rolling 4-week window per instructor And the historical allocation shows Instructor I1 at 5% and I2 at 30% When generating recommendations for next week's prime-time slots Then the engine prioritizes I1 until the projected allocation meets or exceeds 10% without violating hard constraints And recommendations for I2 are down-weighted or replaced to keep the projected allocation at or below 25% And the output includes per-instructor fairness metrics (current share, projected share, min, max) and a flag FAIRNESS_ADJUSTMENT when fairness changed a ranking
Admin Override with Justification and Audit Trail
Given a studio admin with Override role selects a lower-ranked instructor for a prime-time class And the selection would violate fairness balancing but not any hard constraint When the admin confirms the override and enters a justification of at least 15 characters Then the override is applied, the assignment is updated, and the recommendation list is marked with OVERRIDE_APPLIED And an audit record is created with user, timestamp, previous recommendation, new assignment, justification text, impacted constraints, and diff And overrides attempting to violate hard constraints are blocked with an explanatory error and audit record with reason HARD_OVERRIDE_BLOCKED
Scheduling UI Integration
"As a scheduler, I want to apply Instructor Match recommendations within the calendar so that I can act quickly without leaving my workflow."
Description

Embed Instructor Match recommendations directly into the ClassTap calendar and class creation flows. Surface inline suggestions on empty or underperforming slots, with one‑click assign/apply actions, preview of expected impact, and conflict checks. Enable bulk application of recommendations, change tracking with undo, and instructor notification workflows (propose/accept/decline). Ensure mobile‑first responsiveness and parity with existing scheduling features, including double‑booking prevention and waitlist awareness.

Acceptance Criteria
Calendar Inline Suggestions for Empty and Underperforming Slots
Given the scheduler opens the calendar in day or week view with Instructor Match enabled When the system detects empty slots or slots performing ≥15% below the median fill rate for that hour over the last 8 weeks Then inline suggestion chips render on those slots within 500 ms, showing up to 3 recommended instructors with name, rating, segment match, and confidence score And each chip provides a “Why suggested” tooltip referencing historical pull, ratings, and segment alignment And no chips render on slots already assigned with projected performance ≥ baseline for that hour And slots with an active waitlist display a waitlist badge with count on the chip And all chip touch targets are ≥44x44 px and reflow correctly at 320–375 px widths without horizontal scroll
Class Creation Flow Recommendations
Given the scheduler creates a new class and selects date/time and location When the Instructor field is focused Then a Recommendations tab appears first with a ranked list of up to 5 instructors and their projected uplift (percentage range) and confidence And instructors with conflicts are disabled with a conflict badge and tooltip listing overlapping class/time/location And recommendations respect audience segment filters applied in the form And on mobile (≤375 px), the recommendations list is vertically scrollable, keyboard-safe, and interactive latency is ≤1000 ms from focus
One‑Click Apply with Conflict and Double‑Booking Prevention
Given a recommendation chip is visible for a slot When the scheduler clicks Apply on a recommended instructor Then the system performs real-time checks for instructor availability, location overlap, buffer rules, and existing double‑booking prevention policies including hybrid/online flags And if no conflicts are found, the instructor is assigned in ≤1000 ms and the slot updates with the instructor avatar/name And if conflicts or waitlist impacts are detected, the action is blocked with a modal listing each conflict (class id, time, location, waitlist count) and resolution options, and no changes are saved
Impact Preview Panel
Given a recommendation chip is visible When the scheduler selects Preview impact Then a panel displays projected booking uplift as a percentage range (e.g., +8–15%), confidence level, lookback window (e.g., 8 weeks), and top drivers (pull, ratings, segment fit) And the model version and data as‑of timestamp are shown And the preview loads in ≤300 ms or shows a non-blocking error with a retry option within 2 seconds And the Apply button is disabled until preview data is loaded or a retry is attempted
Bulk Apply Recommendations
Given multiple recommendation chips are visible across the selected date range When the scheduler multi-selects up to 50 chips and clicks Apply selected Then the system runs conflict checks per item and applies all conflict-free changes in a single operation And a results summary reports counts by status: applied, skipped‑conflict, skipped‑permission, failed‑other, with links to details And successful applications are not rolled back if others fail And the bulk operation completes in ≤5 seconds for 50 items
Change Tracking with Undo/Redo
Given the scheduler has applied one or more recommendations When the scheduler opens Change log Then each change entry shows timestamp, actor, class id, before/after instructor, source “Instructor Match,” and correlation id And clicking Undo on an entry within 24 hours reverts the assignment, restores prior state, and issues compensating notifications where applicable, all in ≤2 seconds And the undo action is recorded as a new log entry with a Redo option available for the last undone change And the log is immutable and exportable as CSV
Instructor Notification Workflow (Propose/Accept/Decline)
Given a recommendation results in a changed or proposed instructor assignment and notifications are enabled When the scheduler selects Propose to instructor Then the instructor receives an in-app and email (and SMS if configured) notification within 1 minute containing class details and Accept/Decline actions And Accept confirms the assignment, updates the schedule, and notifies the scheduler; Decline unassigns the instructor, returns the slot to suggested state, and notifies the scheduler And if no response within 24 hours, the proposal auto-expires, the scheduler is notified, and the slot remains unassigned And the slot visibly shows status badges: Proposed, Accepted, Declined, or Expired
Outcome Measurement and A/B Testing
"As a business owner, I want to measure the impact of using Instructor Match so that I can decide whether to roll it out across all locations."
Description

Introduce an experiment and reporting layer to quantify uplift from Instructor Match. Support A/B or holdout scheduling comparisons, automatic metric collection (conversion rate, attendance, revenue, no‑show rate) and segment‑level breakdowns. Provide significance testing, guardrail metrics, and experiment governance to avoid harmful degradation. Deliver dashboards and exports that attribute impact to recommendations, instructors, and time windows, enabling iterative tuning of engine weights and operational decisions.

Acceptance Criteria
A/B Schedule Experiment Creation and Randomization
Given I am an Org Admin with Instructor Match enabled And I define experiment scope (locations/classes/time window), exposure unit (class_time_slot or customer_session), variants (Control=BAU schedule, Treatment=Instructor Match), and allocation (default 50/50, configurable 1–99) And I enter pre-registration fields (hypothesis, primary metric, guardrails, MDE, alpha, planned duration, stop criteria) When I click Create Experiment Then an experiment_id is generated and status=Draft, and pre-registration fields become read-only And eligible exposure units are randomized using seeded stable hashing so that the same unit always maps to the same variant And Treatment units receive Instructor Match recommendations while Control units suppress recommendations and retain BAU schedule And SRM monitoring is enabled for the exposure unit with alert threshold p<0.001 When I click Start Then only newly exposed units after start_time are enrolled; no cross-over of previously exposed units occurs And Pausing prevents new enrollments but preserves assignments for already-enrolled units
Automatic Metric Collection: Conversion, Attendance, Revenue, No‑Show
Given an active or completed experiment with enrolled exposure units When booking, payment, check-in, cancellation, and refund events are ingested Then the platform computes per-variant metrics with definitions: - Conversion Rate = confirmed_bookings / unique_booking_page_sessions within experiment window - Attendance Rate = attended_check_ins / confirmed_bookings - Revenue (net) = sum(captured_amount) − sum(refunds) in org currency - No‑Show Rate = 1 − Attendance Rate And metrics are aggregated overall and daily with p95 end-to-end latency ≤ 15 minutes; late events backfill up to 48 hours And cancellations and refunds update metrics within 15 minutes of event And metric definitions are shown in UI tooltips and match export schema
Segment-Level Reporting and Filtering
Given experiment data is available When I filter results by audience segment (e.g., member_status, new_vs_returning, age_band if available, acquisition_channel), instructor_id, location_id, modality (in-person/online), time-of-day bucket, and weekday/weekend Then the dashboard renders per-segment variant metrics (exposures, bookings, attended, revenue_net, conversion_rate, attendance_rate, no_show_rate) and lift with 95% CIs And segments with cell counts < 20 are labeled Insufficient Data and excluded from lift/significance while still counting toward overall totals And filters can be combined and reset, and the URL reflects filter state for shareable views
Statistical Significance, MDE, and Guardrail Auto-Stop
Given the experiment has a configured primary metric, alpha (default 0.05), two-tailed test, and MDE When sufficient sample is accumulated per power assumptions Then the readout displays lift, p-value, 95% CI, decision recommendation (Win/Lose/Inconclusive), and time-to-power estimate And significance and decisions are suppressed if SRM is flagged And guardrails (configurable) default to: No‑Show Rate must not increase > 2.0pp; Revenue per visitor must not decrease > 3%; Attendance Rate must not decrease > 1.0pp (all at 95% confidence) When any guardrail breach is detected Then the experiment auto-pauses, alerts Org Admins via email/SMS, and labels status=Paused (Guardrail Breach) with a rationale log
Attribution Dashboard and CSV/Parquet Export
Given an experiment has completed or has interim data When I open the Attribution view Then I see variant lift attributed by instructor_id and by time_window bucket (e.g., 6–9am, 9–12pm, 12–3pm, 3–6pm, 6–9pm), with top/bottom performers ranked and confidence intervals displayed And only exposures that followed the assigned variant are attributed; overridden schedules are shown separately When I export results Then a CSV and Parquet file are generated within 2 minutes containing columns: experiment_id, variant, segment, instructor_id, location_id, time_window_bucket, date, exposures, bookings, attended, revenue_net, conversion_rate, attendance_rate, no_show_rate, lift, p_value, ci_lower, ci_upper And exports exclude PII (only hashed customer_id where included) and match on-screen totals within 0.1%
Experiment Governance: Pre-Registration, Roles, Audit
Given organization role policies are in place When a user with Org Admin role configures an experiment Then pre-registration requires: hypothesis, exposure unit, variants, allocation, primary metric, guardrails, MDE, alpha, start criteria, max duration, and stop criteria And only Org Admins can Start/Pause/Stop; Editors can view dashboards and exports; Viewers can view dashboards only And after Start, primary metric, allocation, exposure unit, and variants are immutable; only Pause/Resume/Stop and description are editable And all actions are captured in an immutable audit log with timestamp, user_id, action, and field diffs, exportable as CSV
Experiment Health: SRM Detection, Power Check, Data Freshness SLA
Given an experiment is Draft When I enter expected daily traffic and MDE Then a power calculator estimates required sample size and duration; Start is blocked if the estimate exceeds the configured max duration Given an experiment is Running When exposure counts drift significantly between variants beyond chance (chi-square p<0.001) Then an SRM alert is shown, significance is suppressed, and Admins are notified And a Data Freshness indicator shows current pipeline latency; if p95 latency > 15 minutes for > 30 minutes, the dashboard displays Data Delayed, and exports are temporarily disabled until recovery
Privacy and Access Controls
"As an instructor, I want assurances that my performance data is used appropriately and shared only with the right people so that I feel comfortable participating in optimization."
Description

Add role‑based access controls and privacy protections for performance analytics. Limit visibility of instructor‑level metrics to authorized roles (owners/coordinators) and provide instructors with appropriately scoped views of their own data. Implement consent and policy notices, data retention settings, and audit logs of access/overrides. Prevent exposure of sensitive attendee information through aggregation and anonymization while maintaining explainability for recommendations.

Acceptance Criteria
RBAC: Instructor-Level Analytics Visibility
Given a user with role Owner or Coordinator When they open Analytics > Instructor Performance Then they can view metrics for all instructors within their organization Given a user with role Instructor When they open Analytics > My Performance Then they can view only their own metrics and do not see any other instructor’s data in UI widgets, tables, exports, or APIs Given a user with role Instructor When they attempt to access /analytics/instructors/{otherInstructorId} via direct URL or API Then the system returns HTTP 403, shows a generic access denied message, and logs the attempt without revealing whether the target ID exists Given any unauthenticated user When accessing any analytics or recommendation explanation endpoint Then the system redirects to login (UI) or returns HTTP 401 (API) and returns no partial content Given an Owner changes a user’s role When the change is saved Then the new access scope takes effect across UI and API within 60 seconds, and cached pages reflect the new scope on next request
Scoped Exports and APIs Respect RBAC and Pseudonymize PII
Given a user with role Owner or Coordinator When they export instructor performance data Then the export can include all instructors in their organization but contains no attendee PII (name, email, phone) and replaces attendee identifiers with stable pseudonymous IDs Given a user with role Instructor When they export performance data Then the export contains only their own instructor-level metrics and excludes all other instructors Given any user requests analytics via API tokens When the token is scoped to a role and organization Then responses are limited to that scope, and attempts to query outside scope return HTTP 403 without leaking record existence Given a request includes forbidden PII fields When generating an export or API response Then the system rejects the request with a validation error listing forbidden fields and produces no file/response payload with PII
Consent and Policy Notices for Analytics and Recommendations
Given an Instructor accessing analytics or recommendation explanations for the first time When the page loads Then a consent dialog is shown with links to the current data policy and options to consent to analytics insights and to recommendation optimization; the features remain disabled until consent is recorded Given a user provides or revokes consent When they submit the consent form Then the system records consent status with user ID, policy version, timestamp, and locale; changes are auditable and reversible Given the data policy version is updated by the platform When a user next accesses analytics or explanations Then a re-consent banner is displayed and access is gated until the new policy is acknowledged Given an Owner When viewing the Consent Status report Then they can see consent status per instructor but cannot apply consent on behalf of an instructor
Configurable Data Retention and Automated Deletion
Given an Owner on Settings > Data Retention When they set attendee-level analytics retention between 90 and 730 days (default 365) Then the setting is saved per organization and displayed with the effective date Given analytics records older than the configured retention When the nightly retention job runs Then the system irreversibly deletes or anonymizes attendee-level records beyond the horizon and recomputes instructor aggregates without PII Given records are deleted or anonymized by retention When users access analytics, exports, or APIs Then no data older than the retention horizon is returned, caches are purged within 24 hours, and exports indicate the data horizon date Given a disaster recovery restore is performed When production is brought online Then retention rules are re-applied before analytics endpoints are re-enabled, preventing reappearance of out-of-retention PII
Audit Logs for Access, Role Changes, and Overrides
Given any access to instructor-level analytics pages, exports, APIs, or recommendation explanations When the action occurs Then an audit log entry is recorded with actor ID, role, tenant, action, resource identifier, channel (UI/API), timestamp (UTC), and outcome (success/denied) Given a role change or temporary access grant is performed by an Owner When the change is saved Then the audit log captures the change, including target user, new role/scope, actor, timestamp, and a required justification field if scope is expanded Given an Owner views Audit Logs When filtering by actor, resource, action, date range Then the results return within 2 seconds for up to 10k records and can be exported as CSV without PII beyond actor/resource IDs Given audit logs are stored When any user attempts to edit or delete a log entry via UI or API Then the system disallows modification (append-only), and any administrative purge is recorded as a separate audit event with reason
Anonymization and Thresholds in Analytics and Explanations
Given charts, tables, or exports that segment attendee behavior (e.g., segment, time slot, location) When a segment contains fewer than k=5 distinct attendees within the reporting window Then the value is masked (e.g., '≤4'), excluded from exports, and does not display any breakdown that could re-identify individuals Given recommendation explanations for Instructor Match When shown to any role Then the explanations reference only aggregated, non-PII insights (e.g., 'weekday conversion 18% above org average over last 90 days') and never include attendee identifiers or free-text that could reveal identity Given multiple small segments could be combined to infer identities When rendering analytics Then the system applies suppression rules to prevent differencing attacks (e.g., suppressing complementary cells that would reveal masked counts)
Tenant Isolation for Analytics and Instructor Match
Given a user authenticated under Organization A When they attempt to access analytics, exports, or explanations for Organization B via UI or API Then the system returns HTTP 403 and logs the attempt, without indicating whether Organization B or resources exist Given an instructor who belongs to multiple organizations When they switch the active organization context via the org switcher Then all analytics, exports, and explanations immediately reflect only the selected organization with no cross-tenant aggregation Given any export or audit log generated When the file is created Then it is labeled with the organization identifier and contains no records from other organizations

GapFill

Identify idle 30–90 minute room gaps and auto-generate pop-up sessions that fit emerging demand pockets. GapFill suggests class formats, titles, and pricing with an expected revenue delta vs. leaving the slot empty, so you can monetize downtime in two clicks.

Requirements

Smart Gap Detection (30–90 Min)
"As an operations manager, I want the system to identify viable 30–90-minute gaps across rooms and instructors so that I can monetize idle time without scheduling conflicts."
Description

Detect continuous idle slots between 30 and 90 minutes across rooms and instructors by scanning the live ClassTap schedule. Apply business rules for setup/teardown buffers, instructor availability, equipment constraints, operating hours, and blackout dates to ensure only viable gaps are surfaced. Update in near real time on new bookings, cancellations, or overrides; prevent double-bookings via atomic holds and conflict checks. Expose a stable API and UI feed of candidate gaps to downstream GapFill modules. Support timezone correctness, multi-location calendars, and recurring events. Log detection decisions for audit and tuning.

Acceptance Criteria
30–90 Minute Gap Detection With Operational Constraints
Given a live schedule with room bookings, setup/teardown buffers, operating hours, instructor availability, equipment assignments, and blackout dates When the gap detection job scans the schedule Then it returns only continuous idle periods per room that are >= 30 minutes and <= 90 minutes (inclusive) And all returned gaps respect configured setup/teardown buffers on adjacent events And no gap overlaps operating hour boundaries or blackout dates And assigned instructor(s) and required equipment are available for the entire gap And gaps are computed independently per room and do not span rooms And each gap includes start/end timestamps, locationId, roomId, timezone, min/max capacity, and constraint summary
Near Real-Time Gap Updates on Schedule Changes
Given the system is actively monitoring schedule events When a new booking, cancellation, reschedule, or manual override is committed Then all impacted candidate gaps are added/updated/removed in the API and UI feeds within 10 seconds And no stale gap remains visible more than 10 seconds after becoming invalid And gap recalculation is idempotent and does not duplicate gaps across repeated events And event processing order preserves causality per room (no reappearance of removed gaps)
Atomic Holds and Conflict Prevention During Gap Claim
Given a candidate gap is visible in the feed When a downstream module requests a hold on that gap Then an atomic hold is created with a 2-minute TTL and a unique holdId And concurrent hold requests for overlapping time/room are rejected with a conflict error And confirming a held gap revalidates conflicts and converts it to a booking atomically And releasing or expiring a hold makes the gap re-eligible within 5 seconds And in a race test of 50 concurrent hold attempts on the same gap, exactly one succeeds and no double-booking occurs
Timezone and Multi-Location Calendar Correctness
Given multiple locations with distinct timezones (including DST-observing and non-DST) When gaps are detected for a date range that includes DST transitions Then gap start/end are computed in each location’s canonical IANA timezone And gaps do not shift by ±1 hour across DST changes (spring forward/fall back handled correctly) And the API returns timezone per gap and ISO 8601 timestamps with offsets And the UI displays gaps in the user’s selected display timezone without altering server-side conflict logic
Recurring Events, Exceptions, Buffers, and Blackout Enforcement
Given recurring classes with exceptions and room/instructor buffers configured When the detector expands the recurrence for the target window Then exceptions are honored and excluded from availability calculations And setup/teardown buffers are applied on both sides of each occurrence before computing gaps And gaps shortened by buffers below 30 minutes are excluded; gaps above 90 minutes are split or excluded per rule, never reported as 91+ minutes And blackout dates/times suppress gaps entirely within those windows
Stable API and UI Feed of Candidate Gaps
Given clients query the gaps service When calling GET /v1/gaps with filters (locationId, roomId, instructorId, dateRange, minDuration, maxDuration) Then the response uses a versioned, backward-compatible schema with stable field names and types And results are paginated (cursor-based), deterministically ordered by start time then roomId And each gap has a stable gapId for idempotency across reads And ETag/If-None-Match supports conditional GET with 200/304 semantics And the UI feed consumes the same API and renders the exact set returned
Decision Logging for Audit and Tuning
Given gap detection evaluates schedule intervals When a gap is accepted or rejected Then a decision log entry is written with timestamp, correlationId, locationId, roomId, evaluation window, applied rules, and reasons And no PII (e.g., student names, payment data) is persisted in logs And logs are queryable by time range, location, room, instructor, and gapId and exportable as CSV/JSON And retention is at least 90 days with secure access controls And log write failures surface metrics/alerts without blocking core detection
Demand Forecast & Class Suggestions
"As a studio owner, I want data-driven suggestions for what class to run in each gap so that I can maximize attendance and revenue."
Description

Generate data-driven class format and title suggestions for each eligible gap using signals: historical attendance by time/day, search/landing activity, waitlists, booking velocity, instructor popularity, and seasonality. Provide ranked suggestions with rationale, expected attendance range, and target audience. Fall back to catalog heuristics when data is sparse. Integrate with ClassTap’s class templates and taxonomy; respect instructor qualifications and room capabilities. Allow operator configuration with tunable weights and exclusions.

Acceptance Criteria
Ranked Suggestions Generated for an Eligible Gap
Given an eligible 30–90 minute room gap with assigned location, room, and calendar context And historical attendance, search/landing activity, waitlists, booking velocity, instructor popularity, and seasonality signals are available When the operator requests GapFill suggestions for the gap Then the system returns at least 3 and at most 10 suggestions ranked by descending score And each suggestion includes a class format and title And scores are deterministic for identical inputs within a ±0.5% tolerance And the response is returned within 2 seconds for 95th percentile requests
Suggestion Details: Rationale, Attendance Range, Target Audience
Given suggestions have been generated for a gap When the operator views the suggestion list Then each suggestion displays a rationale summarizing the top 3 contributing signals with their normalized weight percentages And each suggestion shows an expected attendance range as an 80% prediction interval (min and max integers) And the expected attendance max does not exceed room capacity And each suggestion includes 1–3 target audience segments drawn from the taxonomy
Data-Sparse Fallback to Catalog Heuristics
Given a gap has insufficient data signals defined as: fewer than 2 historical sessions in the same day-of-week and time band in the past 90 days AND no active waitlist AND fewer than 50 relevant searches/landings in the past 30 days When GapFill is invoked for that gap Then the system generates suggestions using catalog heuristics and templates And each heuristic suggestion is labeled with source = "Heuristic" And the system still returns at least 3 suggestions if catalog content exists And rationale cites the heuristic rules applied instead of data-driven weights
Constraint Enforcement: Instructor Qualifications and Room Capabilities
Given instructor qualifications, availability, and room capabilities are defined in ClassTap When suggestions are generated for a gap Then only instructors who are qualified for the suggested format and available in the gap window are proposed And only rooms that meet equipment and capacity requirements for the format are used And suggestions violating any qualification or capability constraint are excluded And hybrid formats are only suggested for rooms with required streaming capability
Operator Configuration: Tunable Weights and Exclusions
Given the operator configures signal weights (0.0–1.0 per signal) and optional exclusions (formats, instructors, tags) When suggestions are generated for any gap Then the scoring model uses the configured weights normalized to sum to 1.0 And any excluded formats, instructors, or tags do not appear in results And changes to weights or exclusions take effect on the next generation request without redeploy And the operator can reset to system defaults in a single action
Integration with Class Templates and Taxonomy
Given class templates and taxonomy tags exist in ClassTap When a suggestion is generated Then its class format maps to an existing template ID And the suggested title is produced using the template naming rules plus time-of-day/level modifiers where applicable And taxonomy tags (format, level, audience) are attached to the suggestion And publishing a suggestion creates a draft pop-up session with template fields pre-filled
Dynamic Pricing & Revenue Delta
"As a business owner, I want a recommended price and clear revenue impact for each pop-up so that I can decide quickly whether to publish."
Description

Compute recommended price and expected revenue delta versus leaving the slot empty for each suggestion. Incorporate capacity, forecasted attendance, instructor payout rates, room costs, fees, taxes, and discounts. Show confidence bands and breakeven occupancy. Support multi-currency, inclusive/exclusive tax modes, and promo stacking rules. Persist inputs and calculations for audit; expose API for price override with real-time recalculation.

Acceptance Criteria
Recommended Price and Revenue Delta Calculation
Given a suggestion with capacity 20, forecasted paid attendance 12, room cost 30.00 USD, instructor payout 5.00 USD per attendee, platform fee 3% of pre-tax price, tax rate 10% (tax-exclusive), and no discounts When the recommended price is evaluated at 15.00 USD Then the expected revenue delta equals 84.60 USD calculated as 12*(15.00) - 12*(0.03*15.00) - 30.00 - 12*(5.00) and is displayed as 84.60 USD with 2-decimal rounding And the delta updates within 200 ms when any input value changes And the recommended price output is non-negative and rounded to the currency minor unit And the displayed delta is clipped to capacity-constrained forecasted attendance (i.e., min(forecast, capacity))
Confidence Bands and Breakeven Occupancy Display
Given a forecast with attendance quantiles q10=8, q50=12, q90=16 for a class with capacity 20 and a computed recommended price When confidence bands are shown Then three bands are displayed as Low (q10), Most Likely (q50), and High (q90) with integer attendee counts and corresponding revenue delta ranges computed at each quantile And the breakeven occupancy is displayed as the smallest integer attendee count at which revenue delta >= 0, along with its percentage of capacity And if breakeven occupancy exceeds capacity, an Unprofitable at any fill indicator is shown
Multi-Currency and Tax Modes Support
Given session currency EUR with 2 minor units and tax-inclusive mode VAT 20% When the recommended price is 18.00 EUR (tax-inclusive) Then the net-of-tax base used for costs/fees is 15.00 EUR and all displays use EUR with locale-appropriate symbol/format Given session currency JPY with 0 minor units When price and deltas are computed Then all amounts are rounded to whole JPY with bankers rounding disabled (standard half-up) and no fractional values appear Given a room cost of 100.00 USD and session currency GBP with FX rate 0.8000 timestamped T When calculations run Then the converted room cost is 80.00 GBP using the stored rate at T and the FX rate and timestamp are persisted with the calculation
Promo Stacking Rules Application
Given a base price 20.00 EUR, Promo A = 10% off (stackable), Promo B = 5.00 EUR off (stackable), max total discount cap = 40%, price floor = 5.00 EUR, tax-exclusive mode VAT 20% When promos are applied per stacking order percentage-before-fixed Then the effective pre-tax price is 13.00 EUR (20*0.90 - 5.00) and does not breach the price floor or discount cap And taxes are computed on the discounted base per tax mode and displayed separately Given a non-stackable Promo C = 25% off and Promo D = 10% off (non-stackable with C) When both are present Then only the higher-value applicable promo is applied per combinability rules And the final effective price never drops below zero
Audit Persistence of Pricing Inputs and Outputs
Given a pricing calculation is performed for a suggestion When the result is produced Then an immutable audit record is created within 1 second containing: input snapshot (capacity, forecast, payout configuration, room costs, fees, taxes, discounts/promos, tax mode, currency), FX rates and timestamps used, formula/version identifiers, recommended price, confidence bands, breakeven, expected revenue delta, user/context identifiers, and a unique calculation ID And the audit record is retrievable by calculation ID and suggestion ID via admin API and includes a reproducibility flag confirming all required data to recompute is present And any subsequent recalculation creates a new versioned audit record without altering prior records
Price Override API and Real-Time Recalculation
Given an authenticated client with permission to manage GapFill pricing When it POSTs to the price override endpoint with suggestion_id, override_price, currency, and reason_code Then the service validates currency/minor units and business rules, persists the override with the audit trail, recalculates revenue delta and confidence bands in real time, and returns the updated values within 500 ms p95 And the endpoint is idempotent on (suggestion_id, override_price) for 10 minutes, returning the same calculation_id on repeat requests And a domain event PriceOverridden is emitted with the calculation_id and diff
Edge Cases and Error Handling for Pricing
Given forecasted paid attendance is 0 and there are fixed room costs When calculations run Then expected revenue delta equals the negative of fixed session costs and is displayed as a loss Given forecasted paid attendance exceeds capacity When calculations run Then expected attendance used in calculations is capped at capacity Given instructor payout is configured as either a flat per-attendee amount or a percentage of net-of-tax revenue When calculations run Then both payout modes are supported and correctly applied And any computed tax, fee, or price that would be negative is clamped to zero and a warning badge is displayed
Auto-Generate Session Details
"As an instructor, I want session details auto-filled to fit the gap so that I can publish with minimal edits."
Description

Auto-generate a complete draft session pre-filled to fit within the selected gap: title, description, cover image, duration honoring buffers, capacity, room, instructor, equipment, enrollment window, cancellation policy, and payment settings. Ensure compliance with brand themes and white-label settings to produce a ready-to-publish booking page. Validate against conflicts and policy constraints before allowing publish. Allow quick edits with guardrails to keep within the gap.

Acceptance Criteria
Auto-generate complete draft within selected gap
Given a selected room gap between 30 and 90 minutes with defined pre- and post-class buffers And room, instructor, and equipment availability are known When the user clicks "Auto-generate session" Then the system creates a draft session with: title, description, cover image, duration that fits within the gap while honoring buffers, capacity within room maximum, assigned room, instructor, required equipment, enrollment window, cancellation policy, and payment settings pre-filled And the draft start and end times fall strictly within the gap boundaries after applying buffers And duration complies with org min/max class length policies And the draft is saved with a unique ID and editable state
Pre-publish conflict and policy validation
Given a generated draft session exists When the user attempts to publish Then the system validates and confirms: no room/instructor/equipment conflicts, no overlapping sessions, blackout dates respected, facility hours respected, and org policies met (min lead time, max daily hours per instructor, capacity <= room max, age restrictions if applicable) And if any validation fails, publishing is blocked and each violation is displayed inline with field-level messages and suggested fixes And if all validations pass, the session is published successfully and confirmation is returned within 3 seconds on a standard network And a booking inventory lock is created to prevent double-booking
Brand and white-label compliance on generated session
Given the organization has configured theme tokens (colors, typography), logo, approved image sources, content style, and white-label domain When the session is auto-generated Then generated title and description comply with content style rules (no banned terms, proper casing) And the cover image is selected from approved sources and meets 16:9 aspect ratio and <= 500 KB size And all booking page components use the org theme tokens and show no platform branding And the booking page preview URL is under the org’s white-label domain And text contrast on the booking page meets WCAG AA (>= 4.5:1 for normal text)
Guardrailed quick edits stay within gap constraints
Given a draft session is open in the editor When the user adjusts start time or duration Then the editor prevents values that exceed the gap boundaries after buffers and provides real-time validation messages And time inputs snap to 5-minute increments And capacity cannot be set above room capacity; attempts show an error and block save And changing room or instructor triggers immediate availability re-check; conflicts block save And while any validation error exists, the Publish action remains disabled
Payment and enrollment settings pre-filled from policy
Given org payment configuration, pricing rules, taxes, and cancellation policy are defined When the session is auto-generated Then payment settings are pre-filled with correct currency, tax rate, fee handling (absorb/pass-through), and refund window per cancellation policy And enrollment open/close times respect policy min lead time and latest enrollment cutoff and do not precede current time And the suggested price falls within allowed min/max; if out of bounds it is adjusted to the nearest allowed value and flagged for review And if no active payment processor is connected, the draft is created but publish is blocked with a prompt to connect payments
Ready-to-publish booking page preview
Given a draft session exists When the user opens Preview Then a private preview page renders with all session details (title, description, image, time, duration, capacity, price, policy, instructor, room, equipment) And the preview loads within 2.5 seconds on a standard 4G connection And waitlist controls appear when capacity is set and waitlist is enabled by policy And upon publishing, the preview URL becomes public without changing the content except the publish state indicator
Two-Click Publish & Sync
"As a studio manager, I want to publish a suggested pop-up in two clicks with calendars and booking pages synced so that I can act quickly."
Description

Enable publishing of a GapFill session in two clicks: review suggestion, confirm price, publish. On publish, atomically create the session, hold the room/instructor, generate the branded booking page and SEO-safe URL, sync to all calendars (web, iCal, Google), and enable secure payments. Provide a reversible draft state and a one-click unpublish within a grace window. Emit webhooks and analytics events for downstream systems.

Acceptance Criteria
Two-Click Publish from Suggestion Card
Given a GapFill suggestion is in Draft and has a valid instructor and room assignment When the user clicks “Review & Confirm Price” on the suggestion card and then clicks “Publish” without editing any fields Then the session is published in no more than 2 clicks total from the suggestion card And no intermediate confirmation modal is required And keyboard input is not required And the UI displays a success state within 2 seconds of the Publish click
Atomic Publish Transaction and Rollback
Given a GapFill suggestion is published When the publish action starts Then the following operations complete atomically in a single transaction: session record created; room and instructor are held; branded booking page generated; SEO-safe URL generated; payments enabled; calendar sync jobs queued And if any operation fails, all changes are rolled back and no session, hold, page, or URL remains And the user receives a single error response with a human-readable reason and a unique error ID And re-clicking Publish within 2 minutes with the same idempotency key results in exactly one session created
SEO-Safe Booking Page Generation
Given a session is successfully published When the booking page is generated Then the page is publicly reachable over HTTPS and returns HTTP 200 within 2 seconds And the URL slug is lowercase, hyphenated, <= 60 characters, unique, and includes a normalized class title and start datetime And the page includes canonical URL, meta title, meta description, and Open Graph tags derived from the session And navigating to the slug prior to publish returns 404 and after unpublish returns 410 within 5 minutes
Calendar Sync and Double-Booking Prevention
Given a session is being published When the system attempts to hold the room and instructor for the scheduled time Then if a conflict exists with any existing booking, the publish is blocked with a clear conflict message and no changes are committed And if no conflict exists, the hold is placed and the publish proceeds And on success, the session appears on the embedded web calendar within 1 second, in the iCal feed within 60 seconds, and in connected Google Calendars within 60 seconds
Secure Payments Enabled on Publish
Given a session is successfully published and the organization has a connected payment processor When a user opens the booking page Then the checkout is enabled and served over HTTPS with TLS 1.2+ and HSTS And card entry fields are hosted or tokenized by a PCI DSS Level 1 provider And a test purchase using a valid test card authorizes and captures successfully and returns a receipt event And if no payment processor is connected, the publish is blocked with an actionable error
Draft State and One-Click Unpublish Grace Window
Given a session was just published and has no bookings When the user clicks “Unpublish” within the grace window of 15 minutes from publish time Then the session status reverts to Draft with a single click And the booking page is removed and the URL begins returning HTTP 410 within 5 minutes And room and instructor holds are released And calendar entries are removed or marked canceled across web, iCal, and Google within 60 seconds And if the first booking occurs or the grace window expires, the Unpublish action is disabled and the user is directed to the standard cancellation flow
Webhooks and Analytics Events on Publish/Unpublish
Given a publish or unpublish completes (success or failure) When events are emitted Then webhooks are delivered within 5 seconds with retries using exponential backoff up to 24 hours and include idempotency keys And the payload includes session_id, org_id, action (published|unpublished|failed), timestamp (ISO-8601), actor_id, price, start_at, end_at, room_id, instructor_id, and URL (if applicable) And analytics events are recorded with the same fields and can be queried within 1 minute And duplicate deliveries are safely handled by consumers via the idempotency key
Waitlist Tap & Targeted Notifications
"As a coordinator, I want GapFill to notify waitlisted and likely attendees automatically so that the pop-up fills fast with minimal manual outreach."
Description

Automatically target interested customers to fill the pop-up: surface relevant waitlists and high-intent segments, generate SMS/email invites with personalized links and optional incentives, and throttle sends to prevent spam. Respect opt-in and regional messaging compliance. Track deliveries, clicks, and conversions; auto-close the campaign when capacity is met. Support RSVP holds and priority access windows for waitlisted users.

Acceptance Criteria
Relevant Waitlists & High-Intent Segments Surfaced for GapFill
Given a draft GapFill session with category, location, start time, duration, skill level, and capacity When the system suggests target audiences Then it proposes segments that include: (a) users waitlisted in the past 60 days for classes with the same category or instructor within 10 miles of the location, (b) users who clicked “Notify me” for similar classes in the past 90 days, and (c) past attendees with high engagement in the category in the past 6 months And it excludes users who are opted out of the selected channel, already booked or on the roster for an overlapping time, or previously messaged by this campaign And it deduplicates contacts across segments with precedence: waitlist > notify-me > past attendees And it displays per-segment counts, inclusion rules, and top 3 reason codes And it estimates expected fill probability and revenue lift for the campaign
Personalized Invites with Deep Links and Optional Incentives
Given segments are selected and an optional incentive (% off, $ off, or limited-quantity promo) is configured When generating SMS and email invites Then each message includes the session title, start time localized to the recipient timezone, venue/meeting link, price (post-incentive), remaining seats, and a unique trackable deep link that preselects the session and applies the incentive And SMS bodies target <=160 GSM-7 characters; if longer, concatenation is indicated and estimated cost is shown And email subject and preview text are personalized with the recipient first name and session title And a test-send to a sample recipient renders correct personalization, deep link attribution, and incentive application And the landing page is brand-themed and reflects the correct price and capacity upon click-through
Throttling & Messaging Compliance
Given a send schedule is started for selected channels When dispatching messages Then only contacts with valid, recorded channel-specific consent are sent to, with consent source and timestamp stored And quiet hours are enforced per recipient timezone (default 20:00–08:00, org-configurable) And regional compliance rules are applied based on recipient country, including required sender ID and opt-out language And per-contact frequency caps are enforced (default max 3 marketing SMS and 3 marketing emails per 7 days, org-configurable) And throughput throttling is applied to respect provider limits and org-configured rates And blocked or deferred messages are logged with reason codes and are excluded from send counts
Delivery, Click, and Conversion Attribution
Given campaign messages have been sent When delivery and recipient interactions occur Then for each contact and channel the system records delivery status, provider message ID, and timestamp And clicks on the tracking link are captured with contact and campaign attribution And a conversion is recorded when a booking for the target session is completed within 24 hours of the last click or last send, whichever is later And the campaign dashboard displays: sent, delivered, failed, clicked, CTR, booked, conversion rate, attributed revenue, and revenue lift vs. empty slot, with data freshness <=5 minutes And metrics are exportable to CSV with the same aggregations
Auto-Close Campaign on Capacity Met
Given the campaign is active When remaining seats (including RSVP holds) reach 0 Then the campaign status changes to Closed, pending queued messages are canceled, and no further sends are initiated And tracking links route to a Sold Out page with an option to join the waitlist And if seats later free up (e.g., hold expiry), reopening requires an explicit admin action and is logged And an audit log entry records the auto-close event with timestamp and triggering seat change
RSVP Holds & Priority Access for Waitlisted Users
Given waitlisted users are targeted with a priority window (e.g., 30 minutes) When invites are sent Then a seat hold is created per invited waitlisted user up to the session capacity And held seats reduce publicly available inventory during the window And the RSVP link allows one-tap claim of the held seat and proceeds to checkout with the hold applied And holds auto-expire at window end, releasing seats back to inventory And all hold creation, claim, expiry, and release events are logged and visible on the roster timeline
Conflict, Dedupe, and Channel Selection
Given a recipient appears in multiple segments and has multiple opted-in channels When preparing the final audience Then the system deduplicates recipients and selects a single channel per recipient based on configurable priority and recent engagement (e.g., choose the channel with higher last-90-day CTR) And recipients with overlapping bookings or instructor assignments at the session time are excluded And recipients who received a similar promotion in the last 24 hours are suppressed And a suppression summary displays counts by reason (conflict, frequency cap, opt-out, regional block)

Product Ideas

Innovative concepts that could enhance this product's value proposition.

TapPass Express

Save cards and enable Apple/Google Pay for one-tap rebooking, completing checkout in under 10 seconds and boosting repeat conversions.

Idea

Link Locker

Issue unique expiring join links tied to each booking; auto-rotate before class and block reused links, cutting link-sharing and late support pings.

Idea

Family Flex Profiles

Let caregivers create child profiles, share payment methods, and apply age-based eligibility and waivers in one flow, removing friction for multi-kid bookings.

Idea

Roster QR Roll-Call

Display a class QR; attendees scan to check in, instantly updating rosters and triggering waitlist auto-fill or credit rules.

Idea

Prereq Gatekeeper

Enforce prerequisites by requiring proof upload or prior class completion before booking, with a drag-and-drop rule builder for admins.

Idea

Smart Fill Waitlist

Auto-promote waitlisters via timed SMS with pay-to-claim links and a 10-minute hold window, keeping seats filled without staff juggling.

Idea

Heatmap Hours

Forecast high-conversion class times from past attendance and local calendars, suggesting slots that raise utilization by surfacing demand pockets.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

P

ClassTap launches Lightning Checkout suite with Turbo Rebook, Wallet Hand‑Off, and Smart Seat Hold to boost repeat bookings and revenue

Imagined Press Article

San Francisco, CA — ClassTap today announced Lightning Checkout, a streamlined booking and payment experience designed to help independent instructors, boutique studios, and community programs convert more visits in less time. The suite combines Turbo Rebook, Wallet Hand‑Off, Smart Seat Hold, One‑Tap Upsells, and FastAuth 3DS to deliver a trusted, 10‑second checkout that prevents double‑bookings, lifts average order value, and cuts front‑desk workload in half. Lightning Checkout addresses the two most critical moments in the class booking journey: the repeat decision and the payment confirmation. With Turbo Rebook, returning attendees see a prominent one‑tap button across class pages, past bookings, receipts, and reminders. The system pre‑selects the same class time, instructor, attendees, and preferred payment method so users can confirm in under 10 seconds. Wallet Hand‑Off deep links open directly into Apple Pay or Google Pay on trusted devices, eliminating the need to log in or re‑type card details. Meanwhile, FastAuth 3DS uses risk‑adaptive authentication to keep taps instant while preserving high approval rates and preventing false declines. “Studios don’t have a conversion problem; they have a friction problem,” said Maya Chen, CEO of ClassTap. “Lightning Checkout removes the tiny speed bumps that derail mobile bookings. When rebooking is one tap and payment feels familiar and secure, you see more filled rosters and happier customers with less staff effort.” Smart Seat Hold automatically holds a seat for 60 seconds the moment the wallet sheet opens, preventing double‑bookings during peak demand. If a user abandons, the hold expires cleanly and cascades to the next shopper or waitlister. One‑Tap Upsells lets providers offer lightweight add‑ons—like mat rentals, guest passes, or class packs—right inside the wallet step with a simple toggle, increasing average order value without adding friction or redirecting away from the flow. Early access customers report faster lines and fewer manual fixes. “We used to get calls about declined cards or duplicate bookings at 5:55 pm,” said a boutique studio manager in Austin. “With Wallet Hand‑Off and Smart Seat Hold, the app handles those edge cases quietly. We’ve already noticed fewer no‑shows and a clearer roster before class starts.” Lightning Checkout extends beyond the moment of purchase to keep classes full. Reminder Paylinks include secure, expiring TapPass links that open directly to one‑tap confirm, supporting quick rebooks, waitlist promotions, and last‑minute fills. Smart Expiry adjusts each booking’s join‑link validity window to open just before class and expire shortly after, with time zone awareness and class‑type rules—preventing early link leaks and late access issues. For hybrid and virtual sessions, Device Lock and Silent Rotate protect access by binding links to the attendee’s verified device and rotating tokens just before start time, automatically updating the same SMS, email, and calendar threads. “Instructors want the confidence that when they see a ‘confirmed’ status, it really means confirmed,” added Chen. “Our combination of seat holds, link security, and adaptive authentication ensures rosters are clean, payments are approved, and attendees get in quickly without confusing steps.” Key components of Lightning Checkout include: - Turbo Rebook for one‑tap returns across class pages, receipts, and reminders - Wallet Hand‑Off to native Apple Pay/Google Pay on trusted devices - FastAuth 3DS for intelligent, low‑friction authentication - Smart Seat Hold to prevent double‑bookings at peak times - One‑Tap Upsells for add‑ons inside the wallet sheet - Reminder Paylinks to drive last‑minute fills and rebooks - Smart Expiry, Device Lock, and Silent Rotate to secure hybrid join links Availability and pricing Lightning Checkout is available today to all ClassTap customers on the Core and Pro plans at no additional cost. One‑Tap Upsells and Device Lock are included in Pro. Existing customers can enable the suite under Settings > Payments & Checkout. New customers can launch a branded booking page in under two minutes and begin accepting secure payments immediately. Roadmap and compatibility Lightning Checkout works on mobile web and desktop, and supports Apple Pay and Google Pay on eligible devices and browsers. Reminder Paylinks and rebook prompts are compatible with SMS, email, and calendar notifications. ClassTap will expand upsell options and add more granular A/B controls in upcoming releases. Customer impact Independent instructors, boutique studio managers, and front‑desk teams can expect to halve admin time associated with payment issues, reduce double‑bookings, and lift repeat conversions. Repeat attendees benefit from a trusted, familiar wallet flow and a clear one‑tap path to rebook the same class pattern they love. About ClassTap ClassTap is a lightweight white‑label class‑booking platform for independent instructors, community centers, and small studios running in‑person or hybrid classes. It handles scheduling, secure payments, waitlists, and automated SMS/email reminders while preventing double‑bookings. Providers generate branded booking pages in under two minutes, cut no‑shows, and halve admin time with tools like Turbo Rebook, Wallet Hand‑Off, and smart roster controls. Media contact Media: press@classtap.com Phone: +1 (415) 555‑0137 Website: https://www.classtap.com/press Forward‑looking statements This press release may contain forward‑looking statements regarding product features and availability. Actual results may differ based on technical, regulatory, or operational factors.

P

ClassTap unveils Family Suite to simplify youth and family bookings with Family Cart, AgeSmart Filter, and Guardian eConsent

Imagined Press Article

San Francisco, CA — ClassTap today introduced Family Suite, a set of tools built for youth programs, camps, and family‑oriented studios that need faster multi‑child booking, reliable eligibility checks, and frictionless day‑of check‑in. Family Suite combines Family Cart, AgeSmart Filter, Guardian eConsent, Household Wallet, Co‑Guardian Roles, Sibling Swap, and Family Waitlist with powerful front‑desk scanning and offline‑ready operations to keep lines moving and rosters compliant. Busy caregivers often manage multiple schedules, policies, and price rules at once. Family Cart lets parents book several children in a single, streamlined checkout. Each child gets an individualized eligibility check, pricing, and add‑ons, and the system applies family discounts automatically. Seat holds apply per child to prevent double‑bookings, and everyone lands on the correct roster with one organized confirmation. AgeSmart Filter shows only classes each child is eligible for based on age, grade, or prerequisites, with clear reasons when a class isn’t a fit and smart suggestions for the nearest eligible option. “Families are the most motivated customers—and the most time‑pressed,” said Maya Chen, CEO of ClassTap. “We designed Family Suite to remove complexity from their path. When a caregiver can book two kids, apply the right waivers, pay once with Apple Pay or Google Pay, and trust they’ll breeze through check‑in, attendance stays high and staff stress drops.” Guardian eConsent handles reusable e‑sign waivers and consents per child—liability, photo release, medical, and policies—with dual‑guardian signatures when required. The system manages expiry, renewal reminders, and step‑up prompts inside reminder messages to keep rosters compliant without last‑minute paperwork. Household Wallet lets guardians share payment methods with optional spend caps per child and approval rules for higher‑cost items, automatically tagging receipts by child for reimbursements. Co‑Guardian Roles allow another caregiver to be invited with granular permissions to book, pay, view health notes, receive reminders, or manage pickups, while both see a unified family schedule. “Before ClassTap, our front desk felt like air traffic control,” said a community program coordinator in Chicago. “Now parents see only eligible classes, sign consents once, and we can check in siblings together. The queue just… moves.” When plans change, Sibling Swap makes it easy to transfer a booked seat from one child to another without calls or spreadsheets. Eligibility and consents are rechecked automatically, price differences are handled as a quick credit or charge, and rosters plus reminders update instantly—preventing no‑shows and keeping families flexible. Family Waitlist supports “keep together” or “okay to split,” prioritizing siblings fairly. When spots open, seats are held per child and sent via timed pay‑to‑claim links. Family Suite also streamlines day‑of operations. QuickScan Kiosk presents a high‑contrast view for continuous scanning with instant green/red feedback and live counts. Family Auto‑Scan checks in linked family members with one scan, applying per‑child eligibility and consent checks, flagging exceptions, and letting staff approve or deny specific children in one tap. Offline Sync Queue ensures check‑ins continue even when Wi‑Fi drops, queuing locally and syncing with conflict resolution to prevent duplicates. Late Window Rules let providers set grace periods and cutoffs that auto‑mark late or no‑show, triggering policy outcomes—credit return, pass decrement, fee, or waitlist release—without manual reconciliation. To protect integrity at the door, Dynamic QR Shield rotates the class QR every few seconds and binds it to session, location, and time window to block screenshots and early scans. Reuse attempts trigger a friendly help prompt for the attendee and an alert for staff. Doorline Waitlist adds a lightweight poster QR so walk‑ins can join a waitlist with a quick profile and payment hold. When a no‑show is confirmed, the next in line gets a pay‑to‑claim text and is auto‑added on payment—filling last‑minute seats without front‑desk juggling. “Parents appreciate clarity and speed, and staff need predictable rules that apply consistently,” added Chen. “Family Suite pairs guidance with guardrails so bookings are easy and check‑ins are fair.” Availability and pricing Family Suite is available today on ClassTap Pro. Family Cart, AgeSmart Filter, Guardian eConsent, Household Wallet, and Co‑Guardian Roles are included. Sibling Swap, Family Waitlist, QuickScan Kiosk, and Family Auto‑Scan are add‑ons that can be enabled per location. Dynamic QR Shield, Offline Sync Queue, and Late Window Rules are included with the Check‑In add‑on. Who it’s for - Community Program Coordinators running recurring youth classes and workshops - Boutique Studio Managers offering kids programs or family memberships - Parent and caregiver bookers juggling multiple children and schedules - Front‑Desk Bookers handling walk‑ins, phone bookings, and day‑of exceptions Onboarding and migration Existing ClassTap customers can enable Family Suite under Settings > Families & Youth. CSV importers are available for family profiles, consent statuses, and membership packs. For new customers, ClassTap offers guided setup to publish a branded booking page in under two minutes and bulk‑load families from existing systems. About ClassTap ClassTap is a lightweight white‑label class‑booking platform that handles scheduling, secure payments, waitlists, and automated SMS/email reminders for in‑person and hybrid classes. Providers prevent double‑bookings, halve admin time, cut no‑shows, and generate branded booking pages in under two minutes. Family Suite extends ClassTap’s mission of making participation simple for every household. Media contact Media: press@classtap.com Phone: +1 (415) 555‑0137 Website: https://www.classtap.com/press Forward‑looking statements This press release may contain forward‑looking statements. Features and availability are subject to change and may vary by region or payment provider.

P

ClassTap introduces Compliance and Pathways to streamline accredited training with Rule Canvas, Cert Snap, and Compliance Ledger

Imagined Press Article

San Francisco, CA — ClassTap today launched Compliance and Pathways, a purpose‑built solution for accredited training, certifications, and classes with prerequisites. The release combines a visual Rule Canvas, fast mobile proof capture with Cert Snap, clear booking guidance via Fit Check, and program‑level visibility through Compliance Ledger. With Pathway Tracker and Gated Waitlist, programs can turn complex requirements into a guided journey that keeps rosters eligible and full without manual oversight. Many education and training providers still manage prerequisites with spreadsheets, email chains, and last‑minute document collection. That leads to misbookings, frustrated learners, and compliance risks. ClassTap’s Compliance and Pathways replaces ad hoc checks with transparent, testable rules and reusable documentation that travels with the learner across bookings. “Administrators need two things: certainty and simplicity,” said Maya Chen, CEO of ClassTap. “Compliance and Pathways brings both. Rules are defined visibly and evaluated consistently, while learners get a straightforward explanation and a one‑tap path to qualify.” Rule Canvas is a drag‑and‑drop builder that lets admins combine blocks like Prior Class Completion, Certification Valid Within X Months, Age/Grade, Waiver on File, and Instructor Approval. A test mode and inline previews show who qualifies before publishing, cutting manual checks and keeping rules consistent across classes and locations without code. Timed Override supports controlled exceptions with reason codes, scoped to a specific class or series, and set to auto‑expire. Rosters display a subtle badge and follow‑up tasks such as “Upload cert by 09/30.” Cert Snap modernizes document collection at the moment of intent. Learners snap or upload certification cards on mobile; ClassTap auto‑reads name, issue and expiry dates, and provider. If the document matches the rule set, it is auto‑approved. If not, it routes to quick review with suggested next steps. Documents are stored once and reused across bookings, with renewal reminders baked into SMS and email so learners stay in good standing. Fit Check sits on class pages and paywalls to explain what is needed to book, such as “Complete Intro CPR” or “Upload lifeguard card.” It offers one‑tap actions: upload proof, add the prerequisite to the cart, or request review. Pathway Tracker turns prerequisites into a clear progress bar. It suggests the next available prerequisite sessions that fit the learner’s schedule, supports “Add All” to book the required steps at once, and applies bundle pricing when rules allow. This converts ineligible interest into committed enrollments while optimizing capacity. “Certs expire and rules change; what can’t change is a learner’s need for clarity,” said an education administrator at a regional training center. “ClassTap shows exactly what’s missing and lets us make exceptions we can defend during audits.” Compliance Ledger provides a tamper‑evident audit trail of rule evaluations, documents, approvals, and check‑ins with timestamps and actors. Exports to CSV/PDF in accreditor‑friendly formats reduce the pain of audit season. Alerts fire for upcoming expirations that could affect bookings, giving admins time to intervene before sessions are impacted. To keep last‑minute fills fair and compliant, Gated Waitlist only promotes candidates who meet the rules. Ineligible users can hold a provisional spot with a deadline and guided steps to qualify. When a seat opens, pay‑to‑claim links go only to those who have completed requirements or who met them in time. AutoClaim and FairQueue may be layered on to further boost fill rates with transparent, policy‑aligned prioritization. Availability and integration Compliance and Pathways is available today on ClassTap Pro. Rule Canvas, Fit Check, and Compliance Ledger are included. Cert Snap, Pathway Tracker, Timed Override, and Gated Waitlist are add‑ons that can be enabled per program. ClassTap supports SSO for staff and SCIM for role provisioning, and offers API endpoints for exporting evaluations and ledger entries to third‑party systems. Learner experience and accessibility Learners see clear guidance on mobile and desktop, with large tap targets and accessible contrast. All consent screens and document capture flows support keyboard navigation and screen readers. Renewal reminders respect quiet hours and local time zones. Security and privacy Documents are encrypted in transit and at rest. Access is limited by role, with audit trails for every view and change. Providers can configure retention policies aligned to accreditor or institutional requirements. About ClassTap ClassTap is a lightweight white‑label class‑booking platform for independent instructors, community centers, and professional training programs running in‑person or hybrid classes. ClassTap handles scheduling, secure payments, waitlists, and automated reminders while preventing double‑bookings. The Compliance and Pathways release extends ClassTap’s mission of making participation simple by bringing clarity and consistency to accredited learning. Media contact Media: press@classtap.com Phone: +1 (415) 555‑0137 Website: https://www.classtap.com/press Forward‑looking statements This press release contains forward‑looking statements regarding product features and availability. Actual results may differ based on regulatory approvals, integrations, or operational factors.

P

ClassTap debuts Waitlist Intelligence to keep classes full with AutoClaim, FairQueue, and Last‑Call Burst

Imagined Press Article

San Francisco, CA — ClassTap today announced Waitlist Intelligence, a set of coordinated tools that turn demand signals into confirmed seats with minimal staff intervention. The release blends AutoClaim, FairQueue, HoldGuard, Last‑Call Burst, KeepTogether, and ChannelSmart with Reminder Paylinks to fill drop‑offs quickly and fairly—while showing customers where they stand and what to expect. Studios often rely on manual texts, messy spreadsheets, and first‑come links that frustrate regulars and confuse families. Waitlist Intelligence replaces guesswork with transparent rules and automated, payment‑secured holds so providers can run at capacity without door‑time drama. “Empty seats are expensive, and manual juggling is even more expensive,” said Maya Chen, CEO of ClassTap. “Waitlist Intelligence gives teams a fair system that customers understand. It turns waitlists from a support burden into a reliable revenue engine.” AutoClaim lets waitlisted attendees opt into automatic seat claiming within a chosen timeframe and price cap. When a seat opens, ClassTap instantly charges the saved payment method and confirms the spot—no taps required. FairQueue introduces a configurable, transparent ranking engine that blends join time with smart priorities like membership status, attendance streak, proximity, prerequisites, and group rules. Users see their position and ETA, which reduces support questions and builds trust. HoldGuard uses adaptive hold timers that flex with context: keep the standard window, shorten near class start, and auto‑extend when a user opens the pay sheet or passes verification. Visual countdowns set clear expectations, while expired holds cascade to the next in line without manual intervention. Last‑Call Burst engages the final minutes before class by sending offers to a small, smart batch—such as the top three—with first‑pay‑wins logic. Timers compress and cascades accelerate so empty seats are filled fast, with safeguards to prevent double‑booking. Group bookings get special treatment with KeepTogether, which respects “keep together” or “okay to split” preferences, holding and offering the right number of seats or presenting clear partial‑claim options when only some seats free up. ChannelSmart delivers claim invites via the channel each user is most likely to see—SMS, email, push, or WhatsApp—with automatic fallbacks and localization. It respects quiet hours and urgency windows, optimizing send timing and content to maximize conversions. “Previously, we’d text five people and hope someone replied,” said an operations lead at a boutique studio group. “Now the system handles holds and payments, and the roster updates itself. Our staff can focus on greetings instead of spreadsheets.” Waitlist Intelligence integrates with Lightning Checkout. Reminder Paylinks include secure, expiring TapPass links that open directly to one‑tap confirm, while Smart Seat Hold prevents double‑booking during peak demand. For hybrid classes, Device Lock and Silent Rotate keep join links secure as seats transfer, ensuring only confirmed attendees can enter. Configuration and policy alignment Providers can tune FairQueue priorities to balance fairness and business goals, weighting factors such as tenure, membership, attendance streak, or proximity. Rules for families and groups can ensure that siblings or friends are not routinely split. HoldGuard timers can be customized by class type and time to start, and can trigger specific outcomes—such as immediate cascade to “doorline” mobile waitlists. Doorline synergy For walk‑ins, Doorline Waitlist offers a lightweight poster QR that lets visitors join a queue with a quick profile and payment hold. As no‑shows are confirmed, Waitlist Intelligence targets doorline candidates with timed claim links, and claims are auto‑added on payment—filling last‑minute seats without front‑desk juggling. Availability and pricing Waitlist Intelligence is available today on ClassTap Core and Pro. FairQueue and HoldGuard are included. AutoClaim, KeepTogether, and Last‑Call Burst are Pro features. ChannelSmart is included with Messaging add‑ons. Doorline Waitlist is available as part of the Check‑In add‑on. Customer impact - Higher fill rates with fewer staff touches - Clearer expectations for customers, with positions and ETAs - Faster last‑minute fills without double‑booking risk - Better group experience for families and friends About ClassTap ClassTap is a lightweight white‑label class‑booking platform for independent instructors, community centers, and small studios. It handles scheduling, secure payments, waitlists, and automated SMS/email reminders, preventing double‑bookings and helping providers launch branded booking pages in under two minutes. Waitlist Intelligence advances ClassTap’s mission by converting demand into confirmed seats, fairly and automatically. Media contact Media: press@classtap.com Phone: +1 (415) 555‑0137 Website: https://www.classtap.com/press Forward‑looking statements This press release may contain forward‑looking statements. Feature availability and timing may vary by market and integration.

P

ClassTap rolls out Demand Pulse AI scheduling to publish classes that fill faster with SlotSense and EventAware

Imagined Press Article

San Francisco, CA — ClassTap today released Demand Pulse, a predictive scheduling system that helps instructors and studios publish calendars that fill faster with fewer experiments. Demand Pulse combines a live, color‑coded heatmap of conversion likelihood by day and hour with SlotSense suggestions, EventAware local signal enrichment, MicroShift timing optimizations, Cadence Fit for recurring series, Instructor Match, and GapFill for monetizing idle room gaps. Scheduling is often a costly guessing game. Classes launched at the wrong hour struggle for traction, while good hours go underutilized because teams can’t see demand patterns clearly. Demand Pulse makes the invisible visible so schedulers can make confident decisions grounded in data—not hunches. “Great programming is wasted on the wrong calendar,” said Maya Chen, CEO of ClassTap. “Demand Pulse surfaces when and where classes are most likely to convert, and SlotSense takes the next step by suggesting exact times that fit your instructors and rooms. It’s like a co‑pilot for your schedule.” The Demand Pulse heatmap predicts conversion likelihood by day and hour and can be filtered by location, room, class type, and audience. Confidence bands reflect data volume and quality to avoid false precision. EventAware enriches forecasts with local signals—school breaks, holidays, major events, even weather alerts—flagging risky hours before publication, proposing safer alternatives, and adjusting confidence accordingly. SlotSense suggests top time slots ranked by predicted utilization. It weighs instructor availability, room conflicts, seasonality, past waitlists, and commute patterns, then applies your choice in one tap—publishing a schedule that fills faster with less back‑and‑forth. MicroShift recommends small 5–20 minute adjustments to align with school drop‑offs, transit arrivals, and neighboring class end times. Preview panes show the expected lift in on‑time arrivals and reduced no‑shows before you commit. Cadence Fit models attendance decay and cohort behavior for recurring series. It proposes the best weekly rhythm—such as Tue/Thu 6:10 pm for six weeks—to maximize completion and minimize churn between cycles. Instructor Match reveals when each teacher overperforms the average by audience segment and hour, pairing the right instructor with the right slot to lift conversion without adding more classes. “Instead of copying last season’s calendar and hoping for the best, we’re now starting with data,” said a regional operations lead at a multi‑location studio. “Our piloted slots hit waitlist in week one, while weaker hours were flagged before publishing. It’s saved time and money.” GapFill identifies idle 30–90 minute room gaps and auto‑generates pop‑up sessions that fit emerging demand pockets. It suggests class formats, titles, and pricing with an expected revenue delta versus leaving the slot empty, so teams can monetize downtime in two clicks. Privacy and control Demand Pulse uses aggregated, de‑identified performance signals from your own account to make predictions. Providers can opt out of specific external signals in EventAware and can set guardrails to prevent publications outside operating hours or staffing policies. All recommendations are transparent and adjustable before publishing. Getting started Demand Pulse is available today on ClassTap Pro. Heatmaps and SlotSense are included. EventAware, MicroShift, Cadence Fit, Instructor Match, and GapFill are Pro add‑ons. Existing customers can find the new features under Schedule > Optimize. New customers can launch ClassTap in under two minutes, import existing calendars, and start optimizing immediately. Who benefits - Boutique Studio Managers seeking higher utilization with fewer schedule changes - Indie Instructors who want quick guidance on when to host classes for the best turnout - Franchise Ops leaders standardizing scheduling quality across locations without heavy processes - Community Program Coordinators aligning with school and city calendars for consistent attendance Roadmap Upcoming iterations will deepen audience segmentation and incorporate feedback loops from waitlist conversions, last‑minute fills, and late arrivals to further refine predictions. Providers will be able to set business goals—such as maximizing beginner capacity or improving morning utilization—and receive tailored schedule suggestions. About ClassTap ClassTap is a lightweight white‑label class‑booking platform for independent instructors, community centers, and small studios running in‑person or hybrid classes. It handles scheduling, secure payments, waitlists, and automated reminders while preventing double‑bookings. Demand Pulse extends ClassTap’s mission by helping providers publish the right classes at the right times, informed by real‑world demand. Media contact Media: press@classtap.com Phone: +1 (415) 555‑0137 Website: https://www.classtap.com/press Forward‑looking statements This press release may contain forward‑looking statements. Features and timelines are subject to change based on data availability, integrations, and market conditions.

Want More Amazing Product Ideas?

Subscribe to receive a fresh, AI-generated product idea in your inbox every day. It's completely free, and you might just discover your next big thing!

Product team collaborating

Transform ideas into products

Full.CX effortlessly brings product visions to life.

This product was entirely generated using our AI and advanced algorithms. When you upgrade, you'll gain access to detailed product requirements, user personas, and feature specifications just like what you see below.