Class booking

ClassNest

Fill Every Seat, Effortlessly

ClassNest is a lightweight SaaS that provides mobile-first instant booking pages, secure payments, automated SMS/email reminders, and a live smart waitlist that auto-offers openings, helping independent fitness, arts, and wellness instructors and small studios cut admin 60%, reduce no-shows 30%, and boost paid bookings 25% within three months.

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

ClassNest

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 local instructors to effortlessly fill every seat, eliminate admin friction, and cultivate thriving community-driven studios.
Long Term Goal
Within 3 years, enable 10,000 small studios to run 50,000 monthly classes while cutting instructor admin time by 40% and increasing paid bookings 25%
Impact
ClassNest reduces admin time by 60% and no-shows by 30%, increasing paid bookings 25% within three months for independent fitness, arts, and wellness instructors and small studios, turning fragmented scheduling into consistent full classes and recovered revenue.

Problem & Solution

Problem Statement
Independent fitness, arts, and wellness instructors and small studios lose revenue and time to fragmented booking, manual payments, and unpredictable no-shows, while enterprise-focused platforms charge high monthly fees and require complex setup.
Solution Overview
ClassNest centralizes bookings and payments with mobile-first instant booking pages and secure payments, then uses automated SMS/email reminders plus a live smart waitlist that auto-offers open spots to eliminate clipboard sign-ups, payment chasing, and empty seats.

Details & Audience

Description
ClassNest is a lightweight SaaS that provides instant booking pages, secure payments, automated reminders, and waitlist management for local classes and workshops. It serves independent fitness, arts, and wellness instructors, community centers, and small studios running in-person or hybrid classes. It cuts admin by automating payments, confirmations, and reminders to reduce no-shows and increase paid bookings. Its live smart waitlist auto-offers open spots via SMS and email.
Target Audience
Independent fitness, arts, and wellness instructors aged 25–55 needing effortless bookings, automated admin, mobile-first
Inspiration
At a neighborhood pottery class I watched the instructor juggle a clipboard, cash envelopes, and a frazzled handwritten waitlist as six students left without paying and two seats stayed empty. She stayed late reconciling notes, apologizing to latecomers. That afternoon made clear the need for a deceptively simple, mobile-first system that sells seats, collects payments, and auto-fills openings without training.

User Personas

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

W

Workplace Wellness Wren

- People Ops/HR coordinator at 250–700 employee company. - Based in major metro; hybrid office schedule. - Bachelor’s in HR/communications; SHRM-certified. - Compensation manager-level salary; vendor budget responsibility.

Background

Started as office manager, inheriting wellness programming after a chaotic vendor switch. Missed attendance targets twice, then built a pilot with smaller classes and tighter reminders, seeking tools that scale reliably.

Needs & Pain Points

Needs

1. Company-branded booking links per department session. 2. Automated SMS/email reminders boosting turnout. 3. Smart waitlist auto-fills last-minute gaps.

Pain Points

1. Chasing RSVPs across email, spreadsheets, and DMs. 2. No-shows torpedoing reported engagement metrics. 3. Manual backfill when cancellations hit same-day.

Psychographics

- Chases visible, measurable employee engagement wins. - Prioritizes low-effort tools staff actually use. - Risk-averse about data, vendor reliability. - Loves tidy processes and predictable calendars.

Channels

1. LinkedIn HR groups 2. Email vendor outreach 3. Google Search corporate wellness 4. Slack HR channels 5. YouTube tool demos

C

Community Coordinator Cam

- Program coordinator at city-run community center. - Serves mixed ages; sliding-scale pricing sensitivity. - Works weekdays at front desk; evening events. - Modest tech budget, procurement requirements.

Background

Former front-desk lead who modernized paper signups after repeated lobby lines and missed calls. Needs tools legible to seniors and teens, with fewer refunds from confusion.

Needs & Pain Points

Needs

1. Public pages readable on shared family phones. 2. SMS-first reminders with clear location details. 3. Auto-waitlist offers replacing refund calls.

Pain Points

1. Phone lines jammed with “is this full?” questions. 2. No-shows wasting subsidized room time. 3. Manual roster shuffles during room changes.

Psychographics

- Mission-first, equity-minded public service champion. - Pragmatic about tech; hates vendor lock-in. - Values accessibility and clear, large text. - Prefers phone over complex dashboards.

Channels

1. Facebook Groups community admins 2. Email city listserv 3. Google Search registration software 4. YouTube how-to tutorials 5. Nextdoor local posts

H

Hybrid Host Harper

- Independent fitness/arts instructor; 60% online, 40% in-person. - Based in apartment studio; rents pop-up rooms weekends. - Uses Zoom and YouTube Live; mid-tier gear. - Income diversified across memberships and drop-ins.

Background

Pivoted online during lockdowns, kept hybrid demand afterwards. Learned the cost of manually sending links minutes before class and wants automation that scales with audience growth.

Needs & Pain Points

Needs

1. Auto-deliver unique livestream links per booking. 2. Timezone-aware listings for global followers. 3. Capacity syncing for room and virtual seats.

Pain Points

1. Late DMs asking “where’s the link?”. 2. Duplicate bookings across platforms. 3. Confused start times across time zones.

Psychographics

- Audience-obsessed; optimizes for show-up rates. - Comfortable with tech, seeks clean integrations. - Data-curious; checks conversion and retention. - Values flexibility over rigid schedules.

Channels

1. Instagram Stories 2. YouTube channel 3. TikTok lives 4. Discord community 5. Email newsletter

R

Retreat Roster Rina

- Wellness/Yoga instructor turned retreat host. - Runs 4–6 retreats yearly; 12–20 guests each. - Travels internationally; mixed currencies familiarity. - Premium price points; expects pro branding.

Background

Scaled from local workshops to destination weekends after sellouts. A single late cancellation once cost thousands, prompting a shift to automated reminders and waitlist-driven backfill.

Needs & Pain Points

Needs

1. Polished landing pages with clear inclusions/exclusions. 2. Payment collection and deadline reminder sequences. 3. Instant waitlist offers when a spot opens.

Pain Points

1. Last-minute cancellations create costly empty rooms. 2. DMs overwhelm during launch windows. 3. Confusion over what’s included and logistics.

Psychographics

- Obsessive about experience and details. - Risk-averse about unsold inventory. - Values premium aesthetics and trust. - Loves concise, real-time confirmations.

Channels

1. Instagram Reels 2. Email VIP list 3. WhatsApp interest groups 4. Google Search destinations 5. Eventbrite browsing

A

After-School Scheduler Sloane

- PTA/booster program lead or after-school director. - Coordinates 5–12 weekly activities across campuses. - Communicates with guardians via text-first channels. - Shoestring budget; favors simple, reliable tools.

Background

Volunteered to fix chaotic signups after missed pickups and cash envelopes. Built structured rosters but still drowns in texts; wants automation that parents actually read.

Needs & Pain Points

Needs

1. One-tap mobile booking with saved child profiles. 2. Guardian-specific SMS reminders and pickup notes. 3. Auto-waitlist prioritizing siblings and proximity.

Pain Points

1. Parents forget class times and pickup changes. 2. Handling cash/checks and chasing late payments. 3. Mid-session roster changes cause confusion.

Psychographics

- Safety and reliability over everything. - Time-starved multitasker; avoids complex interfaces. - Prefers clear, friendly tone and visuals. - Community-minded connector; transparency-focused and fair.

Channels

1. Facebook Groups parents 2. Email school list 3. WhatsApp class chats 4. Google Calendar shares 5. Instagram updates

R

Recovery Group Rowan

- Physiotherapy/clinic coordinator at two suburban locations. - 4–8 person groups; older adult demographics. - Works weekdays; limited evening sessions. - Prefers low-friction tools for staff and patients.

Background

Shifted from 1:1 appointments to groups to increase access. Early cohorts suffered no-shows and phone tag; now seeks automation to keep clinicians’ calendars productive.

Needs & Pain Points

Needs

1. Staggered reminders tailored to older adults’ routines. 2. Quick one-tap reschedule options via SMS. 3. Attendance tracking for follow-up calls.

Pain Points

1. No-shows waste therapist time and rooms. 2. Endless phone tag for rescheduling. 3. Wrong clinic location confusion.

Psychographics

- Outcome-driven clinician; fiercely protective of schedules. - Compliance-minded, privacy-conscious, documentation-friendly professional always. - Calm, methodical communicator focused on clarity. - Values clear, large, readable interfaces.

Channels

1. Email professional newsletters 2. Google Search clinic operations 3. Facebook local communities 4. Reddit physiotherapy 5. YouTube patient education

Product Features

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

Smart Keyword Router

Let clients text simple words or phrases (e.g., “yoga”, “tonight”, “kids”). We automatically match them to the right class using schedule, location, and time context, handling typos and synonyms. Studios can set per-class or auto-routing keywords. Result: fewer back-and-forths, higher first-try bookings, and effortless scaling across multiple instructors.

Requirements

Intent Matching & Fuzzy Keyword Engine
"As a client who prefers texting, I want to text simple words like “yoga tonight” and get the right class immediately so that I can book quickly without browsing a website."
Description

Build an NLP-driven pipeline that interprets incoming SMS phrases (e.g., “yoga”, “tonight”, “kids”, misspellings, and synonyms) and maps them to the best-fit class using ClassNest schedule, class metadata, and historical matches. The engine must combine fuzzy string matching, a synonym dictionary, and lightweight intent detection for activities, time expressions, and audience (e.g., “beginner”, “prenatal”). It returns a ranked list of candidate classes with confidence scores and reasons (e.g., keyword hit, time fit, proximity), enabling transparent routing and downstream decisions. It must support studio-specific vocabularies, multi-instructor catalogs, and be performant for real-time SMS responses (<1.5s end-to-end).

Acceptance Criteria
Fuzzy Matching & Synonym Resolution
Given a studio catalog with ≥10 distinct activity names and a global+studio synonym map with ≥3 synonyms per activity When a user texts a single activity word or short phrase containing up to 2 character edits (insertions/deletions/substitutions) or a known synonym Then the engine normalizes and identifies the intended activity and the top-1 candidate’s activity matches ground truth in ≥92% of a 200-query misspelling/synonym test set And the Top-3 contains the correct activity in ≥98% of the same set And the top candidate includes a reason containing "keyword" or "fuzzy" (with the matched token) and has score ≥0.75 And when the edit distance >2 and no synonym exists, the engine does not map to an unrelated activity (precision ≥98%)
Time Expression Parsing & Windowing
Given the studio timezone is configured and the schedule contains classes over the next 14 days When a user texts time phrases such as "tonight", "tomorrow 6", "this weekend", "next Tue 7am", or relative phrases like "in 2 hours" Then the engine resolves each phrase to concrete local time windows: tonight = today 17:00–23:59; this weekend = upcoming Sat 00:00–Sun 23:59; tomorrow 6 = [06:00–06:59] and [18:00–18:59] unless am/pm is specified; next Tue 7am = the next calendar Tuesday 07:00–07:59; in 2 hours = [now+2h ± 30m] And candidates include only classes whose start_time falls within the resolved windows and include reason "time fit" with the resolved window And for a labeled set of 150 time-phrase queries, correct windowing (as per spec above) is achieved in ≥95% of cases
Audience and Level Intent Detection
Given class metadata contains audience/level tags (e.g., beginner, advanced, kids, prenatal, seniors) and a synonyms map (e.g., newbie→beginner, children→kids, moms-to-be→prenatal, 55+→seniors) When a user query includes an audience or level expression or its synonym Then the engine detects the audience/level intent and filters candidates to classes whose metadata tags intersect the detected audience/level And if no such classes exist and audienceRelaxation=false (default), the engine returns no candidates and sets reason "no availability" And if audienceRelaxation=true, the engine may include nearest alternatives, marks reason "audience relaxed", and reduces their scores by ≥0.20 And audience/level detection achieves ≥95% precision and ≥90% recall on a 100-query labeled set
Location Handling & Multi-Instructor Catalog Scope
Given a studio with multiple instructors and locations (with lat/long and addresses) and a catalog limited to that studio When a user query includes a location hint (ZIP/postcode, city name, or "near me") or none Then the engine restricts candidates to classes belonging only to that studio’s catalog And when a location hint is provided, the engine resolves a reference point (user last-known location for "near me" if available; otherwise the specified ZIP/city; if none, the studio primary location) and computes distance to each class location And candidates include reason "proximity" with a numeric distance (mi or km) and nearer classes receive a score boost such that, all else equal, nearer ranks higher And for 100 location-labeled queries, the top-1 candidate is within 10 km of the reference point in ≥95% of cases
Studio Vocabulary Overrides & Per-Class Keywords
Given global synonyms, studio-level synonyms/aliases, and per-class keywords (with optional attributes: exclusive:boolean, weight:0–1) When a conflict exists between vocabularies Then precedence is per-class keyword > studio synonym > global synonym And when a per-class keyword is marked exclusive and the class has an upcoming session in the resolved window, only that class may appear in the top-3 for queries matching that keyword And changes to studio or class vocabularies become effective in the matching engine within ≤60 seconds of save across 3 consecutive attempts And each vocabulary change is auditable with an entry containing actor, timestamp, and before/after values
Ranked Output, Confidence, Explainability, and Fallback
Given any parsed query and candidate set When the engine returns results Then it returns up to 5 candidates ordered by score descending; each item includes class_id, score in [0,1] (≤3 decimals), and reasons[] drawn from {"keyword","fuzzy","synonym","time fit","audience","proximity","historical boost","studio override","per-class keyword","disambiguation","no availability"} And ties where |Δscore|<0.01 are broken by earliest start_time, then nearest proximity And if top-1 score<0.60 or top-2 scores differ by ≤0.05 and map to different activities, the engine returns a disambiguation payload with at least two distinct suggestion prompts (no booking action) And if no classes match constraints, the engine returns an empty candidate list and a flag waitlistSuggested=true with reason "no availability" And for 50 labeled queries, the top-1 candidate’s reasons include at least two contributing factors in ≥90% of cases
Performance Under Real-Time SMS Load
Given a dataset of up to 5,000 upcoming class instances, 200 activities, and 5,000 synonym/vocabulary entries When processing SMS queries at 100 requests/second sustained for 15 minutes Then end-to-end latency (ingress to response) is ≤1.5s at p95 and ≤0.40s at p50; error rate <0.5%; and timeouts = 0 And cold-start latency p95 after 15 minutes of idle time is ≤1.2s And CPU utilization ≤70% and memory usage stays within configured limits without eviction or OOM across the test window
Studio Keyword Management & Routing Rules
"As a studio owner, I want to define and test the keywords that map to each class so that my clients get routed correctly the first time."
Description

Provide an admin interface for studios to configure per-class and global keywords, synonyms, and routing rules (e.g., keyword precedence, exclude terms, auto-route on single high-confidence match). Support CRUD for keywords, rule simulation/test against sample messages, and conflict resolution when multiple classes share terms. Include import/export of keyword sets, default suggestions based on class titles/tags, and safe-guards (e.g., max global keywords, validation). Changes must version and audit, and rules should propagate instantly to the routing engine via a cache-safe configuration service.

Acceptance Criteria
CRUD: Per-Class and Global Keywords with Validation & Safeguards
Given I am a studio admin on the Keyword Management screen When I create a per-class keyword "yoga" with synonyms ["vinyasa","flow"] and a global keyword "tonight" and an exclude term "free" Then both keywords and the exclude term are saved and immediately visible in their respective scopes without duplicates (case/whitespace-insensitive) And validation enforces 1–50 characters, letters/numbers/spaces/hyphens only, no emojis, and blocks reserved terms configured by the platform And attempting to exceed the max global keywords limit (default 200) shows a blocking error and prevents save And editing updates the record preserving references; deletion removes it and the item no longer appears in searches or simulator And all create/update/delete actions are attributed to the actor and prepared for audit/versioning
Routing Rules: Precedence, Exclude Terms, and Conflict Resolution
Given a global keyword "yoga" and per-class keyword "yoga" exist for Class A and Class B with precedence set to Per-class over Global When I change precedence to Global over Per-class and save Then the rule order updates and the simulator reflects the new precedence in match explanations When I add an exclude term "kids" to Class A and simulate with message "kids yoga" Then Class A is excluded from matches and the explanation cites the exclude rule When multiple classes share the same effective terms and priorities are equal Then the system flags a conflict, prevents enabling auto-route for those terms, and requires setting explicit class priority or disambiguation before allowing auto-route
Auto-Route on Single High-Confidence Match Configuration
Given Auto-route on single match is enabled and the confidence threshold is configurable between 0.50 and 0.95 (default 0.80) When a simulated message yields exactly one class at or above the threshold and all others below Then the simulator marks the result as Auto-route eligible and shows the score and threshold used When two or more classes are at/above the threshold Then Auto-route is not eligible and the simulator explains the tie condition When Auto-route is disabled Then no simulation result is marked Auto-route eligible regardless of scores
Rule Simulator: Test Messages Against Current Rules
Given I enter a sample message and click Simulate Then results return within 2 seconds with ranked matches, confidence scores, and a human-readable rationale including which keywords, synonyms, or excludes triggered And spelling tolerance and synonym expansions are shown in the rationale when applicable And running the simulator does not persist any data changes When no rules match Then the simulator displays "No match" with guidance to add keywords or synonyms
Import/Export of Keyword Sets with Preview and Validation
Given I choose Import and upload a CSV or JSON containing global and per-class keywords, synonyms, excludes, and priorities Then the system parses and shows a preview with counts of creates/updates/skips and flags invalid rows with reasons When I confirm import with no invalid rows Then the batch applies atomically, deduplicates case-insensitive entries, and returns a summary of created/updated/skipped within the imposed limits (e.g., max global keywords) When I export the current configuration Then I receive a UTF-8 CSV and JSON containing all keywords, synonyms, excludes, scopes, class references, priorities, version, and timestamps
Keyword Suggestions from Class Titles and Tags
Given a class titled "Morning Yoga Flow" with tags ["yoga","vinyasa","beginner"] When I open Keyword Suggestions Then the system proposes keywords like "yoga","morning","flow","vinyasa" and platform-known synonyms, excluding stop words and existing keywords And I can Accept All or select individual suggestions, which are added without duplicates and scoped to the class And rejected suggestions are not re-suggested in the same session
Versioning, Audit Trail, and Instant Propagation to Routing Engine
Given I save any change to keywords or rules Then a new immutable version is created with version ID, timestamp, actor, change summary, and diff, and the prior version remains accessible And I can view a paginated audit log filtered by actor/date/scope and export it as CSV When I click Revert on a prior version Then a new version is created that restores that configuration; no existing version is deleted And the configuration publishes to the routing engine via a cache-safe service and is observable in the simulator within 3 seconds of save or revert; publish status shows Success/Failure When publish fails Then no partial configuration is applied, an error banner appears with retry, and the current active version remains unchanged
Time & Location Context Resolver
"As a client on the go, I want the system to understand when I say “tonight near downtown” so that I’m only shown nearby options that start at a convenient time."
Description

Implement a context layer that interprets temporal and geographic intent in messages (e.g., “tonight”, “after work”, “6pm”, “near me”, city/ZIP) and resolves them against the studio’s timezone(s), booking windows, and locations. It must account for user profile data when available (home location, last booked site), device area code time inference, and holiday/blackout schedules. The resolver filters out past or full classes unless waitlist is enabled, respects lead-time buffers, and returns a context-adjusted candidate set to the matching engine.

Acceptance Criteria
Interpret 'Tonight' Across Timezones
Given a studio with a single timezone America/New_York and current ET date 2025-10-10 When a user with area code 415 texts "tonight" at 15:00 PT on 2025-10-10 Then the resolver interprets the time window as 2025-10-10 17:00–23:59 in America/New_York and returns only classes starting within that window Given a studio with two locations in America/Los_Angeles and America/New_York When a user with area code 415 texts "tonight" at 16:30 PT Then the resolver selects America/Los_Angeles as the user-inferred timezone, scopes the window to 17:00–23:59 PT of the same day, and prioritizes/returns only classes in PT unless the message explicitly names an ET city/ZIP Given the DST fall-back transition day in America/Los_Angeles When a user texts "tonight" after 17:00 local time Then the window honors the DST-adjusted clock times and does not include past instances duplicated by the transition
Absolute Time With Lead-Time Buffer and Booking Window
Given studio lead_time_buffer_minutes = 60 and booking_window_days = 14 in America/New_York When a user texts "6pm today" Then classes starting before now+60 minutes are excluded and classes after 14 days from now are excluded; only classes starting between 18:00–18:59 ET today within the booking window are returned Given studio lead_time_buffer_minutes = 0 and booking_window_days = 7 When a user texts "6:15 pm Fri" Then only classes starting between 18:15–18:29 in the Friday date within 7 days and in the studio timezone are returned Given multiple matches at 18:00 and 18:30 When the user texts "6pm" Then both classes are returned ordered by start time ascending (closest first)
'Near Me' Location Resolution and Fallbacks
Given a user profile with home ZIP 94110 and studio proximity_radius_miles = 25 When the user texts "near me tonight" Then the resolver sets the origin to ZIP 94110, limits candidates to locations within 25 miles, applies the "tonight" window in the applicable timezone, and returns only matching classes at nearby locations Given no home location but a last booked site LAX01 When the user texts "near me" Then the resolver uses LAX01 coordinates as origin and limits candidates to the configured proximity radius Given the message includes a city/ZIP (e.g., "Brooklyn 11215") When the user texts "11215 tomorrow" Then the resolver overrides profile origins with the provided ZIP and returns classes within the radius of 11215 for the "tomorrow" window
Holiday and Blackout Exclusion
Given a studio-wide holiday blackout on 2025-12-25 and two classes scheduled that day When a user texts "Dec 25 morning" Then the resolver returns zero classes and marks the blackout constraint applied in the output metadata Given a location-specific blackout for LOC_A on 2025-07-04 When the user texts "4th of July 6am near LOC_A" Then classes at LOC_A are excluded while classes at other locations remain eligible if they match time and other constraints
Full Classes and Waitlist Eligibility
Given a class at 18:00 with capacity 10 and 10 confirmed bookings and waitlist disabled When a user texts "6pm" Then the class is excluded from the candidate set Given a class at 19:00 with capacity 12, 12 confirmed bookings, and waitlist enabled/open When a user texts "7pm" Then the class is included in the candidate set with is_waitlist_only = true Given two classes, one full with waitlist enabled and one with 2 open spots When a user texts "tonight" Then both are returned with the open-spot class ranked higher by availability
Typo and Synonym Handling for Temporal/Geographic Intent
Given typo tolerance edit_distance <= 2 for key time/location tokens When a user texts "tnite" or "tonite" Then the resolver normalizes to "tonight" and applies the evening window definition Given synonyms for time intents {"after work" -> 17:00–20:59, "evening" -> 17:00–21:59, "noon" -> 12:00–12:59} When a user texts "after work near me" Then the resolver uses the 17:00–20:59 window in the inferred timezone and applies proximity filtering Given a misspelled ZIP/city within one edit distance ("Broooklyn") When a user texts "Broooklyn tomorrow" Then the resolver corrects to "Brooklyn" using the supported synonyms/geo index and applies the location filter
Context-Adjusted Candidate Set Output Contract
Given any valid input text When the resolver returns a candidate set Then the payload includes: normalized_time_window {start_iso, end_iso, timezone}, location_scope {origin_type, origin_value, radius_miles, location_ids_considered}, applied_constraints {lead_time_buffer_minutes, booking_window_days, holiday_blackouts_applied, capacity_filter, waitlist_policy}, and candidates [{class_id, start_iso, location_id, is_waitlist_only, relevance_score}] sorted by relevance_score desc then start_iso asc Given no classes match after constraints When the resolver returns Then candidates = [] and an ambiguity_or_empty_reason field is populated with a machine-readable code and human-readable message Given multiple timezones in the studio When the resolver returns Then timezone used for normalization is explicitly stated and consistent with the resolution rules (single studio TZ or user-inferred TZ for multi-TZ studios)
Disambiguation & Auto-Reply Booking Links
"As a client comparing options, I want a short text with the best matches and one-tap booking links so that I can confirm a spot without back-and-forth."
Description

Design a conversational SMS response flow that, when multiple good matches exist, replies with the top 1–3 options including class title, start time, location, and price, plus numbered quick-reply options and one-tap deep links to ClassNest instant booking pages pre-filled with class and user context. Persist conversation state to support follow-ups (e.g., user replies “2” or “later”), include rate limiting and message templating with studio branding, and track link clicks for conversion analytics. Ensure responses are concise and fit carrier SMS length limits with graceful multi-part handling.

Acceptance Criteria
Disambiguation Reply with Top 1–3 Options
Given an inbound SMS from a recognized phone number containing a keyword or phrase that maps to 2–5 viable class matches within the next 14 days And the studio has Smart Keyword Router enabled When the system generates an auto-reply Then the reply lists the top 1–3 options sorted by relevance score (desc) then soonest start time (asc) And each option includes: numbered index (1..N), class title (max 30 chars, truncate with … if longer), local start time with timezone abbreviation, short location label, and price (e.g., "$25" or "Free") And each option includes a unique booking deep link URL And the reply ends with an instruction: "Reply 1–N to book, 'more' for others, or 'help'" And if only 1 viable match exists, the system sends a single-option reply with the same fields and no "more" instruction And if 0 viable matches exist, the system sends a concise no-match message with one example keyword
Quick-Reply Number Selection Continues Conversation
Given a user previously received a multi-option reply with a correlation ID and option indices 1..N And conversation state is persisted for 60 minutes from the last bot message or until any listed class schedule changes When the user replies with a single digit between 1 and N within the valid window Then the system resolves it to the corresponding class, responds within 3 seconds with the deep link for that class and a concise confirmation, and records the selection in analytics When the user replies "more" within the valid window and there are additional matches Then the system sends the next 1–3 options, updates the correlation ID, and persists the new state When the user replies "later" Then the system offers to join the smart waitlist for the last selected or top-ranked class and provides a waitlist confirmation link When the user replies with an invalid number or the window has expired Then the system sends a single concise re-prompt (e.g., "Send a keyword like 'yoga 6pm'") and does not re-list options automatically
Deep Link Prefill and Seamless Booking
Given the system composes an option for a specific class and recipient phone number When generating the booking deep link Then the URL targets the ClassNest instant booking page with classId, studioId, and a unique tracking token parameters And, if the recipient has a ClassNest profile, the booking page pre-fills contact info (name if available, phone/email) and the selected class And the booking flow opens with the class preselected and requires no class re-selection And the link includes UTM parameters source=sms_router and medium=sms And the link remains valid for at least 7 days; after expiry it redirects to the studio's schedule list with a concise notice
SMS Length Limits and Multi-Part Handling
Given an auto-reply message is being composed When determining encoding Then the system detects GSM-7 vs UCS-2 based on content And if GSM-7 length <= 160 characters, send as a single SMS And if GSM-7 length > 160, split into concatenated parts of max 153 characters each, prefixing each with "(1/2)", "(2/2)" etc., without breaking URLs or words And if UCS-2 length <= 70 characters, send as a single SMS; if > 70, split into parts of max 67 characters with the same part indicators And if the composed message would exceed 2 parts, remove optional content (e.g., truncate titles, omit signature) before sending and append "Reply 'more' for details" And outbound messages never exceed 3 parts
Rate Limiting and Flood Control
Given incoming messages from a single recipient When multiple messages arrive within a short period Then the system sends at most 1 automated reply per recipient per 15 seconds and at most 5 automated replies per recipient per rolling hour And excess replies are suppressed with a single notification (e.g., "We received your messages—please wait…"), sent at most once per hour Given studio-wide traffic When automated replies would exceed 120 messages per minute per studio Then the system queues non-urgent replies and delivers them within 2 minutes in FIFO order and records a rate-limit event And manual agent responses (if present) are not throttled by the bot rate limit
Templated, Branded Messages with Fallbacks
Given a studio has configured an SMS reply template containing supported variables {{studio_name}}, {{options_block}}, {{cta_hint}}, and optional {{signature}} When the system renders a reply Then variables are resolved and unrecognized variables are removed And the final message contains the studio name at least once And if the rendered message would exceed the segment limit, the system first truncates {{signature}} then shortens class titles to fit And if no template is configured, a ClassNest default template is used that includes the studio name and CTA hint And outbound messages include the studio's registered sender ID where supported
Link Click Tracking and Conversion Attribution
Given a deep link is sent in an auto-reply When the recipient taps the link Then a click event is recorded within 300 ms with fields: recipient phone (hashed), studioId, classId, messageId, optionIndex, timestamp, and user-agent And the redirect adds no more than 200 ms additional latency before the booking page loads And multiple clicks from the same recipient on the same message within 30 seconds count as one unique click And if a booking for the class completes within 7 days of the click, the booking is attributed to the original message in analytics And analytics are queryable per studio per day with counts for messages sent, unique clicks, and attributed bookings
Confidence Thresholds & Human Handoff
"As a studio manager, I want unclear requests to either ask a smart follow-up or be sent to staff so that clients still get quick, accurate help."
Description

Introduce configurable confidence thresholds and fallback behaviors. On low confidence or no matches, prompt clarifying questions (e.g., ask for time or location) or route the conversation to a human operator (instructor/front desk) via the ClassNest inbox with alerts (email/SMS/push). Include SLA timers, assignment rules, and an override command set (e.g., HELP, AGENT). Log all interactions for audit and training, and allow studios to customize thresholds per channel or class category.

Acceptance Criteria
Configure Confidence Thresholds by Channel and Class Category
- Given I am an admin with edit permissions, When I set SMS threshold to 0.82 and Email threshold to 0.75 and save, Then those values persist and are applied to subsequent routing decisions per channel. - Given a Yoga category threshold is set to 0.90 and a global default is 0.80, When an SMS message targets a Yoga class, Then 0.90 is used for the decision. - Given no category override exists for Pilates but a channel override exists for SMS at 0.78, When an SMS message targets a Pilates class, Then 0.78 is used. - Given a threshold value outside [0.00, 1.00], When I attempt to save 1.20, Then the system blocks the change and shows a validation error message. - Given a category override exists, When I delete the override and save, Then the system reverts to using the channel (or global) threshold on the next decision within 30 seconds.
Low-Confidence Clarification Prompt
- Given a message’s top intent confidence is below the active threshold but ≥ the detection floor (e.g., 0.30), When processed, Then the user receives a single clarifying question asking for the missing attribute(s) (time, location, level) within 5 seconds. - Given the user replies to a clarification within 10 minutes, When confidence recomputation crosses the active threshold, Then the system returns the best-matching class options and booking link without human handoff. - Given up to 2 clarification rounds are attempted, When confidence remains below threshold after the 2nd prompt, Then the conversation is handed off to a human agent. - Given a message’s confidence is < detection floor, When processed, Then the system skips clarifications and triggers immediate human handoff. - Given the channel is SMS, When sending a clarification, Then the message fits within 160 characters (or is sent as a segmented SMS) and includes a clear reply instruction (e.g., “Reply with a time like 6pm”).
No-Match Human Handoff with Multi-Channel Alerts
- Given no class matches are found or the decision is to hand off, When the conversation is evaluated, Then a new Inbox thread is created with status “Awaiting Agent” and the full message context attached. - Given handoff is created, When alerts are dispatched, Then the assigned/on-call agent receives email, SMS, and push notifications (per their preferences) within 15 seconds containing a deep link to the thread. - Given an alert delivery attempt fails, When a retry policy is applied, Then the system retries up to 3 times with exponential backoff and logs outcomes. - Given a handoff occurs, When the user is notified, Then the user receives a confirmation that a human will assist and an estimated response time based on current SLA settings.
SLA Timers and Assignment Rules
- Given a conversation is in “Awaiting Agent”, When created during business hours, Then a First Response SLA timer (default 5 minutes, configurable) starts and is visible on the thread. - Given no agent has manually picked up the thread within 2 minutes, When auto-assignment runs, Then the thread is assigned round-robin to an on-duty agent with the Routing skill; if none available, it is assigned to the fallback group. - Given the SLA timer breaches, When the breach occurs, Then the thread priority is set to High and an escalation alert is sent to the team owner and fallback group. - Given an agent sends the first human reply, When the message is posted, Then the First Response SLA timer stops; if the thread is set to “Waiting on Customer”, the timer pauses until the user replies. - Given any assignment, SLA, or status change, When it occurs, Then the event is logged with timestamp, actor, and reason.
Override Commands HELP and AGENT
- Given a user sends HELP or AGENT (case-insensitive, common variants) on any channel, When the message is received, Then the system immediately bypasses automation and creates a human handoff. - Given an override command is processed, When acknowledging the user, Then the system sends a confirmation message indicating a human is on the way and includes expected response time. - Given override commands might repeat, When duplicate commands arrive within 60 seconds, Then the system rate-limits duplicate acknowledgements while keeping a single handoff open. - Given analytics aggregation, When override commands are received, Then they are excluded from intent-matching statistics and do not affect keyword routing metrics.
Comprehensive Interaction Logging for Audit and Training
- Given any inbound or outbound message, When processed, Then the system logs timestamp, channel, raw text, normalized text, detected intents, confidence scores, active threshold, decision path (clarify/handoff/book), prompt content, user replies, and correlation IDs. - Given operational events, When alerts, SLA transitions, assignments, and escalations occur, Then each event is logged with outcome, target(s), and latency. - Given compliance requirements, When logs are stored, Then sensitive tokens/PII are masked, access is role-restricted, and all access is audit-logged. - Given a search request, When filtering by time range, user identifier, or conversation ID up to 10k records, Then results return within 1 minute and can be exported to CSV.
Threshold Precedence and Testing Sandbox
- Given global, channel, and category thresholds exist, When a decision is made, Then category-specific threshold overrides channel-specific, which overrides global default; this precedence is documented and enforced. - Given an admin opens Testing Sandbox, When a sample message, channel, and (optional) category are provided, Then the system displays computed confidence, active threshold used, and decision (clarify/handoff/book) within 2 seconds. - Given thresholds are edited, When changes are saved, Then they take effect within 30 seconds and a new version is recorded; reverting restores the prior version and behavior.
Training & Analytics Dashboard
"As a studio owner, I want to see which keywords drive bookings and where clients get stuck so that I can improve routing and increase conversions."
Description

Provide a dashboard with metrics for keyword routing effectiveness: match rate, first-try booking conversion, time-to-first-response, top converting keywords, unmatched terms, and drop-offs. Surface recommended new keywords and synonyms based on unmatched messages and successful free-text patterns. Enable A/B testing of response templates and thresholds, export of CSV, and privacy-preserving samples for review. Feed approved suggestions back to the engine as training data and update studio rules with one click.

Acceptance Criteria
Core Routing Metrics Dashboard
Given a studio with Smart Keyword Router enabled and at least 30 days of routing and booking data When a user opens the Training & Analytics Dashboard and selects a date range, time zone, and filters (instructor, class, location, channel) Then the dashboard shows match rate, first-try booking conversion, time-to-first-response, top converting keywords, unmatched terms, and funnel drop-offs for the selection And each displayed metric matches the back-end aggregate within ±1% for totals ≥ 100 events or ±1 absolute for totals < 100 And metrics refresh within 15 minutes of new events being recorded And filter changes update all widgets within 2 seconds in 95th percentile And each metric has a tooltip with its definition and calculation And clicking a metric drills down to a table of contributing items consistent with the aggregate totals
Recommendations for New Keywords and Synonyms
Given unmatched terms and successful free-text patterns exist in the selected date range When the user opens the Recommendations tab Then the system lists suggested keywords/synonyms with: supporting term frequency, example redacted messages, estimated lift, and target class(es) And suggestions meet a minimum evidence threshold (e.g., ≥ 25 occurrences or ≥ 5 conversions if free-text) And the user can Approve, Edit, or Reject each suggestion; edits allow changing target class, locale, and synonym list And approved suggestions are marked Pending retrain and appear immediately in the studio rules as Draft additions And an audit log captures actor, action, timestamp, and before/after values And recommendations can be filtered by class, instructor, and language
A/B Testing of Templates and Thresholds
Given at least two response template variants or routing confidence thresholds are configured When the user starts an experiment selecting primary KPI (first-try booking conversion), secondary KPIs (match rate, time-to-first-response), allocation (e.g., 50/50), and audience Then traffic is split according to allocation with no user receiving multiple variants within the experiment window And the dashboard reports per-variant metrics, sample sizes, and a 95% confidence significance indicator once minimum sample size is met (default 200 events/variant) And the user can pause, stop, or declare a winner; stopping preserves results and ends traffic And guardrails alert if any variant degrades the primary KPI by ≥ 10% relative at 95% confidence And experiment results are exportable to CSV and preserved for at least 12 months
CSV Export of Analytics and Logs
Given a user with export permissions selects a date range and filters and clicks Export CSV When the export is generated Then a CSV is produced containing metric summaries and an optional detailed events file with columns: timestamp (ISO 8601), studio_tz, class_id, instructor_id, location_id, channel, message_hash, keyword_matched, confidence, routing_outcome, booked (Y/N), response_template_id, experiment_id (if any) And PII fields (phone, email, names) are excluded or irreversibly hashed; free-text samples are redacted And exports up to 1M rows complete within 60 seconds; larger exports generate an emailed link within 15 minutes And exported metrics match on-screen values for the same filters and range And all timestamps are normalized to studio time zone with offset included
Privacy-Preserving Message Samples
Given the user views unmatched terms or top converting keywords When they open Sample Messages Then the system shows up to 50 representative samples per term with phone numbers, emails, card digits, and names masked (e.g., +1******1234, jo***@***.com) And the user can request 50 more samples until a maximum of 500 per term per day And sampling is stratified by channel and time bucket to avoid bias And a Report Unredacted Data control exists; submitting flags the sample and notifies security And copy/download of raw samples is disabled unless the user has a Data Export role
One-Click Apply Suggestions to Engine and Studio Rules
Given an approved keyword/synonym suggestion exists When the user clicks Apply and confirms Then the studio rules are updated immediately with the new keyword/synonym mapping and versioned (e.g., v123→v124) And the training data queue receives the approved suggestion record within 5 minutes for the next model retrain And routing behavior reflects the new rule immediately for deterministic matches; ML-driven matches change only after retrain completes And the UI shows status badges: Applied to rules (instant) and Retrain scheduled/completed with timestamps And a rollback control restores the previous version within 1 click and logs the action
Compliance, Consent & Opt-Out Handling
"As a client, I want to be able to opt out or get help at any time so that I stay in control of my communication preferences."
Description

Ensure the router adheres to SMS/MMS compliance: manage opt-in/consent per contact, honor STOP/START/HELP and country-specific variants, throttle outside quiet hours where applicable, and display required brand identifiers. Store consent timestamps, maintain suppression lists, and redact PII in logs where necessary. Integrate with ClassNest’s existing messaging consent models and apply per-studio settings, ensuring routing does not respond to opted-out numbers and that audit artifacts are retrievable for regulatory requests.

Acceptance Criteria
Consent State Sync & Storage Integration
Given a contact exists in ClassNest with an existing messaging consent status When the Smart Keyword Router receives any inbound message from that contact Then the router reads the canonical consent status from the shared consent store and uses it to determine eligibility to respond Given a contact completes consent via ClassNest consent UI or replies START from their device When consent is recorded Then the system persists consent=true with fields: contact_id, studio_id, phone (E.164), timestamp (UTC), source (web/SMS), country, policy_version And the consent record is retrievable via Admin UI and API within 5 seconds And subsequent router responses are allowed only when consent=true
STOP/Opt-Out Handling with Global Keyword Variants
Given an inbound message body matches any opt-out keyword [STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, ARRET, BAJA, CANCELAR, PARAR] case-insensitive and ignoring surrounding whitespace/punctuation When received Then the system sets consent=false and suppression=true for that contact and studio with a UTC timestamp And sends a single opt-out confirmation SMS including the studio name and "ClassNest" and re-subscribe instructions (e.g., "Reply START to receive messages") And cancels any queued non-compliance outbound messages to that number And the Smart Keyword Router ignores and does not process routing keywords from that number until consent is restored Given an inbound message contains an opt-out keyword alongside other text When received Then the opt-out takes precedence and is processed Given an MMS contains an opt-out keyword in the text body When received Then the same opt-out behavior is applied
START/Resubscribe and HELP Auto-Responses
Given a contact is opted out (suppression=true) When the contact sends START or UNSTOP (case-insensitive) Then suppression is cleared, consent=true is recorded with UTC timestamp and source=SMS And a single subscription confirmation SMS is sent that includes the studio name and "ClassNest" Given any contact sends HELP When received Then send a HELP response including brand/studio identifiers, a studio-configured support link/email, "Reply STOP to opt-out", and "Msg&Data rates may apply" And no consent status is changed Given a contact is not opted in When they send HELP Then a HELP response is sent and no non-compliance content is included
Quiet Hours Throttling and Exceptions
Given studio quiet hours are enabled and it is outside the allowed send window in the studio's configured timezone When a routing keyword (non-compliance) is received Then the router does not send a routing response immediately and schedules it for the next allowed window And the event is logged with reason=quiet_hours Given quiet hours are in effect When a compliance keyword (STOP/START/HELP) is received Then the system processes and replies immediately Given the studio has enabled "quiet hours acknowledgment" When a routing keyword is received during quiet hours Then the system sends a single acknowledgment stating the reply will be sent during allowed hours and includes no promotional content
Brand Identifier and Disclosure in Outbound Messages
Given the router sends the first outbound message to a contact within a rolling 24-hour window When the message is delivered Then the body includes both the studio name and the "ClassNest" brand identifier Given the system sends HELP or STOP confirmation messages When delivered Then those messages include the studio name, "ClassNest", and opt-out/resubscribe instructions Given per-studio branding settings are configured When messages are sent Then the configured studio display name is used consistently across all router replies
PII Redaction in Logs and Observability
Given message processing events are written to logs When logs are viewed by users without the "Compliance Viewer" role Then phone numbers are masked (e.g., +1******45), email addresses are masked (e.g., j***@domain.com), and detected PII in message bodies is redacted Given system diagnostics or trace exports are generated When exported Then PII fields are redacted by default, with an option to include unredacted data only for authorized roles via the Audit Export feature Given logs are forwarded to external observability systems When sent Then no full phone numbers or email addresses are included in those payloads
Audit Artifact Retrieval for Regulatory Requests
Given an admin user requests an audit export for a studio and date range When the export is generated Then it includes for each contact: consent status history with timestamps and source, policy_version, opt-out events with keywords and timestamps, suppression list snapshot, and the text of compliance messages (STOP/START/HELP) with PII appropriately redacted Then the export is available for download within 60 seconds for up to 100,000 records, and larger exports stream in chunks without timeout And the export is retrievable via API with the same content and field names as the UI export

One‑Tap Hold Checkout

Reply instantly with a personalized, pre-filled checkout link that starts a short seat hold (e.g., 10 minutes). Apple Pay/Google Pay ready, with automatic fallback to waitlist if full. Benefit: prevents double-booking, reduces drop‑off, and creates friendly urgency that converts on mobile.

Requirements

Personalized Pre-Filled Checkout Link
"As an instructor, I want to send a one-tap, pre-filled checkout link to a specific student so that they can book quickly on mobile with minimal friction."
Description

A server-generated, single-use checkout URL that embeds the target class/session, price, taxes/fees, and the invitee’s known profile fields (name, email, phone) to minimize typing on mobile. The link is signed, time-bound, and scoped to a specific seat hold, enabling one-tap entry into an Apple Pay/Google Pay-ready checkout. Supports deep linking to the native app when installed and responsive web fallback, preserves UTM/source context for attribution, and localizes currency and copy. Includes token validation, expiration handling, and one-time redemption controls to prevent reuse or sharing. When inventory is unavailable at link open, the flow routes into Auto-Waitlist Fallback without losing user context.

Acceptance Criteria
Signed Single‑Use Checkout Link Generation
- Given an instructor initiates a One‑Tap Hold Checkout invite for a specific session and price, When the server generates the checkout URL, Then the URL contains a signed, tamper‑evident token scoped to the session, tenant, and intended invitee. - Given the URL is generated, When inspected server‑side, Then the payload includes session_id, hold_id, total amount (base + taxes/fees), and invitee profile fields (name, email, phone) required for prefill. - Given a new link is issued, When no one has opened it yet, Then the hold has not started; the seat hold begins only on first successful open. - Given configuration defaults, When a link is issued, Then the link validity window before first open is enforced (default 24h, configurable) and stored with the token.
One‑Tap Prefilled Wallet Checkout
- Given the invitee opens a valid link with available inventory, When the checkout loads, Then a seat hold is created and a visible countdown timer (default 10 minutes, configurable) starts. - Given invitee profile data exists, When the checkout renders, Then name, email, and phone are pre‑filled and editable, and proceeding with Apple Pay/Google Pay requires no typing. - Given the device and merchant are wallet‑eligible, When the checkout renders, Then Apple Pay/Google Pay buttons are shown; otherwise the card form is shown as fallback. - Given the invitee authorizes payment via wallet within the hold window, When payment succeeds, Then the booking is confirmed, the hold is marked booked, and a confirmation screen is displayed. - Given payment is declined or the user cancels, When within the hold window, Then the hold remains until expiry or explicit release and the user is prompted to retry or exit.
Deep Link to App with Web Fallback
- Given the native app is installed and supports universal/app links, When the invitee opens the checkout URL, Then the app opens directly to the checkout screen with full context (session, pricing, hold countdown, locale, UTM). - Given the native app is not installed or the deep link fails, When the invitee opens the URL, Then a responsive web checkout loads with identical context and functionality. - Given either app or web path, When the checkout is presented, Then Apple Pay/Google Pay capability is available where supported and configuration is consistent across paths. - Given performance SLOs, When loading the web checkout on a typical 4G connection, Then time to interactive for the checkout screen is ≤ 2s at p95.
Token Validation and Expiration Controls
- Given the token signature is invalid or the token is malformed, When the link is opened, Then access is denied server‑side and a user‑friendly error is shown with an option to request a new link. - Given the token validity window has elapsed without redemption, When the link is opened, Then an expiration message is shown and the user can request a fresh link and/or join the waitlist. - Given the token has been redeemed, When the link is opened again by any user, Then no new booking is allowed; the original booker sees booking details, others see a neutral message with waitlist option. - Given security requirements, When processing the link, Then single‑use and expiry checks are enforced server‑side and logged for audit.
Auto‑Waitlist Fallback on No Inventory
- Given no seats are available at link open, When the invitee accesses the URL, Then the flow routes into Auto‑Waitlist with session details and invitee profile pre‑filled, preserving UTM/source context. - Given the user is in the waitlist flow, When a seat becomes available, Then an in‑flow auto‑offer is presented to claim the seat without re‑entering details and without creating duplicate holds. - Given attribution requirements, When fallback or auto‑offer occurs, Then analytics and booking attribution retain the original UTM/source values.
Attribution and UTM Preservation
- Given UTM/source parameters are present on the checkout URL, When a booking is completed, Then the booking record persists utm_source, utm_medium, utm_campaign, utm_term, utm_content exactly as received. - Given the URL triggers a deep link into the native app, When the app renders checkout and confirms booking, Then the same UTM/source parameters are passed through and stored on the booking and emitted in analytics events. - Given no UTM parameters are present, When a booking is completed, Then the booking record stores source = "one_tap_hold_link" (or tenant‑configured default).
Localization and Currency Display
- Given tenant currency/locale settings and optional locale hints on the URL, When the checkout renders, Then all monetary amounts display in the tenant currency with correct symbol/ISO code and taxes/fees included or itemized per configuration. - Given localization is resolved, When the UI renders, Then copy and messages appear in the resolved locale using fallback order: URL param -> tenant setting -> device locale. - Given wallet payments, When Apple Pay/Google Pay sheets are invoked, Then the currency code and totals match the on‑screen amounts and localized labels are used. - Given right‑to‑left locales, When the checkout renders, Then layout remains usable and the countdown/time formats display correctly.
Short Seat Hold Engine
"As a student, I want my seat held briefly while I complete payment so that I don’t lose the spot during checkout."
Description

A real-time reservation subsystem that temporarily locks one or more seats for a defined window (default 10 minutes, merchant-configurable) upon link generation or open. Enforces exclusive access for the intended invitee, displays a visible countdown, and transparently extends the hold while payment authorization is in progress. Implements idempotent creation, deterministic release on expiry or cancellation, and immediate inventory restoration on failure. Handles concurrency to prevent double-booking across channels, emits hold lifecycle events (created, extended, converted, expired), and surfaces hold state to staff dashboards and APIs for support visibility.

Acceptance Criteria
Idempotent Hold Creation on Link Generation/Open
Given a booking invite link is generated for a class with available seats and invitee X When the invite link is generated or opened Then a seat hold is created with hold_id and expires_at = now + default hold window (10 minutes unless overridden) And the response includes hold_id, class_id, seat_count, hold_owner = invitee X, expires_at, and payment_url When a repeat request is made with the same idempotency key or invite link token within the active window Then the existing hold is returned unchanged (same hold_id, same expires_at) and no additional seats are reserved And the API returns HTTP 200 with a header indicating idempotent replay
Visible Countdown and Hold Extension During Payment Authorization
Given a user opens the pre-filled checkout with an active hold When the checkout renders Then a countdown shows remaining time until expires_at and updates at least once per second When the user initiates Apple Pay/Google Pay/card authorization before expiry Then the hold is extended while authorization is in progress, with new expires_at visible in the UI within 1 second And the extension does not allocate additional seats and does not exceed the configured max total hold time When authorization succeeds Then the hold converts to a booking and the countdown hides
Deterministic Hold Expiry and Immediate Inventory Restoration
Given an active hold exists When expires_at is reached without successful payment OR the user cancels checkout OR payment authorization fails Then the hold transitions to expired/failed within 1 second of the event And all seats reserved by the hold are immediately returned to inventory And the payment URL returns HTTP 410 (Gone) or shows "Hold expired" with an option to refresh availability And a new hold can be created only if inventory remains
Exclusive Access for Intended Invitee
Given a hold link is intended for invitee X When invitee X opens the link and is verified via signed token or code Then only invitee X can complete the booking using that link And attempts by non-owners result in HTTP 403 (Forbidden) with no seat allocation and no exposure of hold_id And staff can override exclusivity via dashboard with the override action and actor recorded in audit logs
Concurrency Control Across Channels Preventing Double-Booking
Given multiple checkouts or API requests attempt to hold or purchase seats for the same class concurrently across channels When two or more requests contend for the last available seats Then at most the available seat count is reserved; no seat is allocated to more than one active hold or booking And inventory updates are serialized using locking or equivalent with p95 contention wait time <= 150 ms And losing requests receive a clear "Sold out/No capacity" error and create no holds
Lifecycle Events Emission and Staff Visibility
Given a hold is created, extended, converted, expired, or failed When the hold state changes Then an event (hold.created, hold.extended, hold.converted, hold.expired, hold.failed) is emitted within 1 second to the event bus And event delivery is retried at least 3 times with exponential backoff on failure And the staff dashboard shows per-hold state, invitee, expires_at, and last event timestamp with <= 5 seconds lag And GET /holds/{id} returns consistent state with ETag/version for caching
Merchant-Configurable Hold Window with Safe Bounds
Given a merchant sets a default hold duration at the account or class level When the configuration is saved Then new holds use that duration clamped between 2 and 30 minutes (default 10 minutes) And per-hold extensions during payment do not exceed a maximum total hold time of 30 minutes And changes apply only to new holds; existing holds keep their original expires_at And the configured duration is visible in dashboard settings and via API
Apple Pay and Google Pay Express
"As a student, I want to pay with Apple Pay or Google Pay in one tap so that checkout is fast and secure on my phone."
Description

Native express payment support using Apple Pay JS, Google Pay API, and the Payment Request API to enable biometric one-tap checkout on mobile and modern browsers. Prefills line items from the held session, applies discounts, and calculates taxes/fees server-side, with 3DS/SCA handling where required. Falls back gracefully to saved cards or standard card entry when wallets are unavailable. On authorization, confirms payment, converts the hold to a booking atomically, and returns clear success/error states with receipts. Supports multi-currency, refunds/voids on failure, and reconciles payouts through the existing payment processor.

Acceptance Criteria
Apple Pay one-tap checkout on iOS Safari with active hold
Given a user on iOS Safari with Apple Pay provisioned and an active 10‑minute seat hold for a session When the user taps “Apple Pay” on the one‑tap checkout link within the hold window Then the Apple Pay sheet opens with prefilled merchant name, line item(s), currency, and a total sourced from the server And the total includes applied discount(s), tax, and fees computed server-side And upon successful biometric authorization, the payment is authorized and captured And the booking is created atomically with the payment confirmation (no possibility of double-booking) And the hold is cleared and inventory decremented exactly once And the API responds within 3 seconds with HTTP 200, a booking ID, payment ID, and receipt URL And if the user cancels the Apple Pay sheet, no charge is created, the hold remains until TTL, and no booking is created
Google Pay one-tap checkout on Android Chrome with active hold
Given a user on Android Chrome with Google Pay available and an active 10‑minute seat hold for a session When the user taps “Google Pay” on the one‑tap checkout link within the hold window Then the Google Pay sheet opens with merchant info, line item(s), currency, and a server-sourced total And the total includes applied discount(s), tax, and fees computed server-side And upon successful biometric/PIN authorization, the payment is authorized and captured And the booking is created atomically with the payment confirmation (idempotent if retried) And the hold is cleared and inventory decremented exactly once And the API responds within 3 seconds with HTTP 200, a booking ID, payment ID, and receipt URL And if the user cancels the Google Pay sheet, no charge is created, the hold remains until TTL, and no booking is created
Wallet unavailable triggers graceful fallback to saved card or standard card entry
Given a user device/browser that does not support Apple Pay/Google Pay or has no eligible cards When the user opens the one‑tap checkout link Then the system hides disabled wallet buttons and presents Payment Request API if available, else saved cards, else standard card form And the checkout form is prefilled with session details and user contact/shipping (if required) from the hold And SCA is invoked as required for the chosen method And a successful payment via fallback methods results in the same atomic booking conversion and receipt And if no payment method succeeds, no booking is created and the hold remains until TTL or is released on explicit cancel
SCA/3DS challenge completes and booking conversion is atomic with clear states and receipts
Given a transaction that requires 3DS/SCA per issuer/region When the user initiates payment via Apple Pay, Google Pay, Payment Request, saved card, or standard card Then a 3DS/SCA challenge is presented inline or in-sheet per the wallet/method UX And on successful challenge, the payment is confirmed and captured, and the booking is created in the same transaction And on failed/abandoned challenge, no booking is created, any temporary authorization is voided, and the hold is released immediately And the client receives explicit states: success, requires_action, canceled, or failed with machine-readable error codes And on success, an email/SMS receipt is sent within 1 minute including booking details, amount, currency, taxes/fees, and last4/token And all operations are idempotent using a provided idempotency key; retries never create duplicate bookings or charges
Capacity full or hold expired routes to smart waitlist and cancels payment flow
Given a user opens the one‑tap checkout after the 10‑minute hold has expired or the class is at capacity When the user attempts to initiate payment via any method Then the payment sheet does not open and the user is informed the seat is unavailable And the user is offered to join the smart waitlist with one tap And upon join, the user receives confirmation and is not charged And if an opening auto-offer occurs later, the new one‑tap link starts a fresh hold and resumes the express checkout flow
Server-side pricing integrity: discounts, taxes, fees, and line-item prefill
Given a held session with known price, optional discount code, and jurisdictional tax/fee rules When the checkout is initialized via any supported method Then line items, subtotal, discounts, taxes, fees, and grand total are calculated server-side and sent to the wallet/payment sheet And any client-side price manipulation attempts are ignored; the server is the source of truth And the amount authorized/captured equals the server-computed total to the cent in the session currency And the booking record stores the applied discount code, tax breakdown, fees, and final total And a mismatch between client-presented total and server total blocks payment initiation with a clear error
Failure recovery and reconciliation: refund/void and payout records in multi-currency
Given a payment that is authorized but downstream capture or booking creation fails for any reason When the failure is detected Then the authorization is voided or a full refund is issued automatically within 60 seconds And the booking is not created or is rolled back to pre-transaction state, and the hold is released And the transaction ledger records the failure, refund/void ID, and links to the original attempt And for supported currencies (USD, EUR, GBP, AUD, CAD and any enabled), the wallet shows and charges in the session currency; processor settlement and payout records reflect the same currency And all transactions are reconciled to the existing processor’s payouts with matching gross, fees, net, currency, and metadata And an admin report exposes success/failure counts, refund rates, and net revenue by currency for the period
Auto-Waitlist Fallback and Offer
"As a student, I want to be automatically added to the waitlist and notified with a quick-book link if a spot opens so that I still have a chance to attend."
Description

Automatic rerouting to the smart waitlist when the class is full or a hold cannot be honored, capturing the user’s intent with a single confirmation. Preserves the original session context, collects preferred times, and enrolls the user by rank. When a seat opens, sends an instant, personalized offer link that starts a new short hold, respecting notification preferences (SMS/email) and throttling. Integrates with existing waitlist logic for fairness, limits simultaneous offers, and records conversions for analytics.

Acceptance Criteria
Full Class: Auto-Reroute to Waitlist with One-Tap Confirmation
Given a class occurrence has zero seats remaining And the user opens a personalized one‑tap checkout link When the link loads Then the system displays a waitlist enrollment screen instead of the payment screen And the user can join the waitlist with one tap using pre-filled contact details And the original session context (class ID, occurrence time, price, promo/applied discounts, referral/UTM) is stored with the waitlist entry And no hold is created and no payment is attempted
Hold Lost Race: Seamless Waitlist Enrollment on Payment Attempt
Given a user opens a one‑tap checkout link for a class with at least one seat available And a short hold is initiated When another booking captures the last seat before the user completes payment Then the hold attempt is canceled without charging the user And the user is immediately offered one‑tap waitlist enrollment with all previously entered details preserved And any payment sheet (Apple Pay/Google Pay) is safely dismissed And upon confirmation, the waitlist entry persists the original session context and computes the user’s rank
Preference Capture and Ranked Enrollment
Given the waitlist enrollment screen is shown When the user selects preferred time windows or chooses “No preference” Then the selection is required to proceed (either at least one preference or “No preference”) And the preferences are saved on the waitlist entry And eligibility for future offers is filtered by these preferences (unless “No preference”) And the user is ranked using existing waitlist logic with a timestamped join event and deduplication per class occurrence
Instant Personalized Offer with New Short Hold
Given a seat becomes available for a class with an active waitlist When the system generates offers Then the highest-ranked eligible user by waitlist order and stored preferences is selected And a one-time, signed, personalized offer link is sent via the user’s preferred channel within 60 seconds of seat availability And opening the link starts a new short hold of 10 minutes (configurable) And if the hold expires or the user declines, no payment is taken and the link becomes invalid
Notification Preferences and Throttling Enforcement
Given a user has specified notification preferences (SMS and/or email) When sending waitlist offers or confirmations Then messages are sent only via opted-in channels And no more than 2 offer messages per user per class are sent within any rolling 24-hour period And at least 120 minutes must elapse between offers to the same user for the same class And SMS offers are not sent between 21:00 and 08:00 user local time; if SMS is restricted and email is opted-in, email is used; otherwise the SMS is queued until 08:00
Fairness and Simultaneous Offer Limits
Given a class has an active waitlist and openable seats When issuing offers concurrently Then the system limits active outstanding offers to a configurable maximum per class (default 3) And active short holds count toward this limit And upon successful checkout by any recipient, remaining active offers for that seat are canceled with a courteous notification, while users retain their waitlist rank And if all outstanding offers expire or are declined, the next ranked users are offered until the limit is reached
Analytics and Audit Trail for Fallback and Offers
Given users join the waitlist and receive offers When key lifecycle events occur Then the system records events: waitlist_joined, waitlist_rank_assigned, offer_sent, offer_delivered, offer_opened, offer_hold_started, offer_converted, offer_expired, offer_declined, offer_canceled, throttle_applied And each event includes: anonymized user ID, class ID, occurrence time, channel, rank at send, hold length (seconds), timestamps, and session context (UTM/referrer, device) And events are queryable in the analytics warehouse within 15 minutes of occurrence And audit logs allow tracing a user’s rank changes and offer lifecycle with timestamps for support use
Hold Abuse Prevention and Limits
"As a studio owner, I want safeguards against repeated seat holds without purchase so that genuine customers aren’t blocked from booking."
Description

Protections that deter gaming of the hold system, including per-user/device/IP rate limits, maximum concurrent holds, and optional identity verification after repeated holds. Links are bound to the intended recipient via signed tokens and device fingerprint signals to reduce shareability. Suspicious patterns trigger shorter hold windows or require manual confirmation. Provides an audit log for holds and redemptions to support dispute resolution and studio policy enforcement.

Acceptance Criteria
Rate Limiting Hold Creation by User/Device/IP
Given hold rate limits are configured: user_max_per_10m=3, device_max_per_10m=4, ip_max_per_10m=10 And the system tracks attempts per rolling 10-minute window When a user/device/ip initiates hold attempts up to the configured threshold Then each hold starts successfully with status=HELD and remaining_quota decreases accordingly When the same user/device/ip attempts an additional hold exceeding the threshold within the window Then the system rejects the attempt with HTTP 429 and error code HOLD_RATE_LIMITED And the response includes limit_type, limit_value, and retry_after_seconds And the event is logged with reason=RATE_LIMIT and user/device/ip fingerprints And once the window resets, a new hold attempt succeeds on the first try
Maximum Concurrent Holds per Entity
Given concurrent hold limits are configured: per_user=2, per_device=2, per_ip=5 And the user already has active holds equal to per_user across any classes When the user attempts to start an additional hold Then the system denies the hold with HTTP 409 and error code CONCURRENT_HOLD_LIMIT And the response includes active_hold_ids and next_eligible_at timestamp And the audit log records reason=CONCURRENT_LIMIT with counts per entity When one active hold is released or expires Then a subsequent hold attempt succeeds and the active count updates accordingly
Identity Verification After Repeated Holds
Given identity verification policy is configured: threshold_holds_24h=5, method=OTP_SMS, attempts_lockout=3, otp_ttl_seconds=600 And a user has initiated 5 or more holds in the last 24 hours or is flagged suspicious=true When the user attempts to start a new hold Then the system requires identity verification before creating the hold And an OTP is sent to the verified contact within 5 seconds And the OTP expires after 600 seconds and allows up to 3 incorrect attempts before temporary lockout of 15 minutes When the user completes verification successfully within the TTL Then the hold is created with status=HELD and verification_status=PASSED And all steps are recorded in the audit log with reason=VERIFICATION_REQUIRED
Recipient- and Device-Bound Checkout Link
Given a one-tap checkout link is generated with a signed token bound to recipient_contact, optional account_id, device_fingerprint_hash, single_use=true, and token_expiry=10m When the intended recipient opens the link on the bound device within 10 minutes Then the pre-filled checkout loads, a seat is held with status=HELD, and Apple/Google Pay is available When the link is opened by a different contact, account, or device fingerprint Then the system blocks redemption with error LINK_NOT_AUTHORIZED and offers the Join Waitlist flow And no additional hold is started for unauthorized opens And the token becomes unusable after redemption or expiry, with all attempts logged including token_id hash and outcome
Adaptive Hold Window and Manual Confirmation on Suspicious Patterns
Given suspicious pattern rules are configured: short_window_seconds=120, require_manual_confirm=true And the system detects any of: holds_and_abandons>=3 within 15 minutes; ip_asn_changes>=3 within 30 minutes; device_fp_mismatch_rate>=50% in last 20 attempts When the flagged user tries to start a new hold Then the hold window is reduced to 120 seconds and the UI requires explicit manual confirmation before starting And failure to confirm blocks the hold with error MANUAL_CONFIRM_REQUIRED And the countdown timer reflects the reduced window duration And the audit log records suspicious=true with matched_rule_ids
Comprehensive Hold and Redemption Audit Log
Given audit logging is enabled and write-once storage is configured When any hold lifecycle event occurs (created, extended, shortened, verified, denied, redeemed, expired, released, waitlisted) Then an immutable entry is written with: event_id, timestamp_utc, actor_id/contact, class_id, studio_id, device_fp_hash, ip_hash, token_id_hash, action, outcome, reason_code, prior_state, new_state, hold_window_seconds, verification_required, payment_method, related_transaction_id And entries are queryable via filters: date_range, actor, class_id, outcome, reason_code, ip_hash, device_fp_hash, token_id_hash, suspicious_flag And entries are retained for at least 400 days and exportable as CSV and JSON And attempts to modify existing entries are rejected with AUDIT_IMMUTABLE; only append-only correction events are permitted with prior_event_id linkage And fetching by hold_id returns the complete ordered lifecycle with no gaps
Hold Settings and Conversion Analytics
"As a studio owner, I want to tune hold settings and see conversion metrics so that I can optimize for higher paid bookings with fewer drop-offs."
Description

Merchant-facing controls to configure hold duration, wallet/payment options, invitation copy, and reminder messaging, plus dashboards that track key metrics such as hold starts, conversions, expirations, time-to-pay, and revenue attributed to One‑Tap Hold. Supports cohort and channel breakdowns, A/B testing of hold duration and copy, and event export to analytics integrations. Exposes configuration via API and enforces defaults to ensure consistent behavior across booking pages.

Acceptance Criteria
Hold Duration & Settings API Default Enforcement
Given a merchant sets a hold duration of 10 minutes in settings, When a one‑tap hold link is generated via dashboard or API, Then the hold expires exactly 10 minutes after the first open or wallet invocation, whichever occurs first. Given no class-level override exists, When any booking page generates a hold link, Then the merchant default duration is applied consistently across all booking pages. Given a duration outside the allowed range (1–30 minutes) is submitted, When settings are saved via UI or PATCH /v1/holds/settings, Then the request is rejected with HTTP 400 and the prior valid value remains active. Given settings are updated via API with a valid duration, When a subsequent hold link is generated, Then the new duration is effective within 60 seconds and reflected in the link payload and countdown timer. Given a hold expires, When the buyer attempts payment from the expired link, Then payment is blocked and the buyer is offered to join the smart waitlist in one tap.
Wallet/Payment Options Configuration
Given Apple Pay and Google Pay are toggled on, When a supported device opens the hold link, Then the wallet button renders above the card form within 1 second and is actionable. Given wallet options are toggled off, When the buyer opens the hold link, Then only card checkout is displayed with no wallet buttons present. Given the wallet button is tapped and payment succeeds, When analytics are updated, Then the payment method is attributed as wallet and included in One‑Tap Hold revenue. Given a wallet payment attempt fails, When the buyer retries within the active hold window, Then card checkout is offered as fallback without losing the hold. Given a change to wallet toggles is saved, When a new hold link is generated, Then the new options are honored for that link while existing links retain their original options.
Invitation Copy and Reminder Messaging
Given invitation copy uses variables {first_name}, {class_name}, {hold_minutes}, When a hold link is generated and previewed, Then the preview renders variables correctly and matches the sent SMS/email content. Given a reminder offset of 5 minutes before hold expiry is configured, When a buyer has not paid and the hold has more than 5 minutes remaining at first open, Then exactly one reminder is scheduled and sent at the configured offset via the selected channel. Given the hold is converted before the reminder time, When the scheduler runs, Then the pending reminder is automatically canceled and not sent. Given the buyer replies with an opt-out keyword, When future reminders are due, Then reminders are suppressed for that contact and the suppression is logged with timestamp. Given copy Variant A and Variant B are saved, When an A/B test is active, Then the correct variant-specific copy is inserted into the outbound message per the assigned variant.
Conversion Analytics Metrics Dashboard
Given events stream in real time, When the One‑Tap Hold dashboard is opened for a date range, Then it displays Hold Starts, Hold Conversions, Hold Expirations, Conversion Rate, Median Time‑to‑Pay, and Revenue Attributed to One‑Tap Hold for that range. Given a payment completes from a hold, When revenue is computed, Then the full order value is counted toward Revenue Attributed and included in the variant/channel breakdowns. Given the merchant account timezone is set, When the date range filter is applied, Then all timestamps and aggregations use the merchant timezone. Given daily metric rows for the period, When summed, Then the totals equal the summary metrics within rounding tolerance (<=0.5%). Given the dashboard is filtered to a specific class, When metrics load, Then only events for that class are included and load within 2 seconds for the last 30 days.
Cohort and Channel Breakdowns
Given filters Channel (SMS, Email, WhatsApp, Manual Link), Buyer Cohort (New, Returning), Class, and Instructor, When applied, Then the dashboard updates within 2 seconds and overall totals equal the sum of visible breakdowns. Given Channel = SMS is selected, When breakdowns are viewed, Then metrics include only events where delivery_channel = sms. Given cohort rules define Returning as any buyer with a paid booking in the prior 180 days, When cohort = Returning is applied, Then classification follows that rule consistently across all widgets. Given multiple filters are combined, When results are shown, Then counts remain consistent across widgets and exports for the same filter state. Given an export is requested for the current filtered view, When the CSV is downloaded, Then row totals match the on-screen totals.
A/B Testing: Hold Duration and Copy
Given an experiment with variants A (10 min) and B (15 min) at a 50/50 split is activated, When a buyer receives a hold link, Then the buyer is deterministically assigned to a variant and remains in that variant for 7 days for the same class. Given minimum sample size of 200 hold starts per variant is not met, When significance is requested, Then the dashboard displays "insufficient data" and does not declare a winner. Given a variant is paused, When new hold links are generated, Then traffic is reallocated proportionally to active variants only and the paused variant receives 0% of new assignments. Given a conversion event is recorded, When data is written, Then experiment_id and variant are stored on the event and reflected in per‑variant metrics and exports. Given SRM checks run hourly, When an imbalance >10% of expected allocation is detected, Then an alert banner is shown on the experiment with timestamp and suggested actions.
Analytics Event Export and Integrations
Given Segment and GA4 integrations are enabled, When hold events occur, Then events hold_started, hold_reminder_scheduled, hold_reminder_sent, hold_converted, hold_expired are exported with properties hold_id, class_id, channel, experiment_id, variant, time_to_pay_ms, revenue_cents, merchant_id. Given the external endpoint is unavailable, When an export fails, Then the system retries with exponential backoff for up to 24 hours and uses idempotency keys to ensure at‑least‑once delivery without duplicates. Given PII export is disabled for GA4, When events are sent, Then no email, phone, or names are included in the payload. Given a historical backfill is requested for a date range, When the job completes, Then all events in the range are re‑sent with backfill = true and the export report shows counts by event type matching dashboard totals for that range. Given a webhook signature secret is configured, When events are delivered to a custom webhook, Then each request includes an HMAC signature header that validates against the configured secret.

Abandoned Text Rescue

If someone taps but doesn’t finish, we send smart, timed follow‑ups: a gentle nudge, a quick-reply confirm, or nearby time suggestions—configurable tone and cadence. Increases conversion without manual texting and recovers bookings that would have slipped away.

Requirements

Abandonment Detection & Session Tracking
"As a studio owner, I want the system to detect when a customer abandons a booking so that automated follow-ups can recover the sale without my manual intervention."
Description

Detect when a user initiates but does not complete a booking, tracking key events (page view, class selection, form progress, payment step) and defining configurable abandonment thresholds (e.g., inactivity duration, tab close). Persist a session record with identifiers (known phone/email when available), class ID, time, price, promo, device, UTM/source, and the exact step where the user exited. Deduplicate across devices and retries. Emit a single, idempotent "abandoned booking" event to the messaging workflow, integrating with ClassNest’s booking pages, analytics pipeline, and event bus to ensure reliable triggers for follow-ups.

Acceptance Criteria
Track Key Booking Funnel Events
Given a user opens a ClassNest booking page for class {class_id} at {class_time} with UTM parameters configured When the user performs actions: page_view, class_select (with selected time), form_progress (step 1..N), and payment_step (entered) Then each action is recorded as a distinct event with an ordered timestamp and stable session_id And the payload for each event includes: class_id, class_time, displayed_price, promo_code_at_step (nullable), device_type, utm_source/medium/campaign/content/term (nullable), and referrer_url (nullable) And events appear in the analytics pipeline within 60 seconds of occurrence And the latest recorded event always reflects the most advanced step reached for the session
Inactivity Threshold Abandonment
Given the inactivity abandonment threshold T is configured to 5 minutes for booking pages And the booking has not been completed When no tracked activity occurs for more than T after the last recorded funnel event Then the system marks the session as abandoned at the last_step with last_event_timestamp And a single "abandoned_booking" event is emitted within 10 seconds of threshold crossing with fields: session_id, class_id, class_time, last_step, last_event_timestamp, inactivity_threshold_ms, idempotency_key And no "abandoned_booking" event is emitted if a completion event is recorded before T elapses
Tab Close Abandonment
Given "tab/window close" is enabled as an abandonment trigger And the booking has not been completed When the user closes the tab, navigates away, or the page visibility changes to hidden followed by unload Then the system finalizes the session as abandoned at the last reached step And one "abandoned_booking" event is emitted within 5 seconds of the close/unload signal with idempotency_key and exit_reason = "tab_close" And no abandonment event is emitted if a payment confirmation/completion event was recorded prior to the close signal
Persist Abandoned Session Record
Given an abandonment is determined for a session When the session record is persisted Then the record contains: session_id, known_identifiers (phone, email when available), class_id, class_time (UTC), displayed_price_at_exit, applied_promo_code (nullable), device_type, user_agent, utm_source/medium/campaign/content/term (nullable), referral_url (nullable), exit_step, exit_reason, last_event_timestamp (UTC), currency, locale And class_id, class_time, session_id, exit_step, last_event_timestamp are non-null And timestamps are stored in ISO-8601 UTC format And the record is upserted so repeat detections update the same session_id rather than creating duplicates
Cross-Device and Retry Deduplication
Given the same user is identifiable by phone and/or email across devices and browsers And the user initiates multiple attempts for the same class_id and class_time within a 24-hour dedup window without completing When abandonment is detected for each attempt Then only one downstream "abandoned_booking" event is delivered, using a stable idempotency_key derived from {user_identifier,class_id,class_time,window} And transport retries or replays do not create additional processed events due to the idempotency_key And if a new attempt occurs after the dedup window, a new unique "abandoned_booking" event is emitted And if a session starts anonymous and later captures phone/email, the persisted record is updated and deduplication is applied before emitting
Idempotent Event Emission and Integrations
Given an abandoned session has been finalized When the event is published Then it is sent to the ClassNest event bus with schema_version = v1, idempotency_key, and a validated payload matching the contract And the analytics pipeline ingests the event and exposes it under the abandoned_booking dataset within 2 minutes And the Abandoned Text Rescue workflow receives exactly one trigger per idempotency_key And publish and consumer acknowledgements are logged with correlation_id and publish_timestamp within 10 seconds of emission
Consent & Compliance + Opt-Out Management
"As a business owner, I want follow-ups to respect consent and quiet hours so that I stay compliant and maintain customer trust."
Description

Enforce messaging only to users with valid consent while honoring regional regulations (e.g., TCPA, GDPR, CASL) and business quiet hours. Maintain consent provenance (timestamp, source, channel), suppression lists, and audit logs. Include mandatory disclosures and opt-out keywords (e.g., STOP) in SMS, process opt-outs in real time, and prevent future sends. Validate phone/email deliverability, handle bounces, and provide email fallback when SMS is not permitted. Expose compliance settings in admin and integrate with provider webhooks for status updates.

Acceptance Criteria
Consent Gate for Abandoned Text Rescue Sends
Given a contact enters the Abandoned Text Rescue flow And the contact's SMS consent is absent, revoked, expired, or regionally restricted When the system evaluates eligibility to send an SMS follow-up Then the SMS is not sent and no request is made to the SMS provider And the decision is logged with reason code "CONSENT_BLOCK" and the applicable regulation (e.g., TCPA, CASL, GDPR) And if email consent is valid, an email variant is queued within the same step; otherwise the step is skipped And the contact timeline shows the channel decision and timestamp
Real-Time SMS Opt-Out, HELP, and Re-Opt-In Handling
Given a previously messaged contact replies with an opt-out keyword (STOP, CANCEL, UNSUBSCRIBE) in any case or with surrounding text When the inbound message is received via provider webhook or polling Then the contact's SMS channel is marked suppressed globally within 2 seconds And a single confirmation SMS is sent with brand, confirmation of opt-out, and how to rejoin (e.g., "Reply START") And the suppression applies immediately to all scheduled and triggered SMS across ClassNest, including Abandoned Text Rescue and waitlist offers And the opt-out event is recorded with timestamp, keyword, message ID, and source channel, immutable in audit logs When the contact sends HELP Then a HELP response is sent once per 24 hours without changing consent state When the contact sends START or UNSTOP after prior opt-out Then SMS consent is restored only in regions permitting keyword re-opt-in and provenance is recorded
Quiet Hours Enforcement by Recipient Timezone
Given business quiet hours are configured in Admin > Compliance (e.g., 21:00–08:00) with recipient-local timezone rules And a send in the Abandoned Text Rescue sequence is due during quiet hours for the recipient's timezone When scheduling the message Then the message is deferred to the next allowable window preserving the relative delay And the rescheduled time is displayed in the activity log and next-run timestamps And if deferral would exceed the maximum window for the sequence (configurable), the message is canceled with reason "QUIET_HOURS_EXPIRED" And admin changes to quiet hours take effect for future scheduling and are versioned/audited
Consent Provenance Capture and Auditability
Given a consent state change occurs via checkout checkbox, double opt-in SMS, API, admin action, import, or keyword When the change is saved Then a consent record is appended with fields: contact ID, channel, new state, legal basis, source, timestamp (UTC), IP (if web), user agent (if web), region And the latest consent state is reflected on the contact profile within 1 second And records are immutable; admin can only append changes, not alter history And authorized users can export consent history filtered by date range, channel, and contact
Mandatory Disclosures and Regional Compliance Footers
Given the recipient's region is resolved from phone country, profile, or IP fallback When rendering an SMS message for Abandoned Text Rescue Then the message includes brand identification and opt-out instruction (e.g., "Reply STOP to opt out") appropriate to the region And for US/CA, "Msg & data rates may apply" appears at first touchpoint per conversation where required And templates prevent removal of mandatory disclosures; attempting to save without required disclosures is blocked with validation And the final rendered content is archived with template ID, region, and disclosure version And Admin > Compliance allows configuring disclosure text per region with preview and audit trail
Deliverability Validation and Email Fallback
Given a contact has phone and/or email on file When preparing to send Abandoned Text Rescue Then the phone is validated to E.164 and carrier lookup is performed; invalid/unreachable numbers are marked undeliverable And if SMS is undeliverable or not permitted by consent, the system attempts email only if email consent is valid and status is not hard-bounced And hard-bounced emails immediately suppress the email channel and stop retries; soft bounces retry up to 3 times over 24 hours (configurable) And all channel decisions and delivery outcomes are recorded with provider status codes and timestamps
Provider Webhook Integration and Idempotent Updates
Given the SMS/email provider sends webhooks for delivery, undelivered, spam complaint, opt-out, and bounce events When a webhook is received Then the payload is authenticated (e.g., HMAC), deduplicated by event ID, and processed idempotently And contact channel states and suppression lists are updated according to the event type within 2 seconds p95 And related sends are updated with final status and reason codes And failures are retried with exponential backoff up to 5 attempts and surfaced in an admin error log And webhook processing is disabled if verification fails and an alert is generated
Smart Cadence Orchestration
"As an instructor, I want configurable timing rules for follow-ups so that I can balance conversion with customer experience."
Description

Orchestrate a configurable sequence of follow-ups (gentle nudge, quick-reply confirm, nearby time suggestions) with per-workspace settings for delays, total touches, cooldown windows, and daily caps. Respect user time zones, studio quiet hours, and capacity constraints; cancel or skip messages if the class fills or the user books through another channel. Support rate limiting per user and per class, and pause when a user engages. Integrate with the live waitlist and availability service to ensure messages are timely and relevant.

Acceptance Criteria
Workspace-Level Cadence Configuration Applied
Given a workspace config with delays per step [d1, d2, ...], total_touches T, cooldown_minutes C, and daily_cap D (workspace-wide) When an abandoned checkout event is recorded for a user in that workspace Then the system schedules at most T touches relative to the abandonment timestamp using the configured delays And a minimum of C minutes separates any two scheduled sends for that user within the cadence And no more than D outbound messages are sent by the workspace across all recipients in a calendar day (workspace timezone); additional steps are deferred to the next allowed day while preserving order And if the workspace updates any cadence setting before unsent steps fire, the remaining schedule is recalculated within 60 seconds to reflect the latest config And if any setting is missing, the system applies the global default for that field and records a configuration-fallback audit entry
Time Zone and Quiet Hours Compliance
Given a recipient with a resolved local timezone (from profile or last known device); if unavailable, use the workspace timezone And quiet hours configured as local start/end times (e.g., 21:00–08:00) and allowed days When a touch’s computed send time falls within quiet hours or outside allowed days Then the send is deferred to the next allowed window in the recipient’s local timezone, preserving step order and cooldowns And messages honor daylight saving transitions, targeting the same intended wall-clock times And no message is sent during quiet hours even if a daily cap reset occurs And each deferral due to quiet hours is logged with original and adjusted times
Availability- and Capacity-Aware Messaging
Given a target class with capacity N and real-time availability from the availability service When evaluating a scheduled touch within 2 minutes prior to send time Then the system cancels class-specific nudges/confirms if the class is full or if an auto-offer waitlist is active with pending offers And nearby time suggestions include only classes that have ≥1 open seat at evaluation time And if the target class filled since the last step, all remaining steps for that class are canceled; optional alternative suggestions are generated only if they pass availability checks And no message references a past start time at the moment of send; such touches are dropped as expired and recorded
Auto-Pause on Booking or User Engagement
Given the user replies to the message, taps the booking link, or a booking is recorded for the target class via any channel When this engagement occurs before the next scheduled touch Then the cadence is paused immediately and all remaining steps for that class are canceled if a booking is confirmed And if the user only clicks (no booking), subsequent steps are put on hold for engagement_pause_minutes and then resume at the next step unless further engagement occurs And resumption must continue to honor cooldown windows, quiet hours, and caps And duplicate confirmations are not sent if a booking was already recorded within the last 5 minutes
Per-User and Per-Class Rate Limits
Given per-user and per-class rate limits configured: per_user_max_touches_per_24h U and per_class_max_outbound_per_15m K When scheduling or sending any touch Then the system prevents sending if it would cause the user to exceed U across all campaigns in a rolling 24-hour window; the touch is deferred to the earliest compliant time And the system ensures no more than K messages referencing the same class are sent across all recipients in any rolling 15-minute window; excess touches are queued or deferred And deferred touches preserve sequence order and cooldowns; if a touch cannot be sent before its expiry window (e.g., class start minus 30 minutes), it is dropped as expired And all rate-limit blocks/deferrals are recorded with counters and next-available send timestamps
Cooldown Windows and Sequencing Integrity
Given a cadence with cooldown C minutes and max total touches T When any touch is delayed due to quiet hours, caps, rate limits, or availability Then subsequent steps are shifted so that at least C minutes separate each send And the total number of sends across the cadence never exceeds T, including after deferrals and shifts And if shifting would push a step beyond its expiry window, that step is skipped and the sequence continues, still enforcing C And any schedule recomputation occurs within 60 seconds of the blocking condition change
Live Waitlist Integration
Given the user is on the live smart waitlist for the target class When the waitlist auto-offers a seat to the user or the class becomes full Then the abandoned-text cadence for that class is canceled to prevent conflicting outreach And if the user declines or the offer expires, the cadence may resume with the next step, but only if availability checks pass at send time And while a waitlist offer is active for the user, no confirm-style message is sent for that class And nearby time suggestions prioritize sessions flagged by the availability service as likely-to-open, when configured
Templates & Personalization Controls
"As a studio owner, I want customizable templates and tone options so that messages feel on-brand and relevant."
Description

Provide a library of SMS and email templates with configurable tones (gentle, friendly, direct) and dynamic variables (name, class, time, location, price, credit balance). Support conditional content, multilingual variants, brand sender profiles, preview/test sends, and template versioning with rollback. Personalize with past attendance, instructor preference, and proximity when available. Store render logs for traceability and integrate with ClassNest’s branding and localization systems.

Acceptance Criteria
Abandoned Text Rescue: Template Library & Tone Controls
Given I am an admin in ClassNest, When I create an Abandoned Text Rescue template, Then I can select channel (SMS or Email) and tone (gentle, friendly, direct). Given a tone is selected, When I preview the template, Then tone-specific copy variants render accordingly. Given a template is saved, When I view the template list, Then it displays name, channel, tone, languages, active status, and current version. Given the flow is enabled, When no default template exists for a channel, Then activation is blocked and I see a clear error to set a default template.
Abandoned Text Rescue: Dynamic Variables Rendering & Validation
Given a template uses variables {name}, {class}, {time}, {location}, {price}, {credit_balance}, When rendering for a contact, Then each variable resolves from booking context and user data within 200 ms per render. Given a variable is missing data, When rendering, Then the configured fallback text is used or the token is removed without broken placeholders, and a warning is logged. Given an unknown variable token is present, When saving the template, Then save is prevented with an inline error identifying the invalid token. Given timezone and currency are known, When rendering {time} and {price}, Then they are formatted using the recipient’s locale settings.
Abandoned Text Rescue: Conditional Content & Personalization Rules
Given a template has rules based on past attendance count, favorite instructor, and proximity (radius in km), When conditions are met, Then the corresponding content block is included; otherwise the default block is included. Given multiple rules match, When rendering, Then the first matching rule by priority order is applied and others are ignored. Given proximity is configured to 10 km, When the recipient is >10 km from the selected class location, Then the template suggests up to 3 nearby times within 48 hours and within 10 km. Given the recipient has a favorite instructor, When rendering suggestion blocks, Then classes by that instructor are prioritized.
Abandoned Text Rescue: Multilingual Variants & Localization
Given a template defines variants for en, es, fr, When the recipient locale is fr-CA, Then the fr variant is used; if unavailable, the default language is used. Given localized formatting rules, When rendering {date}, {time}, {price}, Then output follows the recipient locale’s formats and currency symbol. Given an SMS is rendered in a language requiring Unicode, When previewing, Then the system displays GSM vs Unicode encoding and estimated segment count before send. Given a template lacks any variant for the tenant’s default language, When saving, Then an error prevents save until a default is provided.
Abandoned Text Rescue: Brand Sender Profiles & Theming
Given a brand sender profile with email From/Reply-To, SMS sender ID/number, logo, and color theme exists, When sending previews and live messages, Then emails use the brand theme and SMS use the configured sender where compliant. Given the destination country disallows alphanumeric sender IDs, When sending an SMS, Then the system automatically uses a compliant long code/short code and records the fallback in the render log. Given no brand sender profile is configured, When attempting to activate the Abandoned Text Rescue flow, Then activation is blocked with an actionable error to set a brand profile.
Abandoned Text Rescue: Preview & Test Sends
Given a template, When I open Preview, Then I can select a test persona/contact and see the fully rendered message with rule hits, resolved variables, and SMS character/segment count. Given I click Send Test, When I enter a verified test phone/email, Then only the test recipient receives the message and the send is marked as Test in logs. Given links are present in the template, When sending a test, Then links are safe for test use and do not affect production analytics.
Abandoned Text Rescue: Versioning, Rollback, and Render Logs
Given I edit a template, When I save, Then a new immutable version is created with editor, timestamp, changelog note, and diff view available. Given I select a previous version, When I click Rollback, Then that version becomes current and the action is audited without deleting newer history. Given any message render, When it occurs, Then a render log is stored with template ID/version, input context snapshot, selected tone/language/rules path, resolved variables, sender profile used, localization/branding details, and outcome; logs are retained and searchable for at least 180 days.
One‑Tap Resume & Quick‑Reply Booking
"As a customer, I want to complete my booking with a single tap or quick reply so that I don’t have to re-enter my details."
Description

Generate secure deep links that resume checkout at the exact abandoned step with prefilled data, and support SMS quick-reply flows (e.g., reply Y to confirm) that finalize booking and charge the saved payment method. Handle authentication via magic links or signed tokens, validate inventory in real time, apply promos, and return clear confirmations/receipts. Provide robust error handling (expired link, sold-out, card declined) and safe fallbacks back into checkout. Integrate with payments, bookings, and notifications to update all systems atomically.

Acceptance Criteria
Resume Checkout via Secure Magic Link
Given a user abandoned checkout at a specific step and a signed deep link with a 30-minute expiry was issued When the user opens the link within its validity window on any device Then the user is authenticated via the token without entering a password And the checkout opens on the exact abandoned step And previously entered non-sensitive fields (name, email, phone, selected class/date, promo) are prefilled And sensitive card data is never prefilled; a saved payment method may be selected if available And if the booking was already completed for this token, the user is redirected to the receipt instead of checkout
SMS Quick-Reply 'Y' to Confirm Booking
Given the user has SMS opt-in, a verified phone, a valid saved default payment method, and an active seat hold for the selected class When the system receives an SMS reply of 'Y' (case-insensitive) within the hold window Then the booking is finalized and the saved payment method is charged for the quoted total And a confirmation SMS with receipt URL and an email receipt are sent And duplicate 'Y' replies within 10 minutes are idempotent and return the same confirmation without double-charging And if no valid saved payment method is on file, the user receives a secure link to complete checkout instead and no charge is attempted
Real-Time Inventory Validation and Seat Hold on Resume
Given a user resumes checkout via deep link When the checkout loads Then the system validates real-time availability for the selected class/time And if seats are available, a hold is placed for at least 5 minutes and a visible countdown is shown And if sold out or fewer seats remain than requested, the user sees a clear sold-out message with one-tap options to join waitlist or view nearest available times And holds auto-expire and release inventory when the timer ends or the user cancels, and extend while the user actively progresses
Promo Retention, Validation, and Pricing Consistency
Given a promo code or automatic discount was applied before abandonment When the user resumes checkout or confirms via SMS Then the promo is re-applied only if still valid per rules (expiry, usage, eligibility) And if invalid or expired, the user is informed of the new total before any charge and can confirm to proceed And displayed totals (subtotal, taxes, fees, discount) equal the final captured amount across resume and SMS flows And promo stacking/exclusivity rules are enforced consistently
Expired or Tampered Link Safe Fallback
Given a deep link is opened after expiry or with a modified/invalid token When the system verifies the token signature and timestamp Then access is denied without revealing any PII or booking details And the user sees a friendly explanation with a one-tap button to request a fresh magic link And a new link request can be fulfilled via SMS/email within 30 seconds And the event is logged with reason codes; no session is created until re-authenticated
Payment Processing and Atomic Updates
Given a booking confirmation is initiated via SMS quick-reply or checkout submit When payment is authorized and captured Then charge creation, booking record write, inventory decrement, and notification dispatch succeed atomically under a single idempotency key And if any step fails (e.g., capture decline, notification error), the transaction is rolled back, the seat hold is released, and no charge persists And retries using the same idempotency key do not create duplicate charges or bookings And a failure response includes a secure recovery link back to the appropriate checkout step
Clear Confirmations, Receipts, and Error Messaging
Given a booking completes via resume link or SMS When confirmations are generated Then the user receives SMS and email confirmations containing class name, date/time, location, attendee, amount charged, last4, receipt URL, cancellation policy, and support contact And the receipt URL is accessible via signed link without login and from within the user account when logged in And for errors (expired link, sold-out, card declined), the user sees a clear message and a one-tap fallback that returns them to the correct checkout step to recover
Nearby Time Suggestions Generator
"As a customer, I want relevant alternative times when my original slot doesn’t work so that I can still book easily."
Description

Offer dynamic alternative times and classes near the original selection based on current availability, location, instructor, user preferences, and travel buffers. Curate 2–3 best matches, embed as tappable options in messages, and ensure deep links land directly on the selected alternative. Fall back to waitlist or next-available if the original class is full. Perform last-moment inventory checks at send time to avoid stale offers and integrate with the smart waitlist for auto-offers when appropriate.

Acceptance Criteria
Offer 2–3 Best Nearby Alternatives on Abandoned Checkout
Given a user abandons booking after selecting a specific class/session And the system has the original selection’s time, location, instructor, and the user’s preferences and travel buffer When generating rescue suggestions at send time Then compute a ranked list using availability, time proximity, distance proximity, instructor match, and preference fit And select the top 2 or 3 distinct sessions with seats > 0 And ensure each suggestion occurs within the configured time window (default ±48 hours) and within the configured distance radius (default 8 km or 5 mi) And exclude any session that violates hard user preferences or travel buffer constraints
Deep Links Land Directly on Selected Alternative
Given a user taps a suggestion link in SMS or email When the link opens on mobile or desktop Then the app loads the exact alternative session detail/checkout with that session preselected And the user can confirm the booking in 2 taps or fewer from landing And pricing and promotions displayed match those computed at send time And if authentication is required, the user is returned to the same preselected session after sign-in And if the session is no longer available at tap, the page shows an availability change notice and presents updated nearby alternatives or a waitlist join option
Pre‑Send Inventory Freshness Check
Given a set of suggested alternatives has been computed When a rescue message is about to be sent Then the system revalidates seat availability for each suggestion within 1 second before send And replaces any suggestion whose available seats <= 0 with the next best available match And if fewer than 2 viable matches remain, include a waitlist or next‑available link so that the message contains at least 2 options
Smart Waitlist Integration and Auto‑Offer Coordination
Given the user has an active smart waitlist position for the original or a suggested class When generating suggestions and while the message remains valid Then do not suggest a class for which the user already has an active waitlist position unless issuing an auto‑offer And if a smart waitlist auto‑offer is triggered for the user, suppress conflicting suggestions and send the auto‑offer message instead And upon the user accepting a suggestion, remove or update any overlapping waitlist entries within 5 seconds to prevent double booking
Fallback to Waitlist or Next‑Available When No Nearby Match
Given no alternatives meet the configured time/distance and preference constraints When preparing the rescue message Then include a waitlist join link for the original class with class and time prefilled And include a next‑available session link outside the nearby window if one exists And ensure the total number of options in the message remains 2–3 with no duplicate classes
Tappable Options Render and Track Correctly Across Channels
Given a rescue message is composed for SMS and email When the message is sent Then SMS includes 2–3 numbered options with distinct per‑recipient short links And email includes 2–3 button CTAs with distinct per‑recipient links And each link encodes a unique suggestion ID and click ID for attribution And each link resolves in under 1.5 seconds to the landing page under normal network conditions And all clicks are captured with timestamp and suggestion ID for conversion analysis
Respect Travel Buffers and Existing Bookings
Given the user has upcoming bookings and a configured travel buffer (e.g., 30 minutes) When computing alternative suggestions Then do not propose any session that starts or ends within the buffer window before or after any existing booking And do not propose sessions whose estimated travel time between consecutive locations exceeds the configured buffer And ensure the final 2–3 suggestions satisfy these constraints
Recovery Analytics & A/B Testing
"As a studio owner, I want reports and A/B testing on follow-ups so that I can optimize recovery rates and revenue."
Description

Provide dashboards and exports for abandonment rate, send volume, deliverability, CTR, resume rate, recovered bookings, and revenue uplift. Segment by channel, cadence, tone, class, and campaign. Enable A/B testing for templates, timing, and suggestion sets with significance indicators and guardrails. Offer event-level logs for troubleshooting and privacy-safe aggregation for insights. Expose APIs/CSV for downstream reporting and tie metrics to studio-level ROI to guide optimizations.

Acceptance Criteria
Dashboard KPIs & Definitions
- Given an admin user with analytics access and a studio selected, When they open the Recovery Analytics dashboard and select a date range, Then the dashboard displays abandonment_rate, send_volume, deliverability_rate, ctr, resume_rate, recovered_bookings, and revenue_uplift with tooltip definitions. - Given a date range is applied, When the user compares widget totals to the summary table/export for the same filters, Then values match within a tolerance of max(1 unit, 1%). - Given a studio timezone is set, When the dashboard loads, Then all metrics are calculated in the studio timezone. - Given a KPI is clicked, When the user drills down, Then a detail table appears grouped by class and campaign with sortable columns and matching totals. - Given there is no data for the period, When the dashboard loads, Then an empty state is shown and coverage dates are displayed.
Segmentation & Filters
- Given the dashboard is open, When the user applies filters for channel, cadence, tone, class, and campaign (multi-select, AND logic), Then charts and tables update within 2 seconds and reflect the filters. - Given a segment dimension is selected, When Group by is set to any one of channel, cadence, tone, class, or campaign, Then the results show a breakdown by the chosen dimension and the sum equals the filtered total within max(1 unit, 1%). - Given filters are cleared, When the user selects Reset filters, Then all filters return to All and totals match the unfiltered baseline. - Given a user navigates between dashboard and detail views, When filters are applied, Then the same filters persist until explicitly reset.
A/B Test Setup & Allocation Guardrails
- Given a campaign with at least 2 variants (templates/timing/suggestion sets), When the user creates an A/B test, Then they can add up to 3 variants and set allocation percentages that sum to 100%. - Given minimum sample size per variant is set (default 500 sends), When the test runs, Then winner evaluation does not occur until each active variant reaches its minimum sample size. - Given a variant’s deliverability_rate or ctr falls below 80% of control after at least 200 sends, When guardrails are enabled, Then that variant is auto-paused and its traffic reallocates to the control within 15 minutes. - Given daily eligible abandon events are fewer than 50, When the user configures a test, Then the system displays an estimated duration and a recommendation to extend the test window.
A/B Significance & Result Reporting
- Given an A/B test is running and minimum sample sizes have been reached, When calculating the primary metric (recovered_bookings_rate), Then a winner is flagged only when p-value < 0.05 with a 95% confidence interval displayed for uplift. - Given more than two variants are tested, When multiple comparisons are evaluated, Then Benjamini–Hochberg FDR control is applied at q=0.10 before labeling any winner. - Given a test is stopped before significance, When results are viewed, Then the status reads Inconclusive and no winner label is shown while current estimates and confidence intervals are displayed. - Given a segment filter is applied, When viewing test results, Then significance and confidence intervals are recalculated for the filtered population.
Event-Level Logs & Privacy-Safe Aggregation
- Given event logging is enabled, When an abandon-related event occurs, Then a log entry is written including event_id, timestamp (ISO8601 UTC), user_pseudo_id, studio_id, class_id, campaign_id, channel, variant_id, template_id, message_id, event_type, and revenue_amount when applicable. - Given PII handling, When storing identifiers, Then phone and email are hashed with SHA-256 using a per-studio salt and no raw PII is stored in logs or exports. - Given an analyst with permission views logs, When filtering by date range, studio, campaign, or event_type, Then results are returned with pagination and export limited to the filtered set. - Given retention policies, When querying historical logs, Then event-level data is available for 13 months and aggregated metrics for 36 months. - Given privacy-safe aggregation, When any aggregated cell has unique users < 10, Then the cell displays — and is excluded from CSV/API aggregates. - Given export operations, When a user triggers any export, Then an audit log is written with user_id, timestamp, and filter parameters.
Data Export & API Access
- Given the user requests a CSV export for a 90-day range up to 100k events, When the export is initiated, Then the file is generated within 60 seconds and includes a header with schema_version and column names. - Given larger exports or longer ranges, When generation exceeds 60 seconds, Then an asynchronous job is created and a secure link is emailed on completion; the link expires in 7 days. - Given CSV formatting, When the file is downloaded, Then it is UTF-8 encoded, RFC 4180 compliant, uses comma separators, and fields are properly quoted. - Given the analytics API, When a client calls GET /v1/recovery-analytics with OAuth2 and filters (date_from, date_to, channel, cadence, tone, class_id, campaign_id), Then JSON results are returned with cursor pagination, rate limit 60 requests/min, and a last_processed_at watermark. - Given API pagination, When next is requested, Then the response includes next and prev cursors until the dataset is exhausted. - Given data definitions, When a user requests the data dictionary, Then a downloadable artifact is available that lists fields and metric definitions with versioning.
Studio-Level ROI Attribution
- Given recovered bookings are linked, When a user resumes and completes a booking within 7 days of the last abandon follow-up for the same class and studio, Then the booking is counted as recovered and attributed to the active campaign/variant. - Given ROI calculation, When viewing studio-level results for a date range, Then revenue_uplift is computed as recovered_booking_revenue minus baseline revenue (control variant if present; otherwise 28-day pre-period rate), and ROI is revenue_uplift minus messaging_costs. - Given currency settings, When results are displayed, Then amounts are shown in the studio’s currency with two decimal places and totals reconcile with detail tables within max(1 unit, 1%). - Given cancellations and refunds, When Include cancellations is enabled, Then refunded amounts tied to recovered bookings are subtracted from revenue_uplift.

Member FastPass

Recognizes returning texters and passholders. Auto-applies credits or saved payment, pre-fills details, and supports a simple “Reply Y to confirm” flow for repeat classes. Cuts repeat booking time to seconds, encourages pass usage, and delights loyal clients.

Requirements

SMS Identity Recognition & Consent Management
"As a returning client who texts my studio, I want the system to recognize me and my membership so that I can rebook instantly without re-entering my details."
Description

Implement phone-number-based identity resolution that recognizes returning texters and links them to existing ClassNest customer profiles and memberships/passes across a multi-tenant environment. Persist and enforce per-tenant SMS consent (opt-in/out), honor DNC lists, and comply with TCPA, GDPR, and CCPA. Merge duplicates safely, prevent cross-tenant data leakage, and create lightweight profiles when no match exists. Expose webhooks/events for downstream personalization and ensure real-time retrieval of membership status to power FastPass decisions.

Acceptance Criteria
Return Texter Mapped to Correct Tenant Profile
Given an inbound SMS from +14155551212 to Tenant A's messaging channel and profiles for that number exist in Tenant A and Tenant B When identity resolution runs Then the system matches the message to the Tenant A profile only And the comparison uses E.164-normalized phone values And if multiple Tenant A profiles share the same number, the canonical profileId is selected per dedupe rules And identity lookup completes within 300 ms p95 And an identity.resolved audit record is stored with tenantId, profileId, phone, and timestamp
Per-Tenant SMS Consent Capture and Enforcement
Given number +14155551212 is opted out for Tenant A and opted in for Tenant B When an outbound non-transactional SMS is initiated by Tenant A Then the message is blocked, no carrier send occurs, and a compliance.blocked event is recorded And when the user replies STOP to Tenant B, the consent state for Tenant B becomes OptedOut with timestamp, source=SMS, and a confirmation STOP-ack message is sent And when the user replies START or UNSTOP to Tenant B, consent becomes OptedIn and a confirmation is sent And consent state reads and writes are scoped per tenant and persisted with tenantId, phone, state, timestamp, and source
Honor Global and Tenant Do-Not-Call Lists
Given number +14155551212 is on the global DNC list or Tenant A's DNC list When any outbound SMS is requested by Tenant A Then the system refuses with error code SMS_DNC_BLOCKED and prevents send And a compliance.blocked event is emitted with reason=DNC and tenantId And no webhook containing message content is delivered to third parties
Lightweight Profile Creation for First-Time Texter
Given an inbound SMS from a phone number with no existing profile in Tenant A When identity resolution runs Then a lightweight profile is created in Tenant A with phone as primary identifier and consentState=Unknown And an opt-in prompt is queued for reply per policy And profile.created and identity.resolved events are emitted with tenantId and profileId And profile creation plus event publishing completes within 500 ms p95
Duplicate Profile Safe Merge Within Tenant
Given two profiles in Tenant A share the same E.164 phone number When merge is initiated by dedupe rules or admin action Then the target profile retains the canonical profileId and the source profile is archived And membership, pass balances, and booking history are preserved and associated to the target And the most restrictive consent state is applied to the merged profile And a merge audit record is stored with before/after snapshots and the merge is reversible for 30 days
Cross-Tenant Data Isolation for Shared Numbers
Given +14155551212 has a profile in Tenant B but not in Tenant A When an inbound SMS arrives on Tenant A's channel Then a new Tenant A lightweight profile is created and no data from Tenant B is read or returned And any membership status query is scoped to Tenant A and returns NotFound if no membership exists in Tenant A And emitted webhooks include tenantId and profileId specific to Tenant A only
Real-Time Membership Status for FastPass and Event Webhooks
Given a recognized returning texter with an active pass in Tenant A When the user sends "Book" and identity is resolved Then the system retrieves membership status in real time and returns a decision within 300 ms p95 indicating credits available And events identity.resolved, consent.state.current, and membership.snapshot are posted to configured webhooks within 2 seconds p95 with HMAC-SHA256 signatures and retries using exponential backoff for up to 24 hours And if membership status cannot be retrieved within 750 ms, the decision falls back to "payment required" and a degraded.performance flag is emitted
Credit & Pass Auto-Application Engine
"As a passholder, I want my eligible credits applied automatically when I book so that I can confirm quickly without managing balances manually."
Description

Automatically detect and apply available class credits, passes, and memberships during the rebooking flow. Validate eligibility rules (class types, instructor restrictions, blackout dates), handle edge cases (insufficient credits, expired passes), and support partial or split payments when needed. Guarantee atomic updates to inventory and balances, with idempotent operations to avoid double-charging or double-deducting. Provide clear confirmation text summarizing what was applied and remaining balance, and emit analytics for pass utilization and breakage reduction.

Acceptance Criteria
Auto-Apply Eligible Credit on SMS Rebooking
Given a returning client with 1 eligible class credit and a saved payment method And the class has available inventory When the client replies "Y" to the FastPass SMS for that class Then exactly 1 credit is deducted from the correct pass/membership And the client is charged $0 And the seat is reserved And the confirmation SMS contains the class name, start time (local), "1 credit applied", and the remaining credit count And an analytics event "BookedWithCredit" is emitted with user_id, pass_id, class_id, credits_used=1 within 2 seconds
Enforce Pass Eligibility and Blackout Rules
Given a returning client with a pass that is not valid for the selected class due to restricted class type, instructor restriction, or blackout date When the client replies "Y" to confirm Then no credits are deducted And the pass is not marked as used And the system charges the saved payment method for the full price And the confirmation SMS states "pass not eligible" with reason_code in {CLASS_TYPE_RESTRICTED, INSTRUCTOR_RESTRICTED, BLACKOUT_DATE} And an analytics event "PassIneligible" is emitted with reason_code and a "BookedPaid" event emitted for the fallback charge
Split Payment When Credits Are Insufficient
Given a premium class that requires 2 credits And the client has exactly 1 eligible credit and a saved payment method When the client replies "Y" to confirm Then 1 credit is deducted from the correct pass And the monetary remainder equal to price_of_missing_credits at time of booking is charged to the saved payment method And the seat is reserved And the confirmation SMS states "1 credit applied + $X charged" and shows the remaining credit count And an analytics event "SplitPayment" is emitted with credits_used=1 and amount_charged=$X
Handle Expired Pass With Fallback to Saved Payment
Given a client has an expired pass with remaining credits When the client replies "Y" to confirm a class covered by that pass product Then no credits are deducted And the expired pass remains unchanged And the system charges the saved payment method for the class price And the confirmation SMS includes "pass expired on YYYY-MM-DD" and the amount charged And an analytics event "PassExpiredFallback" is emitted with pass_id and expiry_date
Atomic Inventory and Balance Transaction
Given the last seat of a class is available And the client has 1 eligible credit When two booking attempts for the same user and class are processed concurrently Then at most one booking record is created And inventory is decremented exactly once And credits are deducted exactly once And if the transaction fails, no inventory or credits are changed (full rollback) And the operation outcome is recorded with a transaction_id in the audit log
Idempotent Processing of Duplicate Confirmations
Given the system receives duplicate "Y" confirmations for the same user and class within a 5-minute window When the second confirmation is processed using the same idempotency key derived from (user_id, class_id, class_start) Then the original booking reference and confirmation details are returned And no additional charges or credit deductions occur And an analytics event "IdempotentDuplicate" is emitted with duplicate=true
Confirmation Summary and Analytics Emission
Given any booking confirmation generated by the engine When the booking is finalized Then the SMS confirmation includes: class name, date/time (local), location or join link, what was applied (credit/pass/membership), amount charged (if any), remaining credits or next renewal date, cancellation policy snippet (<=120 chars), and a unique booking reference And the SMS is sent within 5 seconds of booking commit And analytics include a "PassUtilized" event with remaining_credits_after when credits are used, and a "BreakageReduced" event when a user with previously idle credits uses a credit
Saved Payment Autopay with SCA Fallback
"As a repeat buyer, I want my saved card to be charged when I reply Y so that I can book instantly without opening a browser."
Description

Charge saved (tokenized) payment methods automatically when no valid credit/pass applies, completing a booking from a single SMS reply. Use PCI-DSS compliant tokenization, support SCA/3DS where required with a secure fallback link for step-up authentication, and handle retries, declines, and refunds gracefully. Send receipts, update ledger entries, and reconcile with existing payment providers. Ensure idempotency, fraud checks, and configurable studio-level rules (e.g., limit autopay by amount or region).

Acceptance Criteria
Autopay Success via Reply Y with Saved Token
Given a recognized returning client with a valid saved tokenized payment method and no applicable credit/pass And the studio has Autopay enabled for Member FastPass When the client replies "Y" to the SMS within 15 minutes of the offer Then the system authorizes and captures the exact booking amount using the saved tokenized method via the configured provider And the booking status updates to Confirmed within 3 seconds of provider approval And an SMS and email receipt are sent within 5 seconds including amount, currency, provider transaction ID, and booking ID And a ledger entry is created with booking ID, customer ID, token ID (masked), amount, fees, net, currency, provider transaction ID, and timestamp And no full PAN or CVV is stored or logged; only token and last4 are retained
SCA Fallback Link — Successful Authentication
Given an autopay attempt returns a step-up required (SCA/3DS) response for the saved method And the client is in an SCA-mandated region When the system detects challenge_required during autopay Then a unique, single-use, HTTPS 3DS authentication link is sent via SMS within 2 seconds And the booking slot is held for 10 minutes with a visible Pending Payment status When the client completes the 3DS challenge successfully before expiry Then the payment is captured and the booking is marked Confirmed immediately And receipts are sent and the ledger is updated as in the Autopay Success scenario
SCA Fallback Link — Expired or Failed Authentication
Given an SCA/3DS authentication link was issued for the booking When the link expires, is abandoned, or the authentication fails Then no payment is captured and the booking reverts to Not Confirmed And the held slot is released immediately And the client receives an SMS with a secure payment link to retry or change card And a decline/failure event is logged with provider reason code and no more than one notification is sent
Idempotent Handling of Duplicate Confirmations and Retries
Given a booking offer is active and the client sends duplicate "Y" replies or the payment webhook is retried When requests share the same idempotency key composed of offer_id and customer_id Then only one payment is captured and one booking is created And subsequent duplicate attempts return the original success without additional charges And audit logs record deduplication with correlation IDs
Declines and Retries with Customer Notifications
Given an autopay attempt fails with a soft decline or transient network error When the response is retryable Then the system retries up to 2 times with exponential backoff, completing all retries within 2 minutes And if all retries fail or a hard decline occurs, the booking is not confirmed and no further retries are made And the client receives an SMS explaining the failure category and a secure link to update payment And failure events include provider reason codes and are available in operational logs
Studio Rules and Fraud Screening Gate Autopay
Given studio-level autopay rules are configured (max autopay amount, allowed regions, allowed card brands) and fraud screening is enabled When an autopay is initiated Then the system checks rules and risk signals (velocity per customer <= N per 24h, blacklist, device/phone reputation) before charging And if any rule is violated or risk score >= threshold, autopay is blocked, no charge occurs, and a secure pay link is sent instead And the decision, triggered rules, and risk score are logged and visible to staff And staff can override per booking via admin with a reason note
Refunds, Ledger Adjustments, and Provider Reconciliation
Given a confirmed, paid booking created via autopay When the class is canceled by the studio or the client cancels within refundable policy Then a full or pro-rated refund is issued to the original payment method within 1 business day And a refund receipt is sent and the ledger records a reversing entry linked to the original transaction And daily reconciliation stores settlement, fee, and payout references from the provider and flags mismatches for review And the booking and customer balances reflect the refund immediately
Reply Y to Confirm Conversational Flow
"As a loyal client, I want to confirm my usual class by replying Y so that I can book in seconds with minimal effort."
Description

Deliver a robust SMS conversational flow that summarizes the target class (name, time, location, cost/credit usage), requests a simple "Reply Y to confirm," and handles variations (N to cancel, C to change, HELP/STOP). Include language localization, carrier guidelines compliance, timeouts with inventory holds and automatic release, error handling for ambiguous inputs, and confirmation messages with calendar add links. Ensure high throughput, rate limiting, and resilience, with telemetry for delivery, engagement, and conversion.

Acceptance Criteria
Y-to-Confirm Happy Path with Calendar Link
Given a recognized returning user requests to book a specific class with available inventory When the system sends the SMS prompt Then the SMS includes: class name, date and start time with time zone, location (or "Online"), total cost or credit usage, and the instruction: "Reply Y to confirm, N to cancel, C to change, HELP for help, STOP to opt out", plus a short branded link to details And the message is queued within 1 second and delivered p95 within 10 seconds When the user replies "Y" (case-insensitive, ignores surrounding whitespace) within the hold window Then a single booking is created exactly once for that user and class And a confirmation SMS is sent within 5 seconds containing booking ID, start/end time, location, add-to-calendar links (Google and ICS), and the final price or credits used And no duplicate booking is created for repeat "Y" replies within 5 minutes
Non-Y Responses: N Cancel, C Change, and Ambiguous Input Handling
Given the user has received the booking prompt When the user replies "N" or a recognized negative variant (e.g., "No", "n") Then any inventory hold is immediately released and a cancellation confirmation SMS is sent with a link to browse other classes When the user replies "C" or a recognized change variant (e.g., "change") Then the user is offered up to 3 alternative time slots for the same class or nearest matches within 7 days, each selectable via "Reply 1/2/3" or a link; choosing one restarts the Y-to-confirm prompt for that option When the reply is ambiguous or invalid (e.g., emojis, empty, multiple conflicting tokens) Then the system sends one clarification SMS restating the summary and valid options (Y/N/C) And after 3 invalid replies or 2 minutes of inactivity, the session is closed and any hold is released with a closing notification
Pass Credits Auto-Application with Transparent Messaging
Given the user has an active pass with sufficient eligible credits for the target class When the user replies "Y" Then credits are decremented atomically by the class credit cost and no card charge is attempted And the confirmation SMS states "Credits used: <X>; Remaining: <Y>" and the pass name When the user has insufficient credits or pass restrictions apply Then credits are not decremented and the flow proceeds to saved payment handling as defined in Saved Payment Auto-Charge with SCA/Failure Handling
Saved Payment Auto-Charge with SCA/Failure Handling
Given the user has a default saved payment method on file and credits are not applicable When the user replies "Y" Then the saved payment is charged for the full amount including taxes/fees using a tokenized method; no PAN is sent via SMS And upon success, a confirmation SMS is sent within 5 seconds including amount charged and last 4 digits of the card When the charge fails (decline or processor error) Then the user is notified within 10 seconds, the hold is maintained for the remaining hold time, and a secure link to update/complete payment is provided; on success the booking confirms, on expiry it cancels and the hold is released When SCA/3DS is required Then a secure authentication link is sent immediately, the hold is extended up to 10 minutes, and the booking is confirmed only after successful authentication; on failure or timeout the booking is canceled and the hold is released
Timeout, Inventory Hold, and Automatic Release with Concurrency Safeguards
Given the booking prompt has been sent and inventory is available When the session starts Then a seat is held for 5 minutes (configurable per tenant) and the hold start/expiry times are recorded When no valid confirming reply is received before hold expiry Then the seat is automatically released and the user is notified that the offer expired with a link to try again When multiple users attempt to confirm the last seat concurrently Then only the first successful credit decrement or payment authorization succeeds; others receive a "class is full" message with a link to join the waitlist; no duplicate bookings or charges occur And all state transitions use idempotency keys tied to session_id so that retries (e.g., webhook replays) do not create duplicate bookings
Localization and Carrier Compliance (A2P/STOP/HELP/Templates)
Given the user's language preference (or locale fallback to English) and region-specific carrier policies When sending any message in the flow Then the message uses the correct localized template, formats date/time per the class time zone and locale, and includes the brand name at least once per session And every prompt includes an opt-out hint (e.g., "Reply STOP to unsubscribe") When the user replies HELP Then the system returns help text in the user's language with a support link and no state change to the booking session When the user replies STOP Then the number is immediately opted out, a STOP confirmation is sent, the session is terminated, and no further messages are sent And all outbound messages comply with regional A2P rules (e.g., content restrictions, sender registration), limit to ≤3 segments, and use a branded or first-party short link domain
Telemetry, Throughput, Rate Limiting, and Resilience SLOs
Given normal and peak operating conditions When the flow runs Then the system sustains ≥100 outbound SMS/sec across tenants with p95 provider-ack <2s and p99 <5s And p95 time from user "Y" receipt to confirmation SMS is <3s without SCA and <30s with SCA And per-user rate limiting enforces ≤4 outbound messages/minute and ≤1 active booking session per phone number And exponential backoff with jitter is applied on carrier 429/5xx with up to 3 retries; queued messages survive process restarts Then telemetry events are emitted for: message_queued, message_delivered, reply_received, booking_confirmed, payment_succeeded/failed, credit_applied, hold_started/released, session_closed; each includes tenant_id, session_id, class_id, phone_hash, timestamps, language, carrier, delivery_status, and amount_charged when applicable And events are queryable in analytics within 60 seconds and retained for ≥30 days; alerting thresholds are configured for delivery error rate and conversion drop And monthly availability for this flow meets ≥99.9%
Prefilled Profile & Preferences
"As a returning client, I want my details prefilled and easy to update so that I don’t have to retype information every time I book."
Description

Pre-populate booking with stored client details (name, email, waivers on file, preferred class times/instructors, accessibility needs) and allow lightweight edits via SMS prompts (e.g., update email or emergency contact). Persist updates back to the customer profile, respect privacy constraints, and surface only information necessary for the booking. Ensure compatibility with mobile-first flows and existing reminders, and propagate changes to downstream systems via events.

Acceptance Criteria
Prefill Core Profile on FastPass Booking
Given a returning client with stored name, email, valid waiver status, and saved preferences When they initiate a FastPass booking via SMS or mobile booking link Then the booking summary is prefilled with name and email And the system auto-selects the preferred instructor and time if available; otherwise selects the closest match and indicates the change And the prefilled summary renders in under 2 seconds on a 3G-equivalent connection And the client can confirm by replying "Y" without additional input unless required data is missing
Update Email via SMS Prompt
Given the client enters the FastPass flow and replies "update email" or a bounced email is detected When the system prompts for a new email and the client replies Then the email is validated (format per RFC 5322 basic rules and domain MX check) within 3 seconds And on success the system confirms the update and uses the new email for the current booking And the updated email is persisted to the profile with an updated_at timestamp
Update Emergency Contact via SMS Prompt
Given the client selects to update emergency contact during FastPass When prompted, the client provides contact name and phone number Then the phone is validated to E.164 format and the name is non-empty And the system confirms the update and masks the phone except last 2 digits in any SMS echo And the booking flow continues without restart and the contact is persisted to the profile
Privacy-Scoped Data Exposure
Given any FastPass SMS or prefilled booking summary When presenting user data to the client Then SMS content includes at most first name and booking summary; excludes full email, full phone, and accessibility notes And the mobile page shows only fields required to complete the booking and masks email (e.g., j***@domain.com) And accessibility needs are never sent via SMS and are visible only in staff-facing views And if consent flags (e.g., marketing_opt_in=false) restrict use, no opt-in prompts or marketing text are shown
Waiver On-File Check and Prompt
Given a client with a valid waiver on file When initiating FastPass booking Then no waiver step is shown and booking can be confirmed immediately Given a client with no waiver or an expired waiver When initiating FastPass booking Then the client receives a mobile-friendly waiver link and booking confirmation is blocked until waiver status is valid And upon signature the profile waiver status updates and the booking auto-resumes to confirmation
Preference-Based Defaults with Inline Change
Given the client has preferred instructors and time windows stored When a FastPass booking is initiated for a class with available slots Then the preferred instructor and time window are applied by default And the client can reply "change" to select a different option within the SMS flow And if the client replies "save" after choosing a new option, the preference is updated in the profile; otherwise preferences remain unchanged
Downstream Event Propagation
Given any profile, waiver status, or preference update occurs during FastPass When the update is persisted Then a corresponding event (CustomerProfileUpdated, WaiverStatusUpdated, or PreferencesUpdated) is published within 5 seconds And the event includes profile_id, changed_fields, actor=client, source=fastpass, occurred_at, and correlation_id And events retry with exponential backoff up to 5 attempts on transient failures and log success/failure with the correlation_id
Smart Class Selection & Waitlist Integration
"As a regular attendee, I want the system to suggest the best upcoming class or waitlist me automatically so that I can rebook without browsing schedules."
Description

Automatically select the most relevant upcoming class based on the client’s attendance history, preferred time window, and instructor affinity. If the target class is full, offer instant waitlist enrollment and integrate with the live smart waitlist to auto-offer openings via SMS with a single-reply confirmation. Respect pass eligibility and studio business rules, handle time zone differences, and provide clear alternatives when multiple matches exist.

Acceptance Criteria
Auto-select Next Best Class by History, Time Window, and Instructor Affinity
Given a recognized returning client with recorded attendance history, a preferred time window, and instructor affinity scores When the client initiates a FastPass booking via SMS Then the system selects a single Target Class scheduled within the client's local preferred window on the earliest date with open spots And the selected class is taught by the highest-ranked available instructor by affinity; if none in window, the nearest class within ±60 minutes of the window is selected And if multiple classes still qualify, the earliest start time is chosen; if still tied, the class with the earliest date is chosen And the selection response includes class name, date, local time with timezone, instructor name, and a prompt to confirm
Pass Eligibility and Studio Business Rules Enforcement
Given the system has identified a Target Class and the client's active passes, credits, membership, and studio rules When evaluating eligibility for the Target Class Then ineligible classes by pass type, membership tier, level restrictions, blackout dates, daily/weekly limits, or advance-booking windows are excluded from auto-selection And if the client has sufficient eligible credits, one credit is preselected for use and no card charge is attempted And if credits are insufficient but a saved payment method exists, the confirmation prompt states the charge amount and requires an explicit "Y" before any charge And if neither credits nor payment are available, the client receives a clear message and alternative eligible options
Full Class Waitlist Offer and Enrollment
Given the Target Class is full and waitlist is enabled When the client initiates FastPass booking Then the system offers waitlist enrollment via SMS with clear instructions and position estimate And on receiving "W", the client is added to the waitlist and receives a confirmation SMS with their position and opt-out command And no credits are deducted and no charges occur at waitlist join time
Smart Waitlist Auto-Offer with Single-Reply Confirmation
Given a client is on the waitlist for a class and a spot becomes available When the system issues an auto-offer via SMS Then the offer reserves the seat for a configurable hold window (default 10 minutes) and includes class details, price/credit usage, and "Reply Y to confirm" And on "Y" within the hold window, the booking is confirmed, the appropriate credit is applied or payment is charged, and a confirmation SMS is sent And if no valid reply is received within the hold window, the offer expires, the seat is offered to the next eligible waitlisted client, and the original client is notified of the missed offer And eligibility and pricing are revalidated at offer time; ineligible clients are skipped and not offered
Time Zone-Aware Selection and Messaging
Given the client and studio may be in different time zones When presenting class options, confirmation prompts, and booking receipts Then all times are displayed in the client's local time with timezone abbreviation while bookings are stored in the studio's time zone And Daylight Saving Time transitions are handled correctly for both selection and display And if the client's time zone cannot be determined, studio time is used and explicitly labeled as such
Multiple Matches Alternative Options
Given more than one class meets relevance criteria within the selection rules When responding to the client's booking request Then the message lists the top 3 alternatives with numbered options including date, local time, instructor, and location And the system accepts "1", "2", or "3" to select an alternative, "M" to see more options, and responds with help text on invalid input And upon a valid selection, the chosen class becomes the Target Class and the confirmation prompt is sent immediately
FastPass One-Reply Confirmation with Auto-Apply Credits/Payment
Given a returning client with saved details and a selected Target Class When the confirmation prompt is sent Then replying "Y" confirms the booking using eligible credits first, otherwise charging the saved payment method, with no additional data entry required And the booking creation, credit application or charge, and confirmation SMS are completed within 3 seconds of receiving "Y" And replying "N" cancels the flow with no booking and no charges and provides a prompt to view alternatives
FastPass Admin Controls & Analytics
"As a studio owner, I want controls and analytics for FastPass so that I can tune the experience and measure its impact on repeat bookings and revenue."
Description

Provide studio admins with controls to enable/disable FastPass, set eligibility rules (e.g., limit autopay amounts, require SCA always), customize message templates, and configure rate limits. Deliver analytics on conversion time, repeat booking rate, pass utilization, no-show deltas, and revenue attributable to FastPass. Include audit logs of SMS interactions and financial events, exportable reports, and A/B testing toggles to measure impact without code changes.

Acceptance Criteria
Toggle FastPass at Account and Class Levels
Given an admin with permissions toggles FastPass OFF at the account level, When a recognized returning client initiates a booking via SMS, Then the FastPass "Reply Y to confirm" flow is not offered and a standard booking link is sent within 2 seconds. Given FastPass is OFF globally but ENABLED for a specific class, When a recognized client initiates a booking for that class, Then FastPass is offered for that class only. Given FastPass is ON globally but DISABLED for a class or instructor, When a recognized client initiates a booking in that context, Then FastPass is not offered. Given an admin changes any FastPass enable/disable setting, When the next eligible message is processed, Then the change takes effect within 60 seconds and an audit record is created with admin ID, timestamp, scope (account/class/instructor), previous value, and new value. Given any toggle state, When an ineligible user (unrecognized number) initiates a booking, Then standard booking is offered and no FastPass audit entry is created for that user.
Eligibility Rules: Autopay Limits and SCA
Given an admin sets an autopay limit of X (currency), When a booking price is less than or equal to X and a valid saved payment method exists, Then the charge is auto-applied without requiring entry and the booking is confirmed. Given the booking price exceeds X, When FastPass is initiated, Then no auto-charge occurs and the client receives a secure payment link to complete payment. Given "Require SCA always" is enabled, When an auto-charge is attempted, Then a 3DS/SCA flow is initiated and the booking is confirmed only after successful SCA; on failure, the booking remains unconfirmed and a fallback message is sent. Given a client has pass credits, When FastPass booking occurs, Then pass credits are applied first and decremented atomically; if insufficient, payment collection follows configured limits and SCA rules. Given any eligibility rule is evaluated, When the outcome is determined, Then the decision, rule ID, and reason are written to audit logs tied to the booking/payment IDs.
Message Template Customization and Validation
Given an admin opens Messaging Settings, When editing the FastPass SMS and email templates, Then placeholders {first_name}, {class_name}, {start_time}, {confirm_token} are available for insertion. Given a template is saved, When any required placeholder for the flow is missing, Then the save is blocked with a descriptive error specifying the missing placeholders. Given a template draft, When the admin clicks Preview, Then a rendered preview with sample data is shown including SMS character count and segment count, and email subject/body preview. Given a test destination is entered, When Send Test is clicked, Then a test SMS/email is delivered to the destination within 10 seconds and the event is logged. Given a template is updated, When saved, Then a new version is created with version ID, editor, timestamp, and change summary stored in the audit log; rollback to prior version is possible.
Rate Limiting and Abuse Protection
Given an admin configures limits, When per-user FastPass confirmations exceed N per hour/day, Then additional confirmations are throttled with a friendly message and no duplicate bookings are created. Given studio-level SMS rate limit is set to M messages per minute, When outbound volume would exceed M, Then messages are queued/throttled and delivery timestamps reflect queuing; no data loss occurs. Given idempotency keys are generated per booking intent, When duplicate "Y" confirmations arrive within 60 seconds, Then only one booking is created and the duplicates are acknowledged without creating new bookings. Given rate limits are enforced, When a throttle event occurs, Then a log entry with rule ID, user ID, timestamp, and count is recorded and visible in analytics. Given time-based limits, When evaluating windows, Then limits are applied in the studio’s configured timezone and reset at window boundaries accurately.
FastPass Analytics Dashboard
Given a date range is selected, When the dashboard loads, Then it displays median and p90 conversion time (reply to confirmation), repeat booking rate via FastPass, pass utilization rate, no-show delta (FastPass vs non-FastPass), and revenue attributable to FastPass ($ and %). Given filters are applied (location, instructor, class type, platform SMS/email, campaign/tag, A/B variant), When metrics are recalculated, Then results reflect the filters consistently across all widgets. Given the dashboard is viewed, When data freshness is checked, Then a "Last updated" timestamp is shown and data is no older than 15 minutes. Given a metric info icon is clicked, When the definition tooltip opens, Then it shows the exact formula and inclusion/exclusion rules. Given an admin clicks Export, When exporting up to 100k rows, Then a CSV download starts within 60 seconds and contains the same filtered data with column headers and ISO-8601 timestamps.
Audit Logs of SMS and Financial Events
Given any inbound or outbound SMS/email event, When it is processed, Then an immutable log record is created with UTC timestamp, direction, channel, phone/email, user ID, template ID, delivery status, provider message ID, and a redacted body or content hash. Given any financial event (auth, capture, refund), When it occurs, Then an immutable log record is created with booking ID, amount, currency, taxes/fees, payment method ID (tokenized), SCA result, provider IDs, and outcome. Given logs are stored, When a user with audit permissions searches by date range, user, booking, phone, or payment ID for the last 30 days, Then results return within 3 seconds for up to 10k records. Given export is requested, When exporting logs for a selected range up to 30 days, Then CSV and JSON exports are available and begin within 30 seconds; records include a signature to detect tampering. Given retention is configured to 24 months, When the retention window elapses, Then older records are archived or purged per policy with an audit entry of the action.
No-Code A/B Testing Toggles
Given an admin creates an experiment, When defining variants (control + up to 2 variants) and traffic allocation, Then users are bucketed deterministically by user ID and remain in the same variant across sessions and channels. Given an experiment is started or paused, When the state changes, Then the change takes effect within 2 minutes and is reflected in the audit log. Given variants use different FastPass toggles or message templates, When eligible users interact, Then they receive the variant-specific behavior without requiring a code deploy. Given the experiment dashboard is viewed, When comparing variants, Then it displays conversion time, repeat booking rate, and revenue attributable by variant with sample sizes and confidence intervals (if computed) or at least raw deltas. Given multiple experiments target the same audience, When a new experiment is launched, Then users already enrolled in an active experiment are excluded to prevent overlap unless explicitly overridden by the admin.

Auto‑Language Replies

Detect the texter’s preferred language and reply with localized class details, policies, and checkout pages. Customize per-language templates and keywords. Reduces confusion for multilingual communities and boosts completion rates across diverse audiences.

Requirements

Language Detection Engine
"As a studio owner, I want incoming messages to automatically detect the sender’s language so that replies and links match their preference without manual intervention."
Description

Implement a server-side engine that infers a texter’s preferred language from inbound message content, historical conversation context, user profile settings, and phone/country metadata. Assign a confidence score, persist the detected language on the contact record, and expose the value to downstream messaging workflows via webhook context and internal APIs. Support at minimum English, Spanish, French, Portuguese, and German at launch, with extensible language packs. Respect user overrides when explicitly set and handle code-switching by re-evaluating language per thread while maintaining stable preferences once confidence is high. Provide decision logs for observability and QA, and a feature flag to enable/disable per workspace.

Acceptance Criteria
Detect Language with Confidence and Signal Precedence
Given an inbound message is received for a contact And historical conversation messages, contact profile language, and phone/country metadata are available When the engine evaluates all signals Then it outputs detected_language in {en, es, fr, pt, de, und} And outputs confidence as a float 0.00–1.00 with two decimal places And uses precedence: user_override > current message content > recent conversation (last 30 days) > profile setting > phone/country metadata And sets effective_language to detected_language when confidence >= 0.70 And sets effective_language to workspace_default when detected_language = und or confidence < 0.50 And marks effective_language as provisional when 0.50 <= confidence < 0.70 And completes detection in ≤300 ms at p95
Persist Detected Language and Protect Overrides
Given effective_language is determined for a contact When confidence >= 0.70 and there is no user_override Then the contact record persists language_code (ISO 639-1), confidence, decided_at timestamp, and source And no write occurs if the persisted language_code equals effective_language within the last 24 hours And if user_override is present, the engine does not update persisted language_code And persistence failures are retried up to 3 times with exponential backoff And a metrics counter increments for persist_success and persist_failure
Expose Language to Webhooks and Internal APIs
Given a detection event completes When the outbound webhook fires Then the payload includes detected_language, effective_language, confidence, overridden (boolean), sources_used[], decision_id, and decision_trace_url And sensitive raw message content is excluded And internal API GET /contacts/{id} returns persisted language_code, confidence, decided_at, and overridden fields And internal API POST /messages accepts language override and returns effective_language in the response And added fields conform to the published schema and are documented in OpenAPI
Code-Switching Re-evaluation with Stability Controls
Given a thread has effective_language = en with confidence 0.80 When subsequent messages exhibit consistent Spanish content (per-message content confidence >= 0.80) for 3 consecutive messages within 1 hour Then the engine sets effective_language = es for the thread And persists es only if the rolling window of last 5 messages yields aggregate confidence >= 0.75 and no user_override exists And enforces a cooldown of 2 hours before another persisted language change And logs the switch_reason and prior_language in the decision log
Supported Languages and Extensible Packs
Given the system is initialized Then supported_language_codes contains en, es, fr, pt, de When a new language pack is installed for it-IT Then detection can return it with confidence within 5 minutes without service redeploy And if a message in an unsupported language arrives, detected_language = und and effective_language = workspace_default And installing/removing a pack updates supported_language_codes and is reflected in health and readiness checks
Decision Logs and Traceability for QA
Given any detection decision is produced Then a decision log entry is written with decision_id, timestamp, detected_language, effective_language, confidence, sources_scores, precedence_applied, prior_language, persisted_change (boolean), override_state, and workspace_id And logs are queryable via internal API GET /decision-logs?contact_id=... within 10 seconds of the event And PII is redacted according to policy And sampling rate is 100% by default and configurable per workspace
Workspace Feature Flag Controls
Given a workspace has feature flag language_detection disabled When messages arrive Then no detection is performed, no persistence occurs, and webhooks omit detection fields And effective_language resolves to workspace_default When the flag is enabled Then detection and exposure behaviors become active within 60 seconds And the flag state at decision time is recorded in decision logs
Localization Template Manager
"As an instructor, I want to configure reply templates for each language so that students receive accurate, localized information without me rewriting messages."
Description

Provide an admin UI and backend to create, edit, and organize per-language SMS/email reply templates with dynamic variables (e.g., class name, time, location, price, policy link, checkout URL). Support placeholders, conditional sections (e.g., if waitlist), preview with sample data, test-send to a number/email, and versioning with rollback. Map templates to triggers (first contact, booking help, policy request, waitlist offer, confirmation, reminders). Enforce validation for required variables and enable fallbacks to a default language when a localized template is missing. Include import/export for bulk management and access controls per role.

Acceptance Criteria
Create and validate per-language template with dynamic variables
Given I have Template Editor permission and select language "es" and channel "SMS" When I create a new template using placeholders {class_name}, {time}, {location}, {price}, {policy_link}, {checkout_url} Then the system recognizes and highlights all placeholders and validates placeholder syntax Given a trigger is selected for the template When I attempt to save and any required placeholders for that trigger are missing Then the save is blocked and an inline error lists each missing placeholder by name Given the template contains an undefined placeholder {unknown_var} When I click Save Then the system rejects the save with an error identifying {unknown_var} as undefined Given the template uses only defined placeholders and meets required variables When I click Save Then the template is persisted under the chosen language and channel with a unique ID and timestamp
Conditional sections render based on waitlist context
Given the template includes a conditional block for waitlist content When previewing with sample data where waitlist=true Then the conditional section content appears and no conditional markers remain in the output Given the same template When previewing with sample data where waitlist=false Then the conditional section content is omitted and the message has no stray punctuation or extra whitespace Given the template includes nested conditionals for waitlist and has_spot When previewing with waitlist=true and has_spot=false Then inner conditional content is omitted and the output contains no unresolved tokens
Preview and test-send parity with sample data
Given a valid template and selected sample data When I click Preview Then all placeholders are resolved and no unresolved tokens remain in the rendered content Given the same template and preview result When I click Test Send to a valid phone number or email Then the delivered message content exactly matches the preview (SMS body for SMS; subject and body for email) and the UI shows delivery status Given I enter an invalid phone number or email for test send When I click Test Send Then the system shows a validation error and no message is sent Given the messaging provider returns a send failure When the test send is attempted Then the UI displays a failure state with the provider error code/message
Trigger mapping with language selection and default fallback
Given the system supports triggers: first_contact, booking_help, policy_request, waitlist_offer, confirmation, reminder When I map a Spanish template to the confirmation trigger Then Spanish recipients of the confirmation trigger receive the Spanish template Given a recipient preference is French and no French template exists for booking_help When booking_help is triggered Then the system sends the default-language template for booking_help and records a fallback event in logs Given I set the default language to English When any trigger fires for a language without a localized template Then the English template is used automatically Given a trigger has no template in the default language When I attempt to activate the mapping Then activation is blocked with an error requiring a default-language template
Template versioning, compare, and rollback
Given an existing template at version v1 When I edit and save changes Then a new version v2 is created with author, timestamp, and change summary captured Given versions v1 and v2 exist When I open the compare view Then differences between v1 and v2 are highlighted for subject and body/content Given versions v1 and v2 exist When I choose Rollback to v1 Then a new current version v3 is created as a copy of v1 and an audit log records the rollback action and actor
Bulk import and export with validation and reporting
Given a properly formatted import file (CSV or JSON) with N template records When I run a Dry Run import Then the system validates structure and variables and reports counts for to-create, to-update, skipped, and errors with line numbers Given a Dry Run reports zero errors When I Confirm Import Then the system applies changes and the final summary matches the Dry Run counts exactly Given an import file with schema errors or undefined placeholders When I attempt to import (non–Dry Run) Then no changes are applied and a detailed error report is shown Given I select languages [en, es] and triggers [confirmation, reminder] When I click Export Then the system generates a downloadable file containing the selected templates with version metadata
Role-based access controls for template management
Given role permissions are defined for Admin, Editor, and Viewer When logged in as Admin Then I can create, edit, delete templates, configure trigger mappings and default language, perform import/export, and execute rollback Given I am logged in as Editor When using the Template Manager Then I can create and edit templates and use preview/test-send but cannot change trigger mappings, default language, or run import/export Given I am logged in as Viewer When accessing the Template Manager Then I can view templates and previews but cannot modify content or test-send Given a user without permission attempts a restricted action When the action is invoked via UI or API Then the UI control is disabled or hidden and the API returns HTTP 403 Forbidden
Multichannel Auto-Replies
"As a studio admin, I want the system to auto-reply in the user’s language across SMS and email so that students get timely, relevant responses on their preferred channel."
Description

Automatically send localized replies over SMS and email (and WhatsApp if enabled) based on detected language and configured templates. Integrate with existing messaging pipelines to thread responses in the same conversation, apply rate limits and quiet hours, and include trackable deep links. Queue and retry on transient failures, and surface delivery/engagement metrics to the activity timeline. Provide per-channel enablement toggles and safeguard rules to avoid duplicate sends across channels.

Acceptance Criteria
Language Detection and Template Selection
- Given an inbound message in language L with detection confidence >= 0.8, When generating the auto-reply, Then use the per-language template for L and render all placeholders without error. - Given detection confidence < 0.8 and the contact has preferredLocale Y, When generating the auto-reply, Then use the template for Y and log reason "fallback:low-confidence". - Given detection confidence < 0.8 and the contact has no preferredLocale, When generating the auto-reply, Then use the template for account.defaultLocale and log reason "fallback:default-locale". - Given the inbound text begins with a configured keyword K mapped to template variant T for locale Z, When generating the auto-reply, Then select template T for locale Z overriding the default. - Given a deep link is included, When rendering the message, Then include ln={locale}, utm_source={channel}, utm_medium=auto-reply, utm_campaign=autolanguage-replies, and a signed token that expires in 24 hours. - Given any placeholder fails to resolve, When rendering the message, Then abort the send, log the error to the timeline, and send a plain fallback only if account setting fallbackOnTemplateError = true.
SMS Auto-Reply Threading and Content
- Given an inbound SMS from a valid number and the SMS channel is enabled, When auto-replying, Then send the SMS within 5 seconds at p95 and 10 seconds max latency. - Given an inbound SMS conversation, When sending the auto-reply, Then post from the same SMS number and preserve the same conversation/thread ID. - Given an SMS auto-reply, When composing content, Then include class title, next session datetime in the contact’s timezone, price, a cancellation policy summary (<=120 chars), and a checkout deep link. - Given the composed SMS exceeds 2 GSM segments, When sending, Then shorten links and truncate the policy summary to keep total length <= 2 segments. - Given provider delivery callbacks are received, When processing them, Then attach delivery status updates to the same conversation on the timeline. - Given the SMS channel is disabled, When an inbound SMS arrives, Then do not auto-reply via SMS and log reason "channel-disabled" with the evaluated fallback channel (if any).
Email Auto-Reply Localization and Threading
- Given an inbound email and the Email channel is enabled, When auto-replying, Then send the email within 60 seconds at p95 latency. - Given an auto-reply email, When composing, Then set Subject to "Re: {original-subject} [{locale}]" and set In-Reply-To and References to the inbound Message-ID to ensure threading. - Given an auto-reply email, When sending, Then include both HTML and plain-text parts with localized content and tracked deep links. - Given the sending domain configuration, When sending, Then SPF, DKIM, and DMARC alignment all pass as reported by the provider. - Given email provider webhooks, When delivery and engagement events arrive, Then update the timeline with accepted/bounced/open/click events for that message. - Given the Email channel is disabled, When an inbound email arrives, Then do not auto-reply via Email and log reason "channel-disabled".
WhatsApp Handling, Template Use, and Duplication Safeguard
- Given the WhatsApp channel is enabled and the inbound message is within the 24-hour session window, When auto-replying, Then send a locale-specific approved template with dynamic fields populated. - Given the 24-hour session window has expired or the locale template is not approved, When deciding the channel, Then do not send via WhatsApp and fall back to the next available channel per priority rules. - Given a single inbound event, When dispatching auto-replies, Then ensure exactly one channel sends using a deduplication key and record the chosen channel on the timeline. - Given all channels are disabled, When processing the inbound event, Then send nothing and record "all-channels-disabled" on the timeline. - Given WhatsApp delivery and read receipts are received, When processing them, Then update the timeline statuses accordingly.
Rate Limits and Quiet Hours
- Given multiple inbound messages from the same contact on the same channel within 10 minutes, When auto-replying, Then send at most one auto-reply and suppress subsequent sends with timeline notes. - Given an account throughput limit of 120 auto-replies per minute, When the limit is reached, Then queue additional sends and process them FIFO once capacity is available. - Given quiet hours configured as 21:00–07:00 in the account timezone, When an inbound message arrives during quiet hours, Then queue the reply and do not send immediately. - When quiet hours end, Then send at most one aggregated auto-reply within 2 minutes for each contact with queued messages and mark the remaining queued ones as suppressed.
Queueing, Retry, and Idempotency
- Given a transient failure (HTTP 5xx, provider timeout, or 429 rate limit), When sending a message, Then retry up to 3 times with exponential backoff (~2s, 8s, 32s) with jitter and keep total retry window <= 3 minutes. - Given a permanent failure (HTTP 4xx excluding 429), When sending, Then do not retry; instead log the provider error code and reason to the timeline. - Given repeated processing of the same inbound event, When attempting to send, Then use idempotency key "{contactId}:{channel}:{inboundId}" to guarantee exactly-once delivery. - After the maximum retries are exhausted, When the message still fails, Then move it to a dead-letter queue and emit an alert to monitoring.
Activity Timeline Delivery and Engagement Metrics
- Given an auto-reply is dispatched, When creating the timeline entry, Then include channel, locale, template ID, send timestamp, conversation/thread ID, and deduplication key. - Given delivery/engagement webhooks are processed, When updating the timeline, Then add queued/sent/delivered/failed timestamps and error codes; for Email also add open/click; for SMS/WhatsApp add click/read where available. - Given a deep link click event is received, When correlating events, Then attribute it to the originating timeline entry within 60 seconds and to the correct channel and locale. - Given any timeline update, When processed, Then surface it in the UI within 5 seconds of event processing.
Localized Checkout and Policy Deep Links
"As a student, I want the checkout and policy pages to open in my language from the message link so that I can understand details and complete booking confidently."
Description

Generate and attach language-specific deep links for checkout and policy pages, ensuring the mobile-first pages render in the same locale as the outgoing message. Append a locale parameter and signed token for secure, one-tap continuity, persist the locale across the session, and fall back to the default language when localized content is unavailable. Include UTM parameters for attribution and ensure links are shortenable for SMS.

Acceptance Criteria
Checkout Deep Link Contains Locale and Signed Token
Given an outbound auto-language reply with detected locale "es-MX" When the system generates the checkout deep link Then the URL includes query parameter locale=es-MX using a valid IETF BCP 47 tag And the URL includes a signed token parameter st that validates server-side and is bound to the recipient, resource, and locale And removing or altering locale, st, or resource id causes token validation to fail and access is denied with a non-PII error page
Locale Persistence Across Checkout Session
Given a user opens a checkout deep link containing locale=fr-CA When they navigate through class details, attendee info, payment, and confirmation pages in a single session Then all pages and system messages render in fr-CA And the locale is persisted across page refreshes and step transitions without reverting to default And the final confirmation page and receipt use fr-CA
Fallback to Default Language When Translation Missing
Given a policy or checkout deep link with locale=it-IT And specific translation keys for the target page are unavailable in it-IT When the page loads Then missing strings fall back to the account default language without breaking layout And the locale context remains it-IT for all available strings and subsequent requests And no mixed-language occurs for the same string key
Policy Page Deep Link Mirrors Locale and Security
Given an outbound message that references studio policies with detected locale "de-DE" When the system generates the policy deep link Then the URL includes locale=de-DE and a signed st token And opening the link renders the policy page in de-DE or falls back to default if de-DE content is unavailable And token validation, tamper detection, and error handling mirror the checkout deep link behavior
UTM Attribution Parameters Attached and Preserved
Given an outbound message sent via SMS using template "WaitlistOffer" When generating any checkout or policy deep link Then the URL includes utm_source=sms, utm_medium=outbound, utm_campaign=AutoLanguageReplies, and a non-empty utm_content identifying the template or keyword And after any redirects or shortening, analytics receive the same UTM values on the landing request And UTM parameters do not affect token or locale validation outcomes
SMS-Shortenable Links Resolve With Full Context
Given link shortening is enabled for SMS When a localized deep link with locale, st, and UTM parameters is shortened Then the short URL uses HTTPS and resolves to the original destination And the resolved request preserves the original locale, valid st token, and UTM parameters unchanged And click tracking records locale and campaign metadata associated with the short URL
Signed Token Expiration and Error Handling
Given a generated deep link includes a signed st token with a configurable TTL When the link is opened after the token expires or the token is malformed Then the server responds with 401/403 and displays a localized error page in the request locale if valid, else in the default language And no sensitive data is exposed in the page or URL And the user is offered a safe path to request a fresh link
Per-Language Keyword Mapping
"As a support manager, I want language-specific keywords to trigger the right actions so that students can self-serve in their own language."
Description

Enable configuration and detection of localized keywords (e.g., book, cancel, waitlist, help, stop) per language, including common variants, misspellings, and conjugations. Provide an admin interface to customize keywords and precedence rules by language and tenant. Integrate with the detection engine to disambiguate commands from free text and route to the correct localized template or workflow. Maintain an audit log of keyword changes and support sandbox testing before publishing.

Acceptance Criteria
Detects Language-Specific Command From Noisy User SMS
Given tenant T1 has English and Spanish keyword sets configured for book, cancel, waitlist, help, and stop, including documented variants and misspellings And the detection engine is enabled When the system receives 200 Spanish test phrases expressing booking intent with noise (misspellings, diacritics, emojis, punctuation, conjugations) Then the engine identifies language = es and command = book for at least 95% of the phrases And routes each to the Spanish booking template with the correct localized checkout link And for 100 negative Spanish phrases without configured keywords, the engine does not trigger any command (false positives <= 2%)
Admin Customizes Per-Language Keywords and Precedence
Given an admin with "Keyword Manager" permission opens Tenant T1 > Languages > Spanish When they add synonyms "reservar", "reserva", "resérvame" to command book with case/diacritic-insensitive matching And set precedence order: stop > help > cancel > book > waitlist And save as draft v3 with change notes "Add variants; adjust precedence" Then draft v3 validates no duplicates or overlapping synonyms across commands within Spanish And a conflict warning is shown if a synonym exists in multiple commands, requiring explicit precedence or removal before saving And the changes are visible only in Sandbox and do not affect production traffic And production remains on v2 until Publish is confirmed And on Publish, v3 becomes active within 60 seconds and is versioned as current
Disambiguates Commands From Free Text
Given a negative test set per language containing phrases where keywords appear as substrings or different meanings (e.g., "book club", "stopwatch", "helpful"), and messages containing URLs/emails When the phrases are processed by the detection engine Then a command is only triggered when a token matches a configured keyword according to locale-aware tokenization (word boundaries) And the false-positive rate on the 200-item negative set per language is <= 2% And if input matches multiple commands (e.g., "cancel stop"), the selected command follows the configured precedence, and the decision is recorded in detection metadata And if no command confidence >= 0.6, the engine returns no-command and routes to the generic responder
Locale-Aware Normalization and Tokenization
Given normalization rules are enabled: case-insensitive, Unicode NFKC, diacritic folding per language, punctuation trimming, support for multi-word keywords When the engine receives inputs with mixed case, accented characters, punctuation variants, multiple spaces, or emojis Then matching occurs after normalization and tokenization using language-specific boundaries And multi-word keywords (e.g., "lista de espera") match across variable whitespace and punctuation And detection metadata stores both raw text and normalized form for troubleshooting
Reserved Compliance Keywords Override Tenant Mappings
Given global reserved keywords and localized equivalents for STOP/UNSTOP/HELP are configured When a message matches any reserved keyword in any supported language Then the reserved compliance flow executes with highest precedence regardless of tenant-defined rules And the admin UI prevents adding reserved keywords to tenant commands And events triggered by reserved keywords are logged with reason = "reserved" and cannot be disabled
Audit Log Captures Keyword Lifecycle and Detection Outcomes
Given an admin creates, updates, deletes, publishes, or reverts keyword mappings for a tenant/language Then an audit entry records actor id, timestamp (UTC), tenant, language, command, before/after values, rationale, version id, and IP address And audit entries are immutable, paginated, searchable by actor/command/language/date, and exportable as CSV And each detection event stores the mapping version id and matched keyword, retrievable for at least 30 days
Sandbox Testing Session Before Publish
Given an admin opens Sandbox for Tenant T1, Spanish draft v3 When they enter test phrases and select a seed phone number Then the system displays predicted language, command, matched keyword, confidence score, and target template/workflow without sending external messages And the sandbox session is shareable via a secure link with expiry and requires tenant authentication And on Promote to Production, v3 is published and a release note is appended to the audit log
Opt-Out and Compliance Localization
"As a business owner, I want opt-out and compliance messaging to work correctly in each language so that we respect regulations and user preferences."
Description

Localize opt-in/opt-out notices, HELP/STOP handling, consent capture, and required disclosures per language and region. Recognize and honor language-specific STOP/HELP equivalents, store compliance events on the contact record with timestamps, and ensure confirmation messages are sent in the same language. Enforce message classification to block promotional sends after opt-out while allowing permitted transactional notices. Provide configurable templates reviewed for TCPA/CTIA/GDPR alignment and exportable compliance logs.

Acceptance Criteria
Localized Opt-In/Opt-Out Notices per Language and Region
Given a contact’s language and region are known or detected from the inbound message or profile When the system sends an opt-in or opt-out notice Then the content is selected from the matching language–region template And the notice includes all region-required disclosures and mandatory tokens populated (brand, HELP/STOP syntax, rates may apply) And if no exact language–region template exists, the system falls back to the admin-configured default template And the message record stores template_id, language, and region used
Language-Specific STOP/HELP Keyword Recognition and Confirmation
Given an inbound message on a supported channel contains a configured STOP equivalent for the detected language (case- and punctuation-insensitive) When the message is processed Then the contact’s marketing subscription status is set to Opted-Out immediately And a confirmation is sent using the stop-confirmation template in the detected language And an opt-out compliance event is recorded with matched_keyword, language, region, and timestamp_utc And future promotional sends are blocked And if the inbound message matches a configured HELP equivalent, a help response is sent in the detected language with support info and opt-out instructions without changing subscription status
Consent Capture Stored with Timestamps on Contact Record
Given consent is provided via web checkout, SMS keyword, or admin import with explicit consent When consent is captured Then the contact record is appended with an immutable consent event storing consent_type, scope (promotional/transactional), language, region, source, evidence (message_id or checkbox text snapshot), actor (user_id/system), ip (if web), and timestamp_utc (ISO 8601) And subsequent updates create new events without overwriting prior entries And the event is queryable in the contact’s compliance timeline
Same-Language Confirmation Messaging
Given a contact opts out using a language-specific keyword When the system sends the opt-out confirmation message Then the confirmation is sent in the same detected language as the inbound message And includes required elements for the region: brand identifier, confirmation of opt-out, and re-opt-in instructions And excludes links if prohibited by regional policy And if language cannot be confidently detected, the system uses the contact’s stored preferred language or admin-configured fallback order and tags the message with detection_confidence
Message Classification Enforcement Post Opt-Out
Given a contact is Opted-Out for promotional messages When a promotional message send is attempted via API or campaign Then the send is blocked and not queued And a compliance_violation event is logged with reason "opted_out" and timestamp_utc And the API/UI returns a descriptive error code and message And transactional messages that meet permitted criteria (e.g., receipts, class confirmations) are allowed And allowed transactional sends post-opt-out include a region-appropriate non-promotional footer
Configurable Compliance Templates with TCPA/CTIA/GDPR Review
Given an admin edits a compliance template for a specific language and region When saving the template Then validation enforces presence of required tokens/placeholders and blocks save on missing items And a compliance linter runs and blocks save on critical TCPA/CTIA/GDPR alignment failures And the template stores language, region, version, reviewer, review_date_utc, and change_log And a preview renders with variable substitution and shows SMS character count/segments before save
Exportable Compliance Logs with Filters
Given an admin requests a compliance log export When filters for date range, language, region, event_type (opt-in, opt-out, help, blocked_send, confirmation_sent), and contact identifiers are applied Then the system generates downloadable CSV and JSON within 60 seconds for up to 250,000 records And each record includes contact_id, redacted_phone, language, region, event_type, matched_keyword, message_id, template_id, classification, outcome, actor, source_ip, and timestamp_utc And contacts deleted under GDPR are represented with pseudonymous ids and redacted fields And an audit entry is recorded for the export with requester, timestamp_utc, and filter summary
Language Analytics and Template A/B Testing
"As a growth lead, I want to compare engagement and conversion by language and template so that I can iterate on the best-performing localized content."
Description

Deliver dashboards and reports showing language distribution, reply rates, link clicks, and conversions to paid bookings by language and template. Support A/B testing of templates within a language with configurable traffic splits and statistical guardrails. Expose metrics via API and CSV export, attribute outcomes to campaigns and classes, and surface recommendations (e.g., promote better-performing variant). Ensure privacy-safe aggregation and role-based access to insights.

Acceptance Criteria
Dashboard: Language Distribution Accuracy
Given a selected date range and account with verified warehouse totals When the Language Distribution dashboard is loaded Then total message counts per language match warehouse aggregates within max(1%, 5 events) And languages are displayed using ISO 639-1 codes with localized names And messages with undetected language are grouped as "Unknown" and comprise ≤0.5% unless source language is absent And dashboard reflects new events within 15 minutes of ingestion
Metrics: Reply, Click, and Conversion by Language and Template
Given filters for date range, campaign, class, language, and template When the metrics view is applied Then reply_rate = unique recipients who replied ÷ delivered recipients And click_rate = unique recipients who clicked tracked template link ÷ delivered recipients And conversion_rate = unique recipients with paid booking attributed ÷ delivered recipients And metrics are shown per language and per template with totals consistent with grouped rows within ±0.1%
A/B Testing: Configurable Splits and Statistical Guardrails
Given a language with two active templates A and B and a configured split of 70/30 and primary metric = conversion_rate When N ≥ 100 deliveries in that language Then allocation over the next 100 deliveries is within ±2% of the target split And test evaluation remains locked until each variant has ≥ 200 deliveries or 14 days elapse, whichever comes first And a winner can only be declared when p-value < 0.05 on the primary metric and observed lift ≥ 5% And changes to split or primary metric are audited and take effect within 5 minutes
Attribution: Campaign and Class Mapping
Given tracked links encode campaign_id, template_id, language_code, and class_id When a recipient completes a paid booking within 7 days of last click Then the booking is attributed to the last clicked campaign/template/language/class within that window And if multiple clicks occur, the most recent qualifying click wins And cancellations within 24 hours of booking are excluded from conversions And reply events are attributed to the campaign/template that delivered the replied-to message
Exports: API and CSV Metrics Delivery
Given a user with analytics:read scope requests GET /v1/analytics/language with start_date, end_date, and group_by=(day|language|template|campaign|class) When the request is valid Then the API returns 200 with rows: date, language_code, template_id, campaign_id, class_id, deliveries, unique_replies, unique_clicks, paid_bookings, reply_rate, click_rate, conversion_rate And CSV export from the dashboard returns the same columns, UTF-8 with header, within 60 seconds for up to 100k rows And API supports pagination via limit and next_cursor; 95th percentile latency ≤ 2s for ≤ 10k rows And API/CSV metrics match the dashboard within ±0.1%
Privacy: Aggregation and Data Minimization
Given any aggregation row where deliveries < 10 When producing dashboard, API, or CSV outputs Then that row is suppressed and overall totals remain accurate within ±1% after suppression And no user identifiers (phone, email, device, IP) appear in outputs And counts are whole numbers; rates are shown to one decimal percentage point And all analytics access and exports are logged with user_id, timestamp, and row count
Access Control: Role-Based Insights
Given roles Owner and Manager When accessing analytics dashboard or API Then access is granted to all account analytics Given role Instructor When accessing analytics Then only classes where the user is assigned instructor_of_record are visible; other classes’ rows are omitted Given roles Staff, Assistant, or unauthenticated When accessing analytics Then a 403 is returned and UI access is blocked And k-anonymity suppression cannot be bypassed by filter narrowing

Keyword Insights

See which keywords and sources drive texts, tap‑throughs, and paid conversions. Find missed-intent terms, measure time-to-book, and A/B test phrasing. Actionable suggestions help refine posters, bios, and SMS prompts to consistently fill more spots.

Requirements

Unified Keyword Attribution
"As an instructor, I want to see which keywords and channels lead to paid bookings so that I can focus my time and budget on what actually drives revenue."
Description

Implement a real-time event ingestion and attribution layer that captures keywords and source metadata from SMS replies, QR codes, booking page query parameters/UTMs, and referring domains. Normalize channels, de-duplicate events, and stitch sessions to contacts via signed links to attribute tap-throughs and paid conversions to first- and last-touch keywords. Persist per-event metadata in a scalable schema and expose a low-latency (<2 minutes) feed to analytics, A/B testing, and suggestions modules.

Acceptance Criteria
Real-time Ingestion of Multichannel Keyword Events
Given an event arrives via SMS reply, QR code scan, booking page query parameters or UTMs, or referring domain When the ingestion service processes the payload Then it extracts and sets keyword, source_channel, source_detail, campaign identifiers where present, timestamps, and a unique event_id Given the event includes a signed link When parsed Then contact_id, link_id, and intended keyword are captured from the token without requiring user login Given a malformed or missing keyword or source When processed Then the event is labeled reason=missing_metadata and is still emitted on the feed for downstream handling Given an ingestion burst of 500 events per second for 5 minutes When monitored Then dropped events are less than 1 percent and parse errors are less than 0.5 percent
Normalized Channel Mapping and De-duplication
Given two or more raw events with identical fingerprint contact_id or link_id, event_type, source_channel, keyword, and observed_at within 5 minutes When processed Then only one normalized event is stored and exposed and duplicates are flagged with dedupe_of equal to the canonical event_id Given a QR scan opens a signed link and a pageview occurs within 10 seconds from the same device When processed Then they are unified into a single tap-through session event Given replayed webhooks or client retries When events are resent with the same idempotency key or event_id Then no additional normalized events are produced Given a synthetic load with 10 percent duplicated events When evaluated Then at least 99 percent of duplicates are correctly deduplicated
Session Stitching via Signed Links
Given a user taps a signed link containing a token with contact_id and link_id When they subsequently book and pay within 7 days using the same browser or matching phone or email Then the tap-through and conversion are stitched to the same contact and session_id Given prior touchpoints exist in the last 30 days When a conversion occurs Then first-touch is the earliest keyword in the lookback window and last-touch is the most recent keyword before conversion and both are persisted on the conversion event Given no signed link but UTM or referrer present When stitching Then fallback uses UTM_term and referrer domain to create a session and assigns first and last touch accordingly Given a test cohort with known journeys When validated Then at least 98 percent of conversions with a prior signed link are correctly stitched and mismatches are logged with cause
Low-Latency Event Feed to Downstream Modules
Given an event is accepted by ingestion When consumed by analytics A or B testing and suggestions modules Then it becomes available within 120 seconds at the 95th percentile and within 300 seconds at the 99th percentile during a 1 hour soak of 500 events per second Given the system experiences a transient outage up to 10 minutes When it recovers Then backlog processing catches up and restores feed freshness to under 2 minutes within 15 minutes and no events are lost Given consumers subscribe via streaming or poll API When requesting the last 5 minutes of events Then completeness is at least 99.5 percent within 10 minutes of event time
Scalable Per-Event Persistence Schema
Given a normalized event is stored When inspecting the record Then required fields exist with correct types and nullability event_id UUID, event_type, keyword, source_channel, source_detail utm_source, utm_medium, utm_campaign, utm_term, referrer_domain, qr_code_id, sms_prompt_id, contact_id, session_id, link_id, observed_at, ingested_at, attribution_flags first_touch, last_touch, monetization fields booking_id, order_id, amount, currency Given common query patterns such as keyword conversions last 30 days grouped by source When executed on a dataset of 100 million events Then 95th percentile query latency is 2 seconds or less and 99th percentile is 5 seconds or less Given new event types or metadata are added When migrations run Then schema versioning preserves backward compatibility and downstream consumers continue functioning without code changes
Attribution Rules and Accuracy Validation
Given a multi-touch journey with keywords A then B then conversion When attribution runs Then first-touch equals A and last-touch equals B and any touchpoints after conversion are ignored Given a conversion without any keyword-bearing touchpoints in the lookback window When attributed Then it is marked unattributed with reason no_keyword and excluded from first and last touch tallies Given SMS replies containing multiple keywords When parsed Then the first recognized keyword in the message is used and ties are resolved by prompt context Given aggregate reports for a controlled test dataset of 10,000 conversions When reconciled Then the sum of conversions by attribution equals total conversions within plus or minus 0.1 percent and differences outside threshold are logged
Security Integrity and PII Handling
Given a signed link token is tampered or expired When processed Then signature verification fails and the event is rejected from attribution while a security event without PII is logged Given ingestion and persistence When logs are emitted Then no PII such as full phone or email is present and only hashed or redacted forms are used and access to raw PII columns is role restricted and audited Given event ingestion over public networks When transmitting Then TLS 1.2 or higher is enforced and HSTS is enabled and unsigned client side events are marked untrusted and excluded from attribution
Keyword Performance Dashboard
"As a studio owner, I want a clear view of which keywords perform best across my classes so that I can prioritize copy and channels to fill more spots."
Description

Deliver a mobile-first dashboard that ranks keywords by tap-throughs, paid conversions, revenue, and conversion rate, with filters for class, date range, channel, and location. Provide trend lines, top movers, per-keyword funnels, and export. Include segmentation for new vs. returning clients and show attribution model (first/last/multi-touch) applied for transparency.

Acceptance Criteria
Keyword Ranking Metrics Display
Given I am an authenticated instructor with keyword data in the selected date range When I open the Keyword Performance Dashboard on a mobile device (viewport ≥ 375×667) Then I see a table listing keywords with columns: Tap-throughs, Paid Conversions, Revenue, Conversion Rate (%) And the list is ranked by Paid Conversions in descending order by default And when I change the sort metric to Tap-throughs, Revenue, or Conversion Rate, the ranking updates accordingly within 800ms And Conversion Rate = Paid Conversions / Tap-throughs (rounded to 2 decimals) And Revenue displays in the account currency and matches backend totals for the filtered scope
Multi-Dimensional Filtering
Given the dashboard is loaded When I apply filters: Class = "Yoga Flow", Date Range = Last 30 Days, Channel = "Instagram", Location = "Studio A" Then all tables, totals, and charts reflect only records matching all selected filters And the number of displayed keywords equals the count of keywords with activity in the filtered scope And clearing any single filter updates results within 800ms and preserves the remaining filters And the active filters are visibly shown as chips/tags and are included in the URL for shareable deep links
Trend Lines and Top Movers
Given a date range of at least 7 days is selected When I view Trends Then each keyword row displays a sparkline for Tap-throughs and Paid Conversions over the selected period And the Top Movers panel shows the top 5 increases and top 5 decreases by Paid Conversions versus the previous equal-length period, with absolute and percentage deltas And selecting a mover highlights and scrolls to the corresponding keyword row And deltas are greyed out when prior-period data is zero and a percentage cannot be computed
Per-Keyword Funnel View
Given I select a keyword from the table When I open its detail drawer Then I see a funnel with steps: Tap-throughs -> Paid Conversions, each showing counts and step conversion rate And the overall conversion rate equals Paid Conversions / Tap-throughs (rounded to 2 decimals) And Revenue for the keyword is displayed alongside the Paid Conversions step And all values respect the currently applied filters and attribution model
Data Export of Current View
Given filters and attribution model are set When I click Export -> CSV Then a CSV downloads within 10 seconds containing one row per keyword in the current view And the file includes columns: Keyword, Tap-throughs, Paid Conversions, Revenue, Conversion Rate, Attribution Model, Class, Channel, Location, Date Range And the sums of Paid Conversions and Revenue in the CSV match the dashboard totals for the current view (±0.5%) And the first row contains metadata with export timestamp (UTC) and applied filters
Segmentation: New vs Returning Clients
Given segmentation controls are visible When I toggle Split by Client Type = On Then each metric column displays New and Returning sub-columns and a Total column And Total = New + Returning for each metric per keyword And Returning is defined as a client with at least one paid booking dated before the selected date range; New otherwise And switching the toggle Off reverts to aggregate metrics within 800ms
Attribution Model Transparency and Consistency
Given an Attribution selector offering First-touch, Last-touch, and Multi-touch When I switch the attribution model Then all per-keyword metrics and totals recompute using the selected model within 1.2s And a visible label and tooltip indicate the active model and its definition And for Multi-touch, fractional credits for each conversion across keywords sum to 1.00 (±0.01) And per-keyword Revenue equals the sum over conversions of (conversion value × assigned fraction) and aggregates to the displayed total
Missed-Intent Discovery
"As an instructor, I want to find the terms prospective clients actually use that I’m not addressing so that I can adjust my messaging to capture more bookings."
Description

Identify high-engagement, low-conversion terms and uncover semantically related or synonymous phrases not present in current bios, posters, and SMS prompts. Cluster inbound texts and referral queries, flag friction patterns (e.g., price, schedule), and quantify potential lift. Surface prioritized opportunities with evidence (sample messages, affected classes) for quick action.

Acceptance Criteria
High-Engagement, Low-Conversion Term Identification
Given a 30-day lookback window and tracked interactions per term >= 50 When a term’s tap-through/reply rate is in the top quartile and its paid conversion rate is in the bottom quartile versus the account baseline Then label the term “Missed-Intent” and display volume, tap-through/reply rate, paid conversion rate, and source breakdown per term
Semantic Gap Suggestions for Absent Phrases
Given inbound messages and referral queries from the last 30 days and the current bios/posters/SMS text library When the system finds phrases with cosine similarity >= 0.80 to missed-intent terms that do not appear verbatim or as stemmed variants in current assets Then surface a ranked list of at least 10 candidate phrases with similarity score, suggested placements (bio/poster/SMS), and 3–5 sample user messages per phrase
Inbound Text and Referral Query Clustering
Given a corpus of >= 500 inbound messages/queries within the lookback window When clustering is executed Then at least 90% of messages are assigned to a cluster, average silhouette score >= 0.50, and each cluster shows top n-grams, intent label, volume, paid conversion rate, median time-to-book, and affected classes
Friction Pattern Detection (Price, Schedule, Location)
Given clustered intents and outcome metrics When price, schedule, or location-related clusters have conversion rates >= 20% lower than the overall median and frequency share >= 30% above baseline Then flag each as a friction pattern and display message count, conversion delta, sample messages (3–5), and recommended content additions
Potential Lift Estimation and Opportunity Prioritization
Given a set of missed-intent terms and friction patterns with baseline exposure and conversion When expected conversion uplift is estimated per item using historical experiments or cohort analogs Then produce a prioritized list sorted by expected incremental paid bookings over the next 30 days, showing for each item: estimated uplift range, confidence (50–90%), required content change, affected classes count, and a RICE-like score
Evidence Surfacing and One-Click Quick Action
Given a prioritized opportunity When the user opens its detail view Then show the exact assets missing the phrase, 3–5 timestamped redacted sample messages and sources, and provide a one-click action to insert the suggested phrase into selected bios/posters/SMS with preview, publish, and audit log entry
Time-to-Book by Term and Source
Given tracked interactions and bookings in the lookback window When calculating time-to-book segmented by term and source Then display median and 90th percentile time-to-book per term/source and highlight any missed-intent term with median time-to-book >= 25% above baseline as additional friction evidence
Time-to-Book Analytics
"As a marketer, I want to understand how long different keywords take to convert so that I can plan reminders and choose phrases that shorten the path to booking."
Description

Compute and visualize time from first interaction to paid booking by keyword, source, and class, including median, percentile distribution, and drop-off points. Enable cohort comparisons (e.g., weekday vs. weekend classes) and highlight keywords that accelerate or delay bookings to inform reminders and copy changes.

Acceptance Criteria
Compute TTB Metrics by Keyword, Source, and Class
Given tracked events include first interaction and paid booking timestamps per user-session When analytics are computed for a selected date range Then time-to-book is calculated in minutes for each booking And metrics are aggregated by keyword, source, and class with p50 (median), p75, p90, count, and conversion rate And groups with sample size < 20 suppress percentile display and are labeled "Insufficient sample" And metrics recompute within 15 minutes of new booking ingestion And repeated runs with identical inputs vary by no more than ±1% for aggregate metrics
Visualize Percentiles and Drop-off Points
Given a selected dimension group (e.g., a specific keyword or class) When the Time-to-Book chart loads Then a distribution chart (histogram) and cumulative curve render with p50/p75/p90 markers And tooltips display bucket start-end, count, and share of total And the survival curve highlights the top 3 steepest declines (drop-off points) with time labels And chart renders in under 2 seconds for datasets up to 50k events
Cohort Comparison: Weekday vs. Weekend Classes
Given the user selects two cohorts (Weekday vs Weekend) for the same date range When the comparison view is opened Then side-by-side metrics show p50, p75, p90, conversion rate, and sample size for each cohort And deltas are calculated and color-coded (green for faster, red for slower) And statistical significance is indicated using Mann–Whitney U at alpha=0.05 when each cohort has ≥ 50 samples; otherwise labeled "Insufficient sample" And the user can switch cohort dimension to instructor, class category, or source And the comparison can be exported as PNG (charts) and CSV (metrics)
Highlight Accelerating vs. Delaying Keywords
Given a baseline median time-to-book is computed for the selected filters When evaluating keyword performance Then keywords with median TTB at least 20% faster or slower than baseline and sample size ≥ 50 are flagged And a ranked list shows the top 10 accelerators and top 10 delayers with delta median (minutes), sample size, and top sources And clicking a keyword opens a detail view filtered to that keyword with trends and cohort comparisons And flagged keywords are included in CSV exports
A/B Test Impact on Time-to-Book
Given tracking parameters include a variant tag (A or B) on first interaction events And both variants have ≥ 50 bookings in the selected date range When the A/B view is opened Then the dashboard displays p50, p75, p90, conversion rate, and sample size per variant, plus deltas And significance is tested via Mann–Whitney U (alpha=0.05) for p50 difference and a winner is recommended if median TTB improves by ≥ 10% with p<0.05 And the user can align analysis to each variant’s launch date to normalize exposure windows And results auto-update within 15 minutes of new data
Filtering, Date Range, and Timezone Controls
Given no filters are set When the dashboard loads Then the default date range is Last 30 Days using the account default timezone And users can filter by date range, keyword (search/autocomplete), source, class, and instructor, with visible removable filter chips And all charts and metrics reflect applied filters within 2 seconds for datasets up to 50k events And saved views preserve filters, cohort selections, and chart types and can be reloaded by the creator And changing timezone recalculates bucket boundaries and recomputes all metrics accordingly
A/B Phrase Testing
"As a studio owner, I want to A/B test copy across my channels so that I can systematically increase conversions with evidence-backed changes."
Description

Provide a variant manager to A/B test phrasing across booking page headlines, QR poster captions, bios, and SMS prompts. Randomize exposure, prevent user cross-contamination, calculate required sample sizes, and report statistical significance and effect size by keyword and channel. Integrate with existing editors and templates for one-click rollout of winners.

Acceptance Criteria
Create and Assign Variants Across Surfaces
Given I have admin access and Keyword Insights is enabled When I create a new experiment and add surfaces Booking Page Headline, QR Poster Caption, Bio, and SMS Prompt And I add Variant A (Control) and Variant B with specified text for each surface Then the experiment is saved as Draft with both variants visible in the Variant Manager And each selected surface's editor shows a linked experiment badge and preview toggle for A/B And no live content changes occur until I click Start Experiment
Randomized Exposure with Allocation Control
Given an experiment is Started with a 50/50 allocation and eligibility channels Web, QR, and SMS When 1,000 eligible first-time exposures occur per channel Then the observed allocation per channel is within ±2 percentage points of the target And each unique user receives exactly one variant assignment for that experiment And the assignment persists for 30 days or until the experiment ends, whichever comes first
Cross-Contamination Prevention and Consistent Assignment
Given a user is assigned Variant B for Experiment X on any participating surface When the same user later interacts via any other participating surface or channel of Experiment X Then they are served Variant B consistently And if the user is authenticated or identified by phone/email from booking/SMS, their assignment is restored across devices And clearing cookies does not change the assignment once the user re-identifies; otherwise a new anonymous assignment is created
Sample Size and Power Calculation
Given I set baseline paid conversion and minimum detectable effect (relative) And I select 80% power and 95% confidence When I save the configuration Then the system displays required sample size per variant per channel and overall And a Progress indicator shows eligible exposures and conversions versus required per channel And declaring a winner is disabled until minimum required sample is met for the selected channel(s)
Significance and Effect Size by Keyword and Channel
Given minimum sample is met for Channel C and Keyword K When I open Results for Experiment X Then I can view, for each variant, conversion rate, absolute lift, relative lift, and 95% confidence interval for (C,K) And the system reports a p-value using a two-proportion test and labels the (C,K) cell Significant if p < 0.05, else Inconclusive And I can download a CSV of metrics segmented by keyword and channel
One-Click Rollout and Rollback of Winner
Given a variant is marked as the winning variant for at least one channel When I click Roll Out Winner to all linked surfaces Then the winning text replaces control content across the selected editors/templates within 5 minutes And the experiment status changes to Ended and new exposures stop And I can click Roll Back to restore previous content within 5 minutes And a change log records user, time, affected surfaces, and before/after text
Time-to-Book Tracking per Variant
Given exposures and bookings occur while the experiment is running When I open Time-to-Book in Results Then I see median and 75th percentile time-to-book per variant and per channel using a 7-day attribution window And bookings occurring after 7 days from first exposure are excluded from this metric And the difference in medians between variants is displayed with a 95% confidence interval
Actionable Copy Suggestions
"As an instructor, I want practical suggestions I can apply quickly so that I can improve booking rates without spending hours in analytics."
Description

Generate prioritized, context-aware suggestions such as adding/removing specific keywords, rephrasing headlines, or updating SMS prompts, each with an estimated impact based on historical performance. Support one-tap apply to booking pages and templates, track applied changes, and measure realized lift for continuous learning.

Acceptance Criteria
Prioritized Suggestions with Estimated Impact
Given the account has at least 30 bookings or 200 booking‑page views in the last 90 days, When the user opens Keyword Insights > Actionable Suggestions for a selected class or template, Then the system displays 5–15 unique, prioritized suggestions ordered by predicted bookings lift, And each suggestion includes: action type (add/remove keyword, rephrase headline, update SMS prompt), target asset(s), rationale citing contributing keywords/sources, estimated impact on the selected KPI (bookings, tap‑through rate, or time‑to‑book) as baseline and predicted delta (absolute and %), and a confidence score (0.0–1.0), And suggestions respect the class’s language/locale and brand constraints (do‑not‑use terms, tone), And the list loads within 2 seconds at p95.
One‑Tap Apply with Preview and Rollback
Given a suggestion card with targetable assets is visible, When the user taps Apply, Then a preview shows side‑by‑side diffs for all affected assets with character counts and validation, And the user can select destinations (booking page headline/description, SMS template, bio) and confirm in one action, And the change is applied within 2 seconds at p95 and returns a success state, And the system creates a new version of each asset with version ID linked to the suggestion, And the user can rollback in one click from History to the prior version with content fully restored.
Change Tracking and Audit Log
Given any suggestion is applied or rolled back, When the event is saved, Then an immutable audit record is created containing: timestamp (UTC), actor, asset ID(s), channel, suggestion ID, old content hash and snippet, new content hash and snippet, estimated impact at apply time, and environment, And the record is visible in History within 10 seconds, And History supports filter by actor, channel, asset, date range, and suggestion ID and can export CSV for the selected range.
A/B Test Apply and Allocation
Given a suggestion supports experimentation, When the user selects Apply as A/B Test, Then the user can set traffic allocation (e.g., 50/50, 70/30) and target KPI, And the system displays minimum sample size and estimated test duration for 80% power at alpha 0.05 based on historical rates, And variants are created with labels (Control, Variant) and version IDs, And metrics for each variant are tracked in real time and visible in the experiment view, And the system can auto‑stop the test when significance is reached or a max duration elapses, and applies the winner on confirm.
Low‑Data Fallback and Confidence Signaling
Given the account does not meet data thresholds for modeling, When the user requests suggestions, Then the system generates up to 10 best‑practice suggestions using industry priors and similar cohorts and labels confidence as Low (≤0.4), And each suggestion clearly indicates “Limited data” and excludes a numeric uplift prediction, And one‑tap apply remains available with the same preview and tracking.
Realized Lift Measurement and Attribution
Given a suggestion is applied or an experiment completes, When the attribution window closes (default 14 days, configurable 7–30), Then the system computes realized lift for the target KPI versus the baseline (prior 28‑day period or control variant) showing absolute and % change with 95% CI, And the realized lift, p‑value, and sample sizes are displayed on the suggestion card and in History, And the suggestion’s model performance record is updated to improve future estimations.
Missed‑Intent Keywords and Compliance Constraints
Given search/referral data contains queries that drive visits with low conversion, When generating suggestions, Then at least 40% of suggestions include adding/removing specific missed‑intent keywords mapped to those queries, And all SMS suggestions are ≤160 GSM‑7 characters (or ≤70 UCS‑2) and pass link tracking token insertion, And headlines are ≤60 characters and meet readability grade 6–8 (Flesch‑Kincaid), And suggestions exclude blocked terms and duplicates within the session.
Privacy & Consent Compliance
"As a business owner, I want insights that respect user privacy and regulations so that I can confidently use the data and protect my brand."
Description

Enforce privacy-first tracking with explicit consent banners on booking pages, SMS/email opt-out controls, IP anonymization, and data minimization. Provide configurable retention windows, audit logs, and a privacy-safe attribution mode that aggregates metrics without storing PII. Document GDPR/CCPA compliance and surface consent status within analytics.

Acceptance Criteria
Consent Banner on Booking Pages
Given a first-time visitor loads any ClassNest booking page When the page renders Then no tracking scripts, cookies, or localStorage are set or read until the visitor explicitly opts in Given the consent banner is displayed When the visitor reviews options Then Accept All, Reject All, and Manage Settings are equally prominent, functional, and keyboard accessible Given the visitor selects any option When the choice is submitted Then the system stores a consent record with user-agent, timestamp (UTC), banner version, locale, and purpose selections, and applies it within 200 ms to the current session Given a returning visitor with a stored consent decision When they revisit Then the stored decision is honored and the banner is suppressed unless the banner version or purposes changed, in which case re-consent is requested Given a visitor clicks "Change privacy settings" When the control is opened Then they can modify consent and the new decision is enforced immediately and logged
SMS and Email Opt-Out Controls
Given a recipient replies STOP to an SMS sent by ClassNest When the message is received Then the phone number is suppressed from all future SMS within 60 seconds and a confirmation SMS is sent once Given a recipient clicks an email unsubscribe link When the link is loaded Then the email is unsubscribed in under 60 seconds without requiring additional steps, and a confirmation screen is shown Given a recipient has opted out When a new campaign or transactional message is queued Then the system prevents sending to suppressed contacts and logs the suppression reason Given an admin views a contact profile When consent status is displayed Then SMS and email opt-in/opt-out timestamps and sources are visible and exportable Given a recipient attempts to resubscribe When they opt back in Then double opt-in verification is required for SMS and the updated consent is logged
IP Anonymization and Data Minimization
Given any tracking or analytics event is collected When the client IP is processed Then IPv4 is anonymized by zeroing the last octet and IPv6 by truncating to /48 before storage or enrichment Given an event payload is ingested When PII fields (name, email, phone, exact IP) are present Then they are dropped or irreversibly hashed with a rotating secret and are not stored in raw or derived datasets used by Keyword Insights Given data schemas for Keyword Insights When fields are stored Then only the minimum necessary fields (event type, timestamp, campaign/source, keyword, device type, coarse geo at city level, consent state) are retained Given a data export is generated When the file is built Then it contains no PII and passes an automated PII scan with zero violations
Configurable Data Retention Windows
Given a workspace admin opens Privacy Settings When they configure retention Then they can set retention windows (30–730 days) separately for event data, aggregated reports, consent records, and audit logs with clear defaults Given retention settings are saved When the daily purge job runs Then data older than the configured window is permanently deleted and deletion counts are logged by dataset Given retention is reduced When the new window is shorter than the previous one Then an immediate purge job runs and a warning is shown summarizing data to be removed Given a data deletion is executed When deletion completes Then affected aggregates are recomputed or marked stale within 24 hours
Compliance Documentation and Auditability
Given an admin opens Compliance Documentation When the page loads Then the latest GDPR/CCPA overview, subprocessor list, DPA, and privacy policy are accessible, versioned, and downloadable as PDF Given a privacy-relevant action occurs (consent change, retention change, attribution-mode toggle, export created) When the action is committed Then an immutable audit log entry is recorded with actor, timestamp (UTC), action, before/after values, and scope Given a user with Audit Viewer role requests an export When the export is generated Then a signed CSV/JSON of audit logs for a selected date range is produced within 2 minutes and includes tamper-evidence via a hash chain or signature Given audit logs are retained When retention elapses Then logs are deleted per retention settings and the deletion is itself logged
Privacy-Safe Attribution Mode (Aggregated Analytics)
Given Privacy-Safe Attribution Mode is enabled When reports are generated Then only aggregated metrics (impressions, clicks, tap-throughs, bookings, time-to-book percentiles, A/B test uplifts) are computed and displayed without user-level rows Given a segment or keyword has fewer than k=10 contributing unique consented users When a report is requested Then the metric is suppressed and labeled "insufficient data" instead of being shown Given event-level data arrives When aggregation completes Then raw linkable identifiers are discarded within 24 hours and are not queryable by users or staff Given exports are requested while Privacy-Safe mode is enabled When files are generated Then exports contain only aggregated tables with no PII or stable identifiers
Consent Status Visibility in Analytics
Given a user opens Keyword Insights When charts and tables render Then results default to include only consented data and the consent scope is clearly indicated in the UI Given the user toggles the consent filter When set to "Include non-consented (privacy-safe only)" Then only aggregates that meet privacy thresholds are included and all others are suppressed Given a user downloads a report When the export completes Then the applied consent scope is embedded in file metadata and headers, and the data respects that scope Given a metric is impacted by consent filtering When a tooltip or footnote is viewed Then the UI explains the inclusion criteria and any suppression applied

Smart Deposit Rules

Set deposits as a flat amount or percentage, with class-level defaults and per-session overrides. Auto-adjust by demand signals (peak times, limited capacity) and past no‑show rates to balance conversion with protection—no more one-size-fits-all settings.

Requirements

Deposit Type & Calculation Engine
"As an instructor, I want to set deposits as a flat amount or percentage so that deposits match my pricing strategy across different classes."
Description

Enable deposits configurable as either a flat currency amount or a percentage of the booking subtotal, with currency-aware rounding, min/max caps, and toggles to include/exclude taxes, fees, and add-ons. Calculate deposits in real time across pricing, checkout, reschedule, and waitlist offer flows; recompute on price updates prior to payment authorization; and ensure compatibility with coupons/discounts and multi-attendee bookings. Provide guardrails to prevent over-100% deposits, negative values, and conflicts with free/promotional bookings. Expose calculation via API and log inputs/outputs for auditability, delivering accurate, consistent deposits that fit varied pricing models within ClassNest.

Acceptance Criteria
Flat Amount Deposit with Currency Rounding and Caps
Given a class-level default flat deposit and a per-session flat deposit override exists When a booking is initiated for that session Then the per-session override value is used instead of the class default Given a configured flat deposit amount in a supported ISO 4217 currency When the deposit is computed Then it is rounded to the currency’s minor unit (e.g., 2 decimals for USD, 0 for JPY) and never exceeds the amount due Given include/exclude toggles for taxes, fees, and add-ons When the flat deposit is computed Then toggles do not alter the flat amount calculation but caps against the final amount due still apply Given min and/or max deposit caps are configured When the flat deposit is computed Then the final deposit is clamped within [min, max] and cannot be negative
Percentage Deposit with Include/Exclude Components and Caps
Given a percentage-based deposit configuration and include/exclude toggles for taxes, fees, and add-ons When the deposit base is determined Then the base equals the sum of only those components marked as included after discounts are applied Given the deposit percentage and a computed base amount When the percentage is applied and currency rounding is performed Then the deposit equals round(base × percent) to the currency’s minor unit Given min and max caps are configured When the percentage result is outside [min, max] Then the final deposit is clamped within [min, max] and never exceeds the current amount due Given an example: subtotal 100, taxes 10, fees 5, add-ons 20, percent 30%, include taxes=true, fees=false, add-ons=true When deposit is computed Then base=130 and deposit=39.00 in USD (2 decimals)
Real-time Recalculation Across Pricing, Checkout, Reschedule, and Waitlist Offer
Given the user changes any price-impacting input (quantity, attendees, add-ons, coupon, session selection) prior to payment authorization When the change is made in pricing, checkout, reschedule, or waitlist offer flows Then the deposit is recomputed immediately and the displayed deposit matches the backend/API result Given a reschedule to a session with a different price or settings When the reschedule is confirmed prior to authorization Then the deposit is recalculated using the new session’s rules and displayed before the user authorizes payment Given a waitlist auto-offer is generated When the offer is viewed or accepted before authorization Then the deposit shown in the offer details is computed using current pricing and rules and is recomputed if any price input changes before authorization
Coupons, Discounts, and Free/Promotional Bookings Compatibility
Given a coupon or discount (fixed or percentage) is applied When the deposit base is computed Then discounts reduce the base first per include/exclude toggles, and the deposit is then calculated on the discounted base Given a coupon reduces the amount due to zero or below When the deposit is computed Then the deposit is set to 0 and no payment authorization for a deposit is attempted Given a user applies or removes a coupon prior to payment authorization When the change occurs Then the deposit value is recomputed and reflected consistently in UI, API, and any payment intent preview Given the deposit is authorized and later a coupon is added post-authorization When attempting to reuse or alter the existing authorization Then the system prevents inconsistent states by requiring recomputation and re-authorization or cancels the prior authorization per payment flow policy
Multi-Attendee Booking Calculation
Given a booking with multiple attendees and per-attendee prices and add-ons When the deposit base is computed Then the base is the booking-level total of included components across all attendees after discounts Given a percentage-based deposit for multiple attendees When the deposit is computed Then rounding is applied once on the total deposit, not per attendee, and the result does not exceed the booking amount due Given a flat deposit for multiple attendees When the deposit is computed Then a single booking-level flat amount is used and is clamped by min/max and the total amount due Given attendee count or selections change prior to authorization When the change occurs Then the deposit recomputes accordingly and remains consistent across UI and API
Guardrails for Over-100%, Negative Values, and Edge Cases
Given a configuration is saved with percentage > 100, negative percentage, negative flat amount, or min cap > max cap When validation runs Then the configuration is rejected with specific error codes and messages Given any computed deposit exceeds the current amount due When finalizing the deposit Then the deposit is capped at the amount due and never becomes negative Given a zero-price session or a promotional booking resulting in zero due When computing the deposit Then the deposit is 0 and no authorization is created Given a currency with a non-standard exponent (e.g., JPY=0, TND=3) When rounding the deposit Then rounding uses the ISO currency exponent and produces no fractional minor units beyond the exponent
API Calculation Exposure and Audit Logging
Given a client calls the deposit calculation API with required inputs When the request is valid Then the response includes: deposit_amount, currency, calculation_method (flat|percent), base_components breakdown (subtotal, taxes, fees, add-ons), include/exclude flags, discounts applied, caps applied, rounding exponent, attendees_count, calculation_version, and trace_id Given invalid or inconsistent inputs (e.g., negative values, unknown currency, min>max) When the API is called Then a 4xx response is returned with structured error codes per field Given any deposit calculation is performed (UI or API) When the calculation completes Then inputs and outputs are persisted to an audit log with tenant, class/session IDs, user or device identifier (if available), timestamp, and trace_id, with PII masked Given a trace_id from a booking When support queries the audit log via internal tools or API Then the exact calculation inputs/outputs are retrievable for that trace_id
Hierarchical Defaults & Per-Session Overrides
"As a studio owner, I want class-level defaults with per-session overrides so that I can configure once and fine-tune exceptions without repetitive work."
Description

Provide a rules hierarchy with inheritance: account default → class template → class → session override. The most specific rule wins, with clear preview of the effective deposit for any session. Support bulk apply to selected future sessions, effective date ranges, draft/publish states, and rollback to prior versions. Offer role-based permissions for who can create, edit, override, and publish rules. Expose CRUD APIs and change events so external tools remain in sync. This reduces repetitive configuration while enabling precise control for special cases.

Acceptance Criteria
Effective Deposit Preview Resolves Hierarchy
Given an account default deposit of 10%, a class template default of $5 flat, a class default of 15%, and a session override of $8 flat exist When the instructor previews the effective deposit for that session Then the preview shows $8.00 flat and indicates Source: Session Override Given a session has no session override but the class has a class default of 15% When previewing the session Then the preview shows 15% and indicates Source: Class Given a class has no class default but its class template has a $5 flat default When previewing any session of that class Then the preview shows $5.00 flat and indicates Source: Class Template Given neither session, class, nor class template define a rule but the account has a 10% default When previewing a session Then the preview shows 10% and indicates Source: Account Default Then currency values are formatted in the account currency with two decimals; percent values show up to one decimal (e.g., 12.5%)
Per-Session Override Draft and Publish
Given a user with permissions deposit_rules.override.session and deposit_rules.publish creates a session override as Draft with $12 flat When they preview the session Then the effective deposit continues to use the next-most-specific published rule and the UI labels the override as Draft When the same user publishes the draft Then the effective deposit switches to $12.00 flat for new bookings created after the publish timestamp And bookings created before publish retain their original deposit amount Given a user without deposit_rules.publish tries to publish a draft override When they attempt to publish Then the action is blocked with HTTP 403 Forbidden and an inline error "Insufficient permissions"
Bulk Apply Deposit Rules to Future Sessions
Given the user selects 15 future sessions across 3 classes and specifies a deposit rule of 20% When they run Bulk Apply Then 15 targeted sessions are updated with a published session override of 20% and a result summary shows 15 Succeeded, 0 Failed Given some targeted sessions already have session overrides and "Overwrite existing overrides" is unchecked When applying the bulk update Then existing overrides are preserved and reported as Skipped - Existing Override Given a targeted session is in the past relative to now When applying the bulk update Then it is excluded and reported as Skipped - Past Session Given the user repeats the same bulk apply with an identical payload When executing the operation Then the operation is idempotent and the result summary shows 0 Changed, N Up-to-date
Date-Ranged Rules Applied at Booking Time with Deterministic Conflicts
Given a published class-level rule of 15% with effective dates 2025-10-01 to 2025-10-31 When a booking is created on 2025-10-15 for a session on 2025-11-10 Then the deposit applied is 15% because booking time falls within the rule's effective range Given the same rule When a booking is created on 2025-11-02 Then the class-level rule is not applied and the next-most-specific published rule in the hierarchy determines the deposit Given two published rules exist at the same specificity (Class) with overlapping effective date ranges When previewing or computing the effective deposit Then the rule with the most recent publish timestamp wins and the other is flagged as Overlapped in the rule list Given a rule has no end date When computing effective deposit Then it is treated as open-ended until unpublished or superseded
Versioning and Rollback of Deposit Rules
Given a class default rule has versions v1 (10%), v2 (15%), and v3 (12% Published) When the user rolls back from v3 to v1 Then a new version v4 is created identical to v1, marked Published, and becomes the effective rule And the session preview immediately reflects the rolled-back effective value And an immutable audit log entry records user, timestamp, from_version, to_version, and scope And a change event deposit_rule.rolled_back is emitted with version metadata
Permission Enforcement for Create/Edit/Override/Publish
Given fine-grained permissions exist (deposit_rules.create, deposit_rules.edit, deposit_rules.override.session, deposit_rules.publish, deposit_rules.rollback) When a user lacking a required permission attempts the corresponding action Then the API responds HTTP 403 Forbidden and the UI hides or disables the control Given a user has deposit_rules.override.session but not deposit_rules.publish When they create a session override Then it saves as Draft and cannot be published by that user Given a user has deposit_rules.publish at Class scope only When they attempt to publish an Account Default rule Then the action is forbidden and logged with user_id, action, scope, decision, and reason
CRUD APIs and Webhook Change Events for External Sync
Given REST endpoints exist: POST /v1/deposit-rules, GET /v1/deposit-rules/{id}, PATCH /v1/deposit-rules/{id}, DELETE /v1/deposit-rules/{id}, GET /v1/sessions/{id}/effective-deposit When creating, updating (including publishing), deleting rules across scopes (Account, Class Template, Class, Session) Then the API returns 2xx on success, validates scope and date ranges, and responses include id, scope, specificity, status (draft|published), value_type (percent|flat), value, currency, effective_start, effective_end, version Given a webhook subscription to deposit_rule.created, deposit_rule.updated, deposit_rule.published, deposit_rule.deleted, deposit_rule.rolled_back When each corresponding action occurs Then a webhook is delivered at least once with a signed payload containing event_id, event_type, occurred_at (UTC), resource_id, scope, version, and effective snapshot Given webhooks may be delivered more than once When duplicate deliveries occur Then event_id is unique and can be used by consumers for idempotency Given a consumer missed prior events When they GET /v1/sessions/{id}/effective-deposit Then the response reflects the current effective deposit and source, ensuring eventual consistency
Demand-Sensitive Auto-Adjustment
"As an instructor, I want deposits to increase for high-demand sessions so that I protect revenue while maintaining conversion."
Description

Automatically adjust the base deposit using demand signals such as remaining capacity thresholds, booking velocity over recent windows, peak time schedules, and active waitlist length. Provide configurable uplift ranges (e.g., +0–20%), floors/ceilings, and cooldown windows to prevent oscillation. Include simulation/preview tools to estimate impact before enabling, and emit events when adjustments change for analytics and notifications. Apply the effective deposit consistently across checkout and smart waitlist auto-offers, with guardrails to never exceed total price or drop below minimums and to time-box adjustments as the session start approaches.

Acceptance Criteria
Demand Signal Triggered Uplifts
Scenario: Remaining capacity threshold triggers uplift Given a class with total capacity 20 and remaining seats 3 (15%) And a rule "uplift +10% when remaining <= 20%" And a base deposit of $25.00 And no other demand rules are active When a customer opens the booking page or an API quotes the deposit Then the effective deposit is $27.50 and is flagged as demand-adjusted Scenario: Booking velocity triggers uplift Given 12 bookings occurred in the last 15 minutes for a session And a rule "uplift +8% when bookings in last 15 minutes >= 10" And a base deposit of $20.00 And no other demand rules are active When the system computes the deposit Then the effective deposit is $21.60 Scenario: Peak time schedule triggers uplift Given a session scheduled within a configured peak window (Weekdays 17:00–20:00) And a rule "uplift +5% during peak windows" And a base deposit of $30.00 And no other demand rules are active When the deposit is computed at checkout Then the effective deposit is $31.50 Scenario: Active waitlist length triggers uplift Given an active waitlist length of 8 for a session And a rule "uplift +12% when waitlist length >= 5" And a base deposit of $25.00 And no other demand rules are active When the deposit is computed Then the effective deposit is $28.00
Uplift Ranges, Floors, and Ceilings
Scenario: Cap uplift at configured maximum range Given a base deposit of $40.00 And a capacity-triggered uplift that evaluates to +30% And the configured uplift range maximum is +20% When the deposit is computed Then the applied uplift is capped at +20% And the effective deposit is $48.00 Scenario: Do not exceed total price Given a total price of $45.00 And a computed effective deposit of $48.00 before guardrails When guardrails are applied Then the effective deposit is capped at $45.00 Scenario: Respect minimum deposit floor Given a configured minimum deposit of $10.00 And a base deposit of $9.00 When the deposit is computed Then the effective deposit is raised to $10.00
Cooldown and Stability
Scenario: Maintain deposit during cooldown despite signal reversal Given a base deposit of $20.00 And a capacity-triggered uplift of +10% becomes active at 10:00, making the effective deposit $22.00 And a configured cooldown window of 30 minutes When remaining capacity rises above the threshold at 10:10 Then the effective deposit remains $22.00 until 10:30 And all quotes and checkouts between 10:10 and 10:30 return $22.00 Scenario: Multiple signal flips within cooldown do not change deposit Given the same session under cooldown until 10:30 When signals would otherwise increase or decrease the deposit between 10:10 and 10:30 Then the effective deposit remains unchanged at $22.00
Time-Boxing Near Session Start
Scenario: Freeze adjustments within configured pre-start window Given a session start time of 18:00 And a configured freeze window of 60 minutes And a base deposit of $20.00 And at 16:45 the effective deposit is $22.00 due to demand When the time reaches 17:00 (inside freeze window) Then the effective deposit becomes $20.00 and remains unchanged thereafter And no further demand-driven increases are applied within the freeze window And the effective deposit never drops below the configured minimum
Consistency Across Checkout and Smart Waitlist Auto-Offers
Scenario: Single source of truth for effective deposit Given a session with an active waitlist-triggered uplift producing an effective deposit of $28.00 at 14:01 When a shopper opens checkout at 14:01 And a waitlisted customer receives an auto-offer at 14:01 Then both experiences present an identical effective deposit of $28.00 And the charged deposit amount matches the displayed amount in both flows And the value remains consistent across page refreshes and message retries within the same cooldown window
Simulation/Preview Impact Estimator
Scenario: Preview returns metrics and applies no live changes Given an admin configures demand rules and selects a 30-day lookback When they click "Preview impact" Then results return within 3 seconds And the preview displays: number of sessions affected, percent of historical bookings affected, average uplift %, projected incremental deposit revenue, and per-signal contribution breakdown And the preview calculations honor configured floors, ceilings, cooldowns, and freeze windows And no effective deposits in production are changed until the admin enables auto-adjustment and saves
Adjustment Change Event Emission
Scenario: Emit analytics/notification event on effective deposit change Given a session S with effective deposit changing from $22.00 to $24.00 at 11:05 due to waitlist-length change When the change is persisted Then an event named "deposit_adjustment.changed" is emitted within 5 seconds And the payload includes: session_id, class_id, previous_deposit, new_deposit, applied_uplift_percent, trigger_sources, occurred_at (UTC), and a correlation_id And the event is delivered to both the analytics stream and notifications service And duplicate events for the same change are suppressed using correlation_id And an audit log entry is stored with the same payload
No-Show Risk-Based Adjustment
"As a studio owner, I want deposits to adjust based on historical no-shows so that I reduce revenue loss on risky sessions without over-penalizing low-risk ones."
Description

Incorporate historical no-show rates to adjust deposits by class, time of day, and day of week, with recency weighting and data sufficiency thresholds. Optionally factor simple customer signals (e.g., first-time vs returning) without storing sensitive categories; default to aggregate class/time signals when unavailable. Provide configurable uplift/discount bands, caps, and grace periods for new classes with limited data. Show admin-side explanations of adjustments (e.g., “+10% due to 18% no-show rate last 60 days”) and allow per-class opt-outs. Emit metrics and events to monitor impact and fairness.

Acceptance Criteria
Recency-Weighted No-Show Adjustment by Class/Time Slot
Given a booking for a class occurring in a specific time-of-day bucket and day-of-week, and configured recency weights, data window, and sufficiency threshold When the deposit is calculated Then the system computes a recency-weighted no-show rate for the class/time/day within the configured window using the configured weighting scheme And maps that rate to the configured uplift/discount band And applies the adjustment to the base deposit amount subject to configured min/max caps And sets the customer-facing deposit to the computed value And records the inputs and outputs of the decision for auditability
Data Sufficiency Fallback and Grace Period
Given the data for the class/time/day segment is below the configured sufficiency threshold When the deposit is calculated Then the system applies the configured fallback hierarchy that defaults to aggregate class/time signals until a segment meets sufficiency And if no segment meets sufficiency, the system applies the configured new-class grace-period rule (e.g., no adjustment or reduced cap) for the configured duration or count And the decision record includes which fallback or grace rule was used
Customer Signal Factor (First-Time vs Returning)
Given customer signal adjustment is enabled and the customer is identified as first-time or returning without using sensitive categories When the deposit is calculated Then the system applies the configured uplift/discount for the customer signal in addition to the class/time risk adjustment And the combined effect remains within the global min/max deposit caps And if the customer signal is unavailable, the calculation proceeds using only class/time risk signals And no sensitive categories are stored or processed in the decision or emitted events
Bands, Caps, and Calculation Order Enforcement
Given configured risk bands (with explicit boundary rules), global min/max caps, and rounding rules When the system computes the deposit Then it applies adjustments in this order: base deposit → class/time risk band → customer signal adjustment → apply caps → apply rounding And boundary values map to bands per the configured inclusive/exclusive settings And the final deposit is not negative and does not exceed the session price And rounding conforms to the configured increment (e.g., nearest $1) after caps are applied
Admin-Side Explanation of Adjustment
Given an admin views a deposit explanation for a class/session preview or a specific booking When risk-based adjustments are applied Then the UI displays a human-readable breakdown including base deposit, weighted no-show rate with window and sample size, band selected, customer signal effect (if any), caps applied, fallback/grace usage (if any), and final deposit And the explanation includes a concise reason string (e.g., "+10% due to 18% no-show rate last 60 days (n=45)") And when a class is opted out, the UI clearly states that risk-based adjustments are disabled for that class
Per-Class Opt-Out Control
Given a class has a risk-based adjustment opt-out toggle When an admin enables the opt-out Then subsequent deposit evaluations for that class do not apply any risk-based adjustments And the opt-out state persists and is reflected in the admin UI And an audit event is recorded with actor, timestamp, class ID, and new state And disabling the opt-out re-enables adjustments on subsequent evaluations
Metrics and Events for Impact and Fairness Monitoring
Given deposit risk evaluation occurs When the system computes the deposit Then it emits an event (e.g., deposit_risk_evaluated) containing: class ID, time bucket, day of week, sample size, weighted no-show rate, risk band, customer signal type used, fallback/grace reason (if any), caps applied, base and final deposit, request ID, and timestamp And emitted telemetry excludes sensitive personal data and complies with PII policies And the system produces aggregate metrics enabling monitoring of impact (conversion, no-show rate, avg deposit uplift/discount) and fairness across first-time vs returning segments And events are available within the agreed latency SLO and at the configured sampling rate
Checkout Policy Messaging & Transparency
"As a customer, I want clear deposit and refund terms at checkout so that I know exactly what I’m paying now and later."
Description

Display clear, localized messaging at checkout and in confirmations that separates deposit (due now) from remaining balance (due later), explains when/how the balance will be charged, and outlines refund/forfeit conditions tied to cancellation/reschedule windows. Update messaging in real time as demand/no-show adjustments apply. Provide accessible, mobile-first UI with tooltips, policy links, and price breakdown line items; mirror the same policy text in email/SMS and waitlist offer messages. Support per-class custom policy snippets to increase trust and reduce disputes.

Acceptance Criteria
Deposit vs Balance Breakdown at Checkout
Given a shopper selects a session with an active deposit rule When the checkout page loads Then the price breakdown displays two distinct line items labeled “Deposit (due now)” and “Remaining balance (due later)” with amounts that sum exactly to the total price And the deposit amount reflects the effective rule (class default or per‑session override) And a tooltip next to each line item explains what is due now vs later And a visible link labeled “Policy details” opens the full policy in-context without leaving checkout
Localized Policy Messaging and Currency
Given the shopper’s locale and currency are determined (by account settings or user preference) When policy text and price breakdown render at checkout and on confirmation Then dates and times use the shopper’s locale format (e.g., 24h vs 12h, day/month order) And currency amounts use the correct symbol and separators for the locale And all policy strings are shown in the selected language with a defined fallback language if a translation key is missing And the same localized content appears in the receipt/confirmation view
Real-Time Update on Dynamic Deposit Adjustments
Given deposit rules can change based on demand signals (e.g., peak time, limited capacity) or past no‑show rate When the shopper changes the selected session/time or when inventory/demand changes re-evaluate the rule Then the deposit and remaining balance amounts and the policy text update within 2 seconds without a full page reload And an ARIA live region announces the updated amounts for screen readers And the versioned policy snapshot used at the moment of payment is stored with the order and displayed in the confirmation
Refund/Forfeit Conditions by Cancellation/Reschedule Windows
Given a class has defined cancellation and reschedule windows with refund/forfeit outcomes When the shopper opens “Policy details” at checkout Then the messaging clearly states the time windows and outcomes (e.g., refund to original method, deposit forfeited, balance not charged) And examples are rendered relative to the session start (e.g., “Cancel ≥24h before start: full deposit refund”) And after booking, the confirmation displays the same conditions and next steps for cancellation/reschedule And a test cancellation simulation for the current time shows the correct computed outcome
Policy Mirroring in Email, SMS, and Waitlist Offers
Given a booking is completed or a waitlist auto-offer is sent When transactional email, SMS, and in-app messages are generated Then the deposit/balance amounts and policy text match the checkout snapshot exactly (no wording drift) And SMS uses a condensed template under 320 characters with a short link to full policy And all messages use the shopper’s locale and currency And the waitlist offer includes the same deposit/balance breakdown and cancellation outcomes before acceptance
Accessibility and Mobile-First Presentation
Given the shopper is on a mobile device or using assistive technology When viewing checkout policy messaging and price breakdown Then touch targets are at least 44x44px and layout does not require horizontal scrolling at 320px width And color contrast for text and interactive elements meets WCAG 2.1 AA And tooltips and policy links are reachable by keyboard, have visible focus, and are readable by screen readers with descriptive labels (e.g., “Deposit due now”) And the reading order and focus order match the visual order
Per-Class Custom Policy Snippets with Fallback
Given a class has a custom policy snippet configured (or not) When the shopper views checkout and the subsequent confirmations/messages Then the custom snippet renders in the designated policy area for that class And if no custom snippet exists, the account-level default policy is shown And all snippets are sanitized for safety, allowing only approved formatting And the exact snippet used at checkout is mirrored in email/SMS and stored with the order
Payment Capture, Forfeit & Refund Logic
"As an operator, I want deposit capture and refund rules enforced automatically so that I minimize manual intervention and payment disputes."
Description

Capture the deposit at booking, authorize and auto-capture the remaining balance based on configurable triggers (e.g., X hours pre-session or on check-in), and enforce refund/forfeit rules tied to cancellation/reschedule windows. Handle edge cases including failed auto-charge retries and notifications, instructor-initiated cancellations with partial/full refunds, reschedules carrying deposit forward, multi-attendee bookings, price changes post-booking, and expired waitlist offers. Provide idempotent operations, webhooks/events, and reconciliation reports to ensure financial integrity across payouts and dispute workflows.

Acceptance Criteria
Deposit Capture at Booking with Idempotency
Given class default deposit is 30% and session price is $100 When an attendee completes checkout Then the system calculates a $30 deposit, captures it within 5 seconds, and persists processor charge_id and payment_intent_id on the booking Given the same checkout request is retried with the identical idempotency_key within 24 hours When processed Then the system does not create a new charge and returns the original charge_id and payment_intent_id Given a deposit capture fails with processor_declined When processed Then the booking status remains Pending, the seat is held for 10 minutes, the attendee is prompted to update payment, and after 10 minutes the hold is released and no authorizations or captures exist Given a booking originates from a waitlist auto-offer When the attendee accepts within the offer window Then the deposit is captured at acceptance and the booking is confirmed Given a waitlist auto-offer expires without attendee acceptance When the offer window lapses Then any pending authorizations are voided within 5 minutes and no charges are created
Balance Authorization and Auto-Capture Triggers
Given trigger mode is "24 hours pre-session" and a $30 deposit was captured on a $100 booking When current time equals session_start_time minus 24 hours Then the remaining $70 is authorized and captured successfully and the booking balance becomes $0 Given trigger mode is "On check-in" and the attendee is checked in by the instructor When check-in is recorded Then the remaining balance is authorized and captured within 3 seconds and a receipt is issued Given an authorization will expire before the configured capture time When the gateway indicates impending expiry Then the system re-authorizes the remaining balance within the validity window and voids the prior authorization Given the remaining balance is $0 due to promotions or adjustments When the trigger fires Then no authorization or capture is attempted and any prior authorizations are voided
Cancellation Windows: Refund and Forfeit Enforcement
Given free_cancel_cutoff is 24 hours and late_cancel_cutoff is 2 hours When an attendee cancels ≥24 hours before start Then 100% of the deposit and any captured balance are refunded and the seat is released Given the same cutoffs When an attendee cancels between 24 and 2 hours before start Then the deposit is forfeited, any captured balance is refunded, and no further captures are attempted Given the same cutoffs When an attendee cancels <2 hours before start or no-shows Then the deposit is forfeited and no remaining balance is captured; if any balance was pre-captured it is refunded only if instructor policy permits refunds within this window Given an instructor-initiated cancellation at any time When processed Then 100% of all amounts paid (deposit and balance) are refunded to the attendee and the instructor is notified of the payout reversal Given any cancellation is processed Then a cancellation_refund or cancellation_forfeit event is emitted with booking_id, attendee_id, amounts (deposit_refunded, balance_refunded, deposit_forfeited), and timestamp
Reschedule: Deposit Carry-Forward and Repricing
Given an attendee reschedules ≥24 hours before the original start from a $100 session (deposit $30) to a $110 session When reschedule is confirmed Then the original $30 deposit is carried forward as credit and the new remaining balance becomes $80 subject to the configured trigger Given an attendee reschedules within a penalty window where deposit is not forfeited per policy When reschedule is confirmed Then the deposit is carried forward, prior authorizations are voided, and new trigger times are recalculated based on the new session start time Given a reschedule occurs and the new session price is lower than the original When reschedule is confirmed Then the carried-forward deposit is applied up to the new price, any overage is refunded, and no additional capture occurs Given a reschedule is attempted after check-in capture occurred When processed Then the system blocks reschedule or requires a refund + rebook flow, ensuring no double-capture and consistent ledger entries
Failed Auto-Capture Retries and Notifications
Given auto-capture fails with a soft_decline When the first attempt fails Then the system schedules up to 3 retries at 10 minutes, 1 hour, and 12 hours, emits retry_scheduled events, and notifies the attendee with a payment update link Given the second retry succeeds When processed Then the booking balance is captured, retry state clears, attendee and instructor receive success notifications, and a balance_captured event is emitted Given all retries fail before session start and the policy grace_period is 15 minutes after start When grace_period elapses Then the booking status becomes Payment Due, check-in is blocked, the seat may be auto-canceled per policy, and the deposit is handled per the applicable forfeit rules Given a hard_decline or do_not_honor code is returned When processed Then no further retries are scheduled and the attendee is immediately notified to update payment to proceed
Multi-Attendee Bookings and Post-Booking Price Changes
Given a booking for 3 attendees at $50 each with a 20% deposit When checkout completes Then $30 is captured ($10 per seat), and per-seat balances of $40 are tracked Given 1 attendee is canceled ≥24 hours before start When processed Then $10 deposit for that seat is refunded, that seat is released, and the remaining 2 seats are unaffected Given the class price increases to $60 after booking When the payer adds a 4th attendee Then the new seat uses the $60 price with a $12 deposit, while the original 3 seats retain their original balances and deposits Given any partial refund or forfeit is issued on a multi-attendee booking When processed Then receipts, webhooks, and ledger entries itemize amounts per attendee seat and the booking-level totals reconcile to the sum of seat-level entries
Financial Events, Idempotency, and Reconciliation Reports
Given any financial state change (deposit_captured, balance_authorized, balance_captured, refund_issued, forfeit_applied, retry_scheduled, retry_failed, booking_canceled, booking_rescheduled) When it occurs Then a webhook event is emitted with unique event_id, idempotency_key, booking_id, actor, amounts, and ISO-8601 timestamp, with at-least-once delivery and exponential backoff for 72 hours Given a duplicate API call to the refund endpoint includes the same idempotency_key When processed Then exactly one refund is created and subsequent calls return the same refund_id without issuing additional refunds Given a reconciliation report is generated for a date range and instructor account When processed Then the totals for charges, refunds, forfeits, payouts, and fees match gateway settlements with 0.00 variance and every transaction is linked to a payout_id or dispute_id Given a chargeback is received from the gateway When processed Then the related payout is adjusted, the booking is flagged Disputed, a dispute_opened event is emitted, and instructor and admin notifications are sent

Fair Grace Capture

Apply a configurable grace window for late arrivals and define clear no‑show thresholds. Attendance instantly releases the hold; missed check-ins auto-capture the deposit. Keeps enforcement consistent and fair, reducing awkward conversations and manual follow-up.

Requirements

Configurable Grace Window & No-Show Rules
"As a studio owner, I want to configure grace and no-show rules per class so that attendance enforcement is consistent and expectations are clear for clients and staff."
Description

Provide administrators with controls to define late-arrival grace periods and no-show thresholds at account, location, instructor, class-template, and single-session levels. Rules specify the number of minutes after start time a check-in is still honored, and the point at which an absence becomes a no-show that triggers payment actions. Supports defaults, per-class overrides, holiday/event exceptions, and virtual vs. in-person distinctions. Includes clear policy text surfaces on booking pages, reminders, and receipts to set expectations and improve fairness. Applies rules consistently across mobile and web check-in flows, honoring time zones and daylight saving changes. Ensures deterministic outcomes for edge cases (e.g., class start moved, instructor late, multi-entry passes) to eliminate manual adjudication.

Acceptance Criteria
Rule Precedence and Effective Values Resolution
Given configured rules: Account (grace 10, no-show 15), Location=Downtown (grace 5), Instructor=Jamie (no-show 18), Class-Template=Vinyasa 60 (no-show 20), Single-Session=2025-10-05 09:00 (grace 8) When a student books the 2025-10-05 09:00 session for Vinyasa 60 with Instructor Jamie at Downtown Then the effective grace equals 8 minutes and the effective no-show equals 20 minutes And the precedence order is: Single-Session > Class-Template > Instructor > Location > Account And if a level omits a value, it inherits from the next lower-precedence level without ambiguity And the mobile app and web API return the same computed values for the booking
Check-in Window and No-Show Trigger Execution
Given a class start at 2025-11-03 09:00 America/New_York with effective grace=7 minutes and no-show=12 minutes When the attendee checks in at 09:06:30 via mobile or web Then check-in is accepted and the payment hold for this booking is released within 5 seconds And no deposit is captured and the attendance status becomes "Attended" When the attendee has not checked in by 09:12:00 Then at 09:12:00 the booking is marked "No-Show", late check-in is blocked, and the deposit is captured within 60 seconds And the event is audit-logged with timestamp, device, and rule sources
Time Zone and Daylight Saving Integrity
Given Location timezone America/Los_Angeles and in-person rule evaluation uses the location timezone And a session scheduled 2025-03-09 01:30 local (DST start) with grace=5 and no-show=10 When DST jumps from 01:59:59 to 03:00:00 Then the no-show trigger occurs at 01:40 local wall time and does not drift to 03:10 And a session scheduled 2025-11-02 01:30 local (DST end) Then duplicate 01:30 hour is disambiguated using the scheduled offset, and grace/no-show triggers fire exactly once at 01:35 and 01:40 local And virtual sessions evaluate times using the class-template timezone consistently across mobile and web
Distinct Rules for Virtual vs In-Person Sessions
Given a class-template defines In-Person rules (grace 5, no-show 10) and Virtual rules (grace 2, no-show 5) And Session A is flagged Virtual and Session B is flagged In-Person When a student attempts check-in for Session A at 3 minutes after start Then the check-in is rejected as beyond the 2-minute virtual grace and no-show is assessed at 5 minutes if still not checked in When a student attempts check-in for Session B at 4 minutes after start Then the check-in is accepted within the 5-minute in-person grace And both sessions display the correct rule set on booking pages and reminders
Policy Text Display Across Booking, Reminders, Receipts
Given effective rules for a booking are grace=8 and no-show=20 in America/New_York When the student views the booking page, SMS reminder, email reminder, and receipt Then each surface displays: "Late arrival honored up to 8 minutes after start; no-show at 20 minutes triggers deposit capture" with localized times And if an admin changes the session override to grace=10 before start, all surfaces reflect the new value within 2 minutes And the receipt records the rule values in effect at time of attendance/no-show
Holiday/Event Exceptions Override Behavior
Given an exception "Holiday leniency" applies on 2025-12-24 to all locations and extends grace by +5 and defers no-show by +10 And base effective rules are grace=5 and no-show=10 When a session occurs on 2025-12-24 Then the effective rules become grace=10 and no-show=20 for that session And if an exception disables no-show capture, then no payment capture occurs and the absence status is "Absent (No Penalty)" And the admin preview correctly shows exception-adjusted rules for the session
Deterministic Outcomes for Start Changes, Instructor Late, and Multi-Entry Passes
Given a session start time changes from 09:00 to 09:15 before start Then grace and no-show timers recompute from 09:15 and booking/reminder policy text updates within 2 minutes Given the instructor flags "Instructor Late" with a 10-minute delay before start Then late and no-show timers shift by +10 minutes and late check-in remains open accordingly Given a booking reserves 3 seats with a total deposit hold of $30 ($10 per seat) When 1 attendee checks in within grace and 2 do not by the no-show threshold Then $10 is released for the attended seat and $20 is captured for the two no-shows, and the booking shows mixed attendance outcomes
Automated Hold Release & Deposit Capture
"As an instructor, I want holds released on timely arrival and deposits auto-captured on no-shows so that payment enforcement happens fairly without manual work."
Description

Automate payment actions tied to attendance outcomes. On successful check-in within the grace window, immediately release any preauthorization/hold placed at booking. When the no-show threshold elapses without check-in, automatically capture the configured deposit (full or partial), applying tax and fee rules as defined. Integrates with supported gateways (e.g., Stripe) using idempotent operations and webhook reconciliation to prevent double actions. Handles failures with retries and fallbacks, records reason codes, and updates booking/payment states in real time. Surfaces outcomes to the client and staff, and locks records to prevent manual inconsistencies.

Acceptance Criteria
Release Hold on Check-In Within Grace Window
Given a booking with an active payment hold and a configured grace window And the attendee checks in within the grace window in the class’s local time zone When the check-in event is saved Then the system calls the payment gateway to release the hold using an idempotency key unique to the booking and action And updates the booking payment state to Hold Released within 5 seconds And emits a client confirmation and a staff roster banner indicating Hold Released And records an audit entry with reason code CheckInWithinGrace and the gateway reference And prevents any manual capture on this booking thereafter
Auto-Capture Deposit on No-Show Threshold
Given a booking with a configured deposit amount and tax/fee rules And no check-in has been recorded by the no-show threshold When the threshold elapses Then the system captures the deposit amount per configuration, applying taxes and fees And uses an idempotency key unique to the booking and action to prevent duplicate captures And updates booking and payment records to Deposit Captured with captured amount and currency And sends client notification and staff roster alert stating capture due to no-show with reason code NoShowThresholdExceeded And locks payment fields from manual edits after capture
Idempotent Operations and Webhook Reconciliation
Given duplicate gateway webhooks or retried API calls for the same booking and action When reconciliation runs Then only a single financial action is performed and recorded for each action type And subsequent duplicates are acknowledged without side effects And booking state remains consistent with the latest successful gateway state And the audit log shows one completed entry and duplicate events marked as DuplicatedIgnored with references
Gateway Failure Retries and Fallback Workflow
Given a release or capture attempt fails with a retryable gateway error When the attempt fails Then the system retries up to 5 times with exponential backoff over 30 minutes And stops retrying immediately on the first success And on permanent failure marks status as Action Pending, notifies staff, and schedules a reattempt within 12 hours And no more than one successful financial action is ever recorded And all attempts are logged with error codes and correlation IDs
Record Locking to Prevent Manual Inconsistencies
Given an automated payment action is pending or completed for a booking When a staff user attempts manual capture, void, refund, or payment field edits Then the system blocks the action with a message Locked by Automation and logs the attempt And allows attendance check-in to proceed if within policy without altering completed payment outcomes And only users with a designated override role can unlock with a mandatory reason, which is audited
Accurate Amount Calculation with Tax and Fee Rules
Given a configured deposit policy with percentage or fixed amount and applicable taxes and platform fees When the system performs a capture due to no-show Then the net, tax, fee, and gross amounts are calculated per configuration and currency rounding rules And the captured amount sent to the gateway matches the gross amount exactly And the booking ledger reflects itemized breakdowns that sum to the gross amount And discrepancies greater than one minor currency unit cause the action to fail and retry
Real-Time State Updates and Notifications
Given a payment action completes successfully When the action result is received Then client-visible booking status updates within 5 seconds and staff roster badges update within 5 seconds And configured email and SMS notifications are sent within 60 seconds with explicit outcome text And in-app timeline shows the event with timestamp, actor System, reason code, and gateway reference And all updates are idempotent and safe to repeat
Role-based Overrides & Reasoned Exceptions
"As a front-desk staff member, I want controlled overrides with required reasons so that I can handle edge cases fairly while maintaining policy integrity and an audit trail."
Description

Enable authorized staff to apply case-by-case exceptions without breaking policy consistency. Provide role- and scope-based permissions to extend grace, waive capture, or convert a no-show to a late check-in within defined limits. Require a selectable reason and optional note, and automatically log the actor, timestamp, and policy context. Enforce guardrails (e.g., maximum waivers per period, approval workflows for large waivers) and reflect the override outcome in payments, attendance, and client history for transparency.

Acceptance Criteria
Permission Enforcement for Role- and Scope-Based Overrides
Given a staff member without the Override:Waive permission attempts to waive a deposit capture on a booking When they submit the override Then the system denies the action, shows an authorization error, and no payment/attendance/client history changes are made Given a staff member has Override permissions but their scope excludes the booking’s location or class When they attempt any override Then the system denies the action and logs an access-denied audit event with actor, attempted action, booking ID, and timestamp (UTC) Given a staff member with the required permission and scope opens the override form When the form loads Then a required Reason dropdown with policy-configured reasons is present, a free-text Note field is optional and limited to 500 characters, and the Submit button is disabled until a reason is selected Given a valid override is submitted When the system processes it Then an audit log entry is created capturing actor ID and role, timestamp (UTC), action type, booking ID, policy version ID, reason code, and note (if provided)
Extend Grace Within Policy Limits
Given a booking is late within the policy-configured extendable window and no auto-capture has executed When an authorized staff member applies a grace extension of X minutes where 0 < X ≤ remaining extendable minutes Then the new grace deadline is set to now + X minutes, the auto-capture job is scheduled for the new deadline, and the attendance status remains Eligible for Check-In (not No-Show) Given a booking’s grace has already been extended up to the policy maximum When an authorized user attempts a further extension Then the system blocks the action and displays an error indicating the maximum extension has been reached Given a valid grace extension is applied When the change is saved Then the roster reflects the updated grace deadline, the client is not charged, and an audit log records actor, timestamp (UTC), previous and new grace deadlines, reason, and policy version ID
Waive Deposit Capture With Guardrails
Given policy limits waivers to N per client per rolling 30 days and the client has already reached N When an authorized staff member attempts to waive the capture Then the system blocks the waiver, shows remaining waivers = 0, and no financial state is changed Given a waiver amount exceeds the policy approval threshold TH When the waiver is submitted Then the waiver enters Pending Approval state, no capture is taken, and approvers are notified; the booking shows a pending flag Given a waiver is approved by an approver role When the approval is recorded Then the payment ledger records a zero capture with Waived status and reason code, the booking shows Capture Waived, and the client history shows a Waiver entry linked to the booking; revenue reports exclude the waived amount Given a waiver is rejected or expires per policy When the decision/timeout occurs Then the pending flag is cleared, auto-capture proceeds per policy schedule, and the client/staff are notified of the outcome
Convert No-Show to Late Check-In With Reconciliation
Given a booking is marked No-Show and the deposit was captured, and policy allows conversion within W hours When an authorized staff member selects Convert to Late Check-In with a required reason Then attendance changes to Late-Checked-In (Converted), the client is checked in at the override time, and an audit log records the conversion with before/after states, actor, timestamp (UTC), and policy version ID Given the conversion occurs and the payment processor supports same-day voids When the capture reversal is executed within the processor’s void window Then the original capture is voided; otherwise a refund or account credit is issued per policy configuration, and the ledger posts a linked reversal/credit entry Given the conversion attempt is outside the policy time window W or conflicts with guardrails When submission occurs Then the system blocks the action and displays a clear error explaining the violation
Approval Workflow for Large Overrides
Given an override request exceeds policy thresholds (amount, frequency, or risk) or the requester lacks self-approval rights When the request is submitted with a required reason Then an approval record is created with status Pending, assigned to an approver role, and in-app/email notifications are sent to approvers with a link to review Given an approver reviews a pending request When they approve it Then the system executes the override atomically, updates payments/attendance/client history accordingly, records approver identity and timestamp (UTC), and notifies the requester; on reject, no changes are applied and the request is closed with a recorded reason Given a pending approval reaches the policy-configured SLA timeout When the timeout elapses Then the request auto-expires as Rejected (Expired), notifies the requester, and no changes are applied to the booking
Guardrail Counters and Rate Limits
Given policy defines per-staff maximum M overrides per rolling 7 days across specified actions (grace extensions, waivers, conversions) When a staff member attempts an override that would exceed M Then the system blocks the action, displays the remaining allowance (0), and logs the attempt without incrementing counters Given an override is successfully applied When counters are updated Then per-staff and per-client counters increment by 1 for the relevant action type and roll off automatically as events age beyond the policy window Given an admin updates guardrail values (M, windows) in settings When saved Then the new values are enforced for subsequent attempts and are versioned for audit with effective-from timestamps
End-to-End Transparency Across Payments, Attendance, and Client History
Given any override is completed (grace extension, waiver, conversion) When the transaction commits Then three records are written: a payments ledger entry, an attendance timeline entry on the roster, and a client history timeline entry; each includes actor, timestamp (UTC), action type, reason code, policy version ID, and before/after states Given records are stored When a user with View Override History permission opens any of the three surfaces Then the override details are consistently displayed and cross-linked to the other surfaces; clients can see a redacted version (no staff identity) in their portal if client-visible is enabled by policy Given an override record exists When a correction is needed Then the original record remains immutable and a new compensating record is created (reversal or amendment) with linkage to the original
Real-time Client Notifications & Receipts
"As a client, I want clear messages about grace timing and any charges so that I understand what happened and don’t need to argue or chase support."
Description

Send timely, clear communications that reflect the grace policy and any resulting payment actions. Include grace and no-show policy snippets in pre-class reminders via SMS/email. On check-in within grace, confirm attendance and hold release. On no-show capture, notify with the reason, captured amount, and a link to policy and dispute/contact options. Support localization, quiet hours, and brand templates. Ensure delivery status tracking and retries, and write notifications to the client communication timeline for reference.

Acceptance Criteria
Pre-class Reminder Includes Grace and No-Show Policy
Given a client has an upcoming class with a configured grace window and no-show threshold When the pre-class reminder is sent at the configured lead time Then SMS and email include: class start time in client timezone, location/join info, grace window length, no-show threshold, a concise policy snippet, and a link to the full policy And the studio brand template is applied And if the scheduled send time falls within quiet hours, the reminder is queued and delivered within 5 minutes after quiet hours end And SMS length management ensures content stays within 1–2 parts by truncating the policy body and retaining the policy link And the send event is recorded in the client communication timeline with channel, template ID, and preview text
Check-in Within Grace: Attendance Confirmation & Hold Release Notification
Given a client checks in within the configured grace window When attendance is recorded Then the system releases the deposit/authorization immediately And sends an SMS/email confirmation within 60 seconds stating attendance is confirmed, the hold is released, and no charge was captured And the message uses the client’s language and timezone And both the release event and notification with delivery outcome are written to the client communication timeline
No-Show Deposit Capture Notification & Receipt
Given a client has not checked in by the end of the defined no-show threshold When the system auto-captures the deposit per policy Then an SMS/email is sent within 2 minutes stating the reason (missed check-in), the captured amount in client currency, and links to the policy and dispute/contact options And a receipt is attached or linked showing itemized charge, timestamp, tax, last4, and merchant name And amounts, date/time, and policy link are localized per client settings And the capture event and notification with delivery status are recorded in the client communication timeline
Delivery Status Tracking, Retry, and Fallback
Given any outbound SMS or email is initiated When delivery fails Then transient errors are retried up to 3 times at 1 minute, 5 minutes, and 15 minutes And hard bounces or permanent failures are marked Failed without retry And if SMS ultimately fails and email is available, one email fallback attempt is made (unless duplicate sending is disabled by studio settings) And final status (Sent, Delivered, Failed) and retry history are persisted and visible in the client communication timeline
Quiet Hours Compliance
Given studio quiet hours are configured When a notification is scheduled to send during quiet hours Then pre-class reminders and policy notifications are deferred until quiet hours end and are delivered within 5 minutes after quiet hours end And check-in confirmations and receipts bypass quiet hours only if the studio setting "Allow transactional during quiet hours" is enabled; otherwise they are deferred And all deferrals and overrides are logged with reason in the client communication timeline
Localization and Timezone-Correct Rendering
Given a client has preferred language and timezone (or falls back to studio defaults) When any notification is generated Then the localized template for that language is used or falls back to the studio default if unavailable And all dates/times display in the client’s timezone with timezone abbreviation And currency amounts are formatted per locale conventions And policy links resolve to the matching localized policy page
Brand Templates and Sender Identity
Given a studio has configured email and SMS brand templates and verified sender identities When a notification covered by this requirement is sent Then email uses the configured template (logo, colors, footer, legal text) and from-address on the studio’s verified domain or falls back to the system domain with DKIM alignment And SMS uses the configured sender ID/profile and appends the brand signature only if the message remains within 320 characters And the template version ID and sender used are stored with the notification record for audit
Waitlist & Capacity Sync on No-Show
"As a studio manager, I want seats from no-shows to roll to the waitlist automatically so that we keep classes full without manual juggling."
Description

Upon no-show threshold, immediately free the seat and trigger the smart waitlist to auto-offer the opening according to existing waitlist rules and cutoffs. Prevent premature releases by re-checking final status atomically at threshold. Respect class-specific late-offer limits (e.g., no offers after start+X). Update capacity counters, booking statuses, and notifications coherently to minimize empty spots and maximize revenue.

Acceptance Criteria
Atomic No-Show Threshold Evaluation and Seat Release
Given a booked attendee with status "Booked" for a class with graceWindow G and startTime S, and no-show threshold T = S + G And the attendee has not checked in and has not cancelled before T When the scheduler evaluates no-show at T Then the system performs an atomic re-check of the attendee's final status at T And if still not checked-in and not cancelled, the booking status changes to "No-Show" And the seat is freed and reflected in capacity within 3 seconds of T And an audit log records evaluation timestamp, prior status, final decision, and actor "system"
Waitlist Auto-Offer Trigger Within Cutoff
Given a seat is freed due to a no-show at time t where t <= S + L (lateOfferCutoff) And the waitlist has one or more eligible entries per existing prioritization rules When the seat becomes available Then the system creates a hold for the top-ranked eligible waitlister and reduces "available" by 1 and increases "offerHold" by 1 And an offer notification is sent via configured channels immediately And the offer includes a response deadline equal to the class's offerResponseWindow And the waitlister's status becomes "Offer Sent" with expiryTime = t + offerResponseWindow
Late-Offer Cutoff Enforcement
Given a seat is freed due to a no-show at time t where t > S + L (lateOfferCutoff) When the system processes the freed seat Then no waitlist offers are generated And waitlist entry statuses remain unchanged And capacity "available" increases by 1 and "offerHold" does not change
Capacity and Booking Status Consistency Transaction
Given class totalCapacity C and current counters {occupied, available, offerHold} And a single booking is marked "No-Show" and (if applicable) one waitlist offer hold is created When the transaction commits Then occupied + available + offerHold = C And the counters and booking status update atomically in the same transaction (all-or-nothing) And repeated processing with the same idempotency key makes no additional changes
Offer Decline/Timeout Cascade to Next Waitlister
Given an active offer to waitlister W1 with expiryTime E and eligible waitlisters W2...Wn remain When W1 declines before E or the offer expires at E And current time <= S + L Then the hold for W1 is released and the next eligible waitlister receives a new hold and offer within 5 seconds And W1 status updates to "Offer Declined" or "Offer Expired" And the cascade continues until a waitlister accepts or the cutoff is reached And if the cutoff is reached during cascade, no further offers are sent and the remaining hold (if any) is released
Premature Release Prevention at Edge Check-in
Given an attendee checks in at timestamp tc such that tc <= T When the no-show evaluation runs at T Then the attendee is not marked "No-Show" And the seat is not freed And no waitlist offer or notifications related to a no-show are generated for that booking
Notification and Audit Consistency
Given a seat is freed and an auto-offer is sent When notifications are dispatched Then the selected waitlister receives exactly one offer notification per seat via each configured channel And the instructor/studio receives a capacity change alert if enabled And every state change (No-Show marked, capacity updated, offer created, offer response) is recorded in the audit log with timestamps and correlationId
Time Integrity & Check-In Methods
"As a product owner, I want check-ins to use a trusted time source across methods so that grace and no-show decisions are accurate and defensible."
Description

Support reliable, tamper-resistant attendance determination. Provide multiple check-in methods (QR scan, staff console, client self-check on mobile) that all honor the same time source and policy. Use server-side time with synchronized offsets and location time zones to avoid device-clock manipulation. Handle connectivity loss with queued, signed check-ins that apply policy upon sync. Record precise timestamps, source, and device metadata to back decisions and support disputes.

Acceptance Criteria
QR Check-In Honors Server Time and Grace Window
Given a scheduled class with configured grace window G and no-show threshold N in the class’s location timezone And the server is the authoritative time source with a computed client offset O When a client scans the class QR to check in Then the server computes t_checkin using server time (or reconciled time via O) And marks the attendee Present-On-Time if t_checkin <= class_start + G And marks the attendee Present-Late if class_start + G < t_checkin <= class_start + N And marks the attendee No-Show if t_checkin > class_start + N And the decision is identical for repeated QR submissions (idempotent) And the decision is not impacted by the device’s local clock beyond the reconciled offset O
Staff Console Attendance Uses Unified Time Source
Given a staff member checks in a client via the staff console for a scheduled class with grace G and no-show threshold N When the staff taps Check In at time t Then the system uses server time to compute t_checkin And applies the same grace/no-show rules as client QR/self-check methods And produces the same decision that would result from a QR/self-check at the same instant And records the actor=staff_console with staff_user_id in the audit log And the action is idempotent for the same client and class
Mobile Self Check-In Works Offline with Queued, Signed Entries
Given a client is offline and attempts self check-in within the app And the app holds a last-known server time offset Δ and a valid signing key When the client submits check-in Then the app stores a signed payload including class_id, user_id, local_timestamp, offset_version, method=self_check, device_id, and signature And upon reconnect within the configured sync window S, the server verifies the signature and offset_version And computes t_checkin = local_timestamp + Δ if signature valid and Δ age <= A (max offset age) And applies grace/no-show rules using t_checkin in the class’s timezone And if signature invalid or Δ age > A or payload tampered, the server uses receipt_server_time as t_checkin and flags anomaly=true in the audit And the final decision is idempotent across any subsequent online check-in attempts for the same class and user
Device Clock Tamper Attempt Does Not Affect Attendance Decision
Given a device’s local clock is manipulated to differ from server by more than drift threshold D When the attendee attempts any check-in method (QR, self, or staff-assisted on that device) Then the server disregards the raw client local time and bases t_checkin on server time (or bounded by reconciled offset) And flags drift_exceeded=true with measured_drift in the audit log And ensures the attendance decision cannot be earlier than the server-reconciled time And the resulting decision matches what would occur from a check-in on an uncompromised device at the same real time
Time Zone and DST Correctness at Class Location
Given a class scheduled in location timezone TZ_L with grace G and no-show threshold N And an attendee’s device timezone TZ_U may differ from TZ_L When evaluating check-in timeliness Then the system converts all times to absolute epoch and applies policy using TZ_L for class boundaries And handles DST transitions so that class_start, G, and N are anchored to TZ_L correctly even across DST changes And produces the same decision regardless of the attendee’s TZ_U or travel across time zones
Audit Trail Completeness for Dispute Resolution
Given any attendance decision is made When the system persists the record Then the audit log includes: server_epoch_t_checkin, server_epoch_class_start, grace G, no_show_threshold N, method (qr/self/staff), actor (user_id or staff_user_id), device_id (or hash), app_version, client_local_time (if available), computed_offset Δ and version, signature_status, timezone TZ_L, drift_measured, anomaly_flags, and decision And the audit record is immutable, queryable by class_id and user_id, and exportable within 2 seconds And retrieving the audit record reproduces the same decision when re-evaluated
Consistency and Idempotency Across Multiple Methods
Given a user attempts to check in for the same class via multiple methods (e.g., scans QR then staff console) within the attendance window When processing these submissions Then the system deduplicates by class_id + user_id and uses the earliest valid t_checkin among the verified submissions And returns the same decision for subsequent duplicate attempts (HTTP 200 with existing decision) And records all attempts in the audit log with dedup_reason and chosen_source And ensures the final decision is identical regardless of submission order or method mix

Waitlist Rollover

When a spot is forfeited and the waitlist accepts the opening, the original depositor can be auto-refunded or converted to account credit per your policy. Maximizes filled seats while preserving goodwill and encouraging rebooking.

Requirements

Refund/Credit Policy Engine
"As a studio owner, I want to define how forfeited deposits are refunded or credited so that rollovers align with our policies and reduce disputes."
Description

Configurable rules that determine whether forfeited deposits are auto-refunded to the original payment method or converted to account credit, with support for global defaults and per-class overrides. Includes cutoff windows (e.g., hours before start), partial retention or fees, tax treatment, currency handling, and credit expiration/usage rules. Surfaces policy text on booking pages and confirmations, and exposes settings in admin with preview of outcomes. Ensures consistent, compliant handling of customer funds while preserving goodwill and encouraging rebooking.

Acceptance Criteria
Policy Resolution: Class Override vs Global Default
Given a global default refund/credit policy is configured and Class A has a per-class override When a booking for Class A is forfeited due to a waitlist acceptance event Then the engine applies Class A's override policy to determine refund vs credit, fees, tax, and credit rules And the applied policy identifier and version are recorded on the forfeiture event and financial transaction Given Class B has no per-class override When a booking for Class B is forfeited due to a waitlist acceptance event Then the engine applies the global default policy and records the applied policy identifier and version
Cutoff Window Boundary Logic
Given a cutoff window (e.g., 24 hours before class start) is configured for a class in the class's timezone When forfeiture occurs with time-until-start >= the configured cutoff Then before-cutoff rules are applied (as defined by the policy) When forfeiture occurs with time-until-start < the configured cutoff Then after-cutoff rules are applied (as defined by the policy) And the boundary at exactly the cutoff duration is treated as before-cutoff And all time calculations use the class's timezone with correct DST handling
Partial Retention/Fee With Tax Treatment Calculation
Given a policy defines either a percentage retention/fee or a fixed retention/fee (but not both) When a forfeiture is processed Then the retained amount = min(deposit, round_half_up(retention_fee + applicable_tax_on_fee, currency_precision)) And the refundable/creditable amount = round_half_up(deposit - retained_amount, currency_precision) and is never negative And if the fee is marked taxable, tax_on_fee = round_half_up(fee * class_tax_rate, currency_precision); if not taxable, tax_on_fee = 0 And all monetary components (refund/credit amount, fee, tax_on_fee) are itemized on receipts/notifications and stored in the ledger And attempts to save a policy with both percentage and fixed fees configured are rejected with a validation error
Currency Handling for Refunds and Credits
Given the original deposit was paid in currency C_txn When the policy outcome is refund Then the refund is issued to the original payment method in C_txn for the computed refund amount When the policy outcome is account credit Then the credit is recorded in C_txn for the computed credit amount And when that credit is redeemed against an order in a different currency C_order Then the system converts the credit at the provider FX rate at redemption time, rounds to currency precision, and displays the applied rate and converted amounts prior to payment And the applied converted amount differs from the theoretical value by no more than 0.01 of C_order due to rounding
Credit Issuance, Expiration, and Auto-Application at Checkout
Given the policy outcome is to issue account credit with an expiration window (e.g., 180 days) When a forfeiture is processed Then a credit ledger entry is created with amount, currency, issuance timestamp, expiration timestamp, and usage scope per policy When a customer with eligible, unexpired credits initiates checkout Then credits auto-apply to the deposit/order total up to the available balance using FIFO across multiple credits, allowing partial application, and displaying the applied amount and remaining balance And expired credits do not auto-apply and are clearly indicated as expired And usage of credit creates a ledger movement and updates remaining balances without altering original expiration dates
Policy Text on Booking Pages and Confirmations
Given a class has either a per-class policy override or falls back to the global default When a customer views the booking page Then the page displays human-readable policy text that includes refund vs credit outcome rules, cutoff duration, any retention/fees and tax treatment, and credit expiration/usage rules And the displayed cutoff timestamp is computed and shown in the class's timezone with clear labeling When a booking is confirmed (email/SMS/receipt) Then the same policy text (as of booking time) is included on the confirmation and receipt And changes to policies affect future bookings' displayed text but do not retroactively alter past confirmations
Admin Policy Preview Accuracy
Given an admin configures policy parameters (refund vs credit rules, cutoff, fees, tax treatment, currency, credit expiration) When the admin enters preview inputs (deposit amount, class start datetime, current time/forfeit time, currency) Then the preview panel calculates and displays the exact outcome (refund vs credit, amounts, fees, tax, currency, credit expiration) matching the production calculation service to the cent in the relevant currency And the preview updates within 1 second of changing inputs And attempting to save invalid configurations (e.g., both fixed and percentage fees) blocks save with an inline validation message
Auto-Rollover Trigger & Offer Flow
"As a waitlisted student, I want to be offered an open spot and confirm it easily so that I can join without manual coordination."
Description

Automated detection of seat forfeiture (cancellation, payment lapse, or attendance cutoff) that immediately offers the opening to the next eligible waitlisted customer based on queue order and rules. Manages hold windows, acceptance actions (one-tap via SMS/email or in-app), capacity checks, and instant seat assignment upon acceptance. On acceptance, initiates policy-driven refund or credit for the original depositor and updates inventory in real time. Handles concurrency to prevent double assignment and provides clear state transitions visible to admins and users.

Acceptance Criteria
Forfeiture Detection and Offer Initiation
Given a fully booked session with an active waitlist and a forfeiture event occurs (customer cancellation, payment lapse after grace period, or attendance cutoff), When the system processes the event, Then it marks exactly one seat as available and generates a waitlist offer to the next eligible customer within 3 seconds, And publishes an OfferCreated event including sessionId, seatId, offerId, recipientId, forfeitureType, timestamp, And does not create an offer if no eligible waitlisters exist, And if capacity has been refilled by another process before offer creation, no offer is sent and the seat remains assigned.
Offer Hold Window and Auto-Advance
Given a waitlist offer is created, Then the offer hold window is set per policy (default 15 minutes; configurable 5–120 minutes) and the remaining time is displayed to the recipient, When the hold expires without acceptance or the recipient explicitly declines, Then the offer state transitions to Expired or Declined and the system auto-advances to the next eligible waitlister within 2 seconds, And the prior recipient can no longer accept (token invalidated), And only one active offer per seat exists at any time, And if the waitlist is exhausted, the seat returns to public inventory and the admin is notified.
One-Tap Acceptance via SMS/Email/In-App
Given the recipient receives an SMS/email magic link and an in-app banner for the offer, When the recipient taps/clicks the link or Accept button within the hold window, Then the system validates a signed, single-use token bound to offerId and userId with TTL equal to remaining hold, And if payment is required and a saved payment method and auto-capture are allowed, the charge is attempted immediately; otherwise the recipient is prompted to confirm and pay, And upon successful acceptance (and payment if required) the seat is assigned instantly, the offer state becomes Accepted, and a confirmation is sent, And the acceptance flow completes within 3 seconds end-to-end for auto-capture cases, And if the token is invalid/expired or the offer is no longer available, a non-success message is shown and no assignment occurs.
Concurrency Control and Single Assignment
Given multiple recipients attempt to accept the same seat concurrently, When acceptance requests are processed, Then exactly one request commits the assignment and receives a success response, And all others receive an OfferTaken/Conflict response within 1 second and the offer is not assigned to them, And inventory is decremented once and only once, And all acceptance endpoints are idempotent via idempotency keys, And audit logs show one Assigned event and N−1 RejectedDueToConcurrency events for the same offerId.
Policy-Driven Refund or Credit on Acceptance
Given a waitlist acceptance replaces an original depositor, When the seat is assigned to the waitlisted recipient, Then the original depositor is processed per policy: full or partial refund to original payment method or issuance of account credit, And the policy-calculated amount excludes any non-refundable fees configured, And the refund/credit operation is initiated within 5 seconds of acceptance and linked to the original charge, And if a refund fails at the gateway, an equivalent account credit is created and the admin is alerted, And users receive notifications of refund or credit with amounts and references, And ledger entries are recorded for both reversal and new charge (if any).
Real-Time Inventory and State Visibility
Given offers and acceptances occur, Then the admin dashboard shows real-time state transitions (Forfeited → Offered → Accepted/Expired/Declined → Assigned) with timestamps, actor, and reason, And the recipient sees current offer state and remaining hold time in their channel (SMS/email link landing and in-app), And inventory counts update immediately upon state changes and are consistent across API and UI, And OfferCreated/OfferExpired/OfferAccepted/SeatAssigned/RefundIssued events are available via API/webhooks within 1 second of occurrence, And an immutable audit log is stored for each offer and assignment.
Eligibility and Queue Order Enforcement
Given a session has a waitlist with multiple customers, When selecting a recipient for an offer, Then the system uses FIFO queue order after applying eligibility rules (not banned, no scheduling conflicts, meets membership/age limits, meets payment requirements if policy requires), And ineligible customers are skipped with skip reasons logged, And the first eligible in order is selected and recorded, And no reordering occurs except due to explicit decline/expiry or admin action (which is audited), And capacity is rechecked immediately before sending an offer; if no capacity remains, no offer is sent.
Payment Gateway Refunds & Credit Ledger
"As a studio owner, I want refunds and credits processed automatically and accurately so that finances reconcile without manual effort."
Description

Integrated refund execution with supported processors (e.g., Stripe) including partial/full refunds, fee retention, idempotency, webhook reconciliation, and retry logic for transient failures. Implements an internal account credit ledger with issuance, balance tracking, expiration, and automatic application at checkout. Displays credit balances in user profiles and receipts, and provides admin views for adjustments with permission controls. Ensures financial accuracy, auditability, and seamless reuse of value to drive rebooking.

Acceptance Criteria
Auto-refund on forfeited spot via Stripe (full refund)
Given a paid booking captured via Stripe and policy set to auto-refund on forfeiture When the spot is forfeited and the waitlist accepts the opening Then the system creates a refund record with status=Pending and an idempotency key derived from the booking and forfeiture And a Stripe refund is initiated for the full captured amount within 60 seconds And on receipt of refund.succeeded webhook the internal status updates to Succeeded within 2 minutes and stores refund_id, amount, and currency And an immutable ledger entry links booking_id and refund_id with amount equal to the refund (negative value) And the customer receives an email/SMS confirmation including refunded amount and expected settlement window
Partial refund with non‑refundable fee retention
Given policy defines a non-refundable fee and a booking with captured amount and currency When a partial refund is triggered per policy Then the refund amount equals captured_amount minus non_refundable_fee, rounded to currency minor units, and never exceeds captured_amount And the Stripe refund is created for the computed amount and recorded on the refund record And the receipt shows a clear breakdown of refunded_amount and retained_fee And the ledger reflects a negative entry for refunded_amount and no entry for retained_fee, maintaining financial balance
Idempotent refund execution and duplicate request protection
Given a refund request is issued with an idempotency key for a booking When the same refund is retried due to timeouts or duplicate triggers Then exactly one refund exists at the gateway and in the system And all retries return the same refund_id and status without creating additional refunds And only one ledger entry exists for the refund And concurrent attempts are serialized or rejected with HTTP 409 and no side effects
Webhook reconciliation and event deduplication
Given Stripe webhooks are configured for refund events When refund.succeeded or refund.failed events are received, possibly out of order or duplicated Then each event is verified via signature, deduplicated by event_id, and applied idempotently And internal refund status transitions to Succeeded or Failed within 2 minutes of valid event processing And the system records gateway timestamp, event_id, and refund_id for auditability And malformed or unverifiable events are rejected with 400 and do not alter internal state
Retry logic for transient gateway errors on refund initiation
Given a refund initiation attempt encounters a retriable error (HTTP 429/5xx or network timeout) When the refund is attempted Then the system retries with exponential backoff up to 5 attempts within 10 minutes using the same idempotency key And each attempt is logged with timestamp, attempt number, and error code And no duplicate refunds are created at the gateway And after max retries, the refund is marked Action Required and an alert is sent to ops
Account credit issuance and automatic application at checkout
Given policy is set to convert forfeited funds to account credit When a spot is forfeited and the waitlist accepts the opening Then a credit ledger entry is created for the eligible amount with an expiration date per policy And the customer's profile displays the updated credit balance within 1 minute And at the next checkout, the credit auto-applies up to the order total before charging any payment method And the receipt shows credit_applied, remaining_credit, and charged_amount And if credit >= order total, the charge is $0 and the remaining balance is reduced accordingly
Admin credit adjustments with permissions and audit trail
Given an admin opens the credit adjustment tool for a user When the admin has Finance or Owner role and submits an adjustment with a required reason Then the adjustment is applied and an immutable ledger entry is created capturing before_balance, delta, after_balance, actor_id, timestamp, and reason_code And users without the required role cannot access or submit adjustments (UI hidden, API returns 403) And all adjustments appear in the audit log and export with correlation to admin user and affected account
Participant Notifications & Templates
"As a student, I want clear, timely notifications about offers and refunds so that I can take action and trust the process."
Description

Automated, event-driven SMS and email messages for all rollover stages: offer sent, offer expiring, offer accepted/declined, seat assigned, refund issued, credit issued. Includes customizable templates with variables (class name, time, hold window, amount), localization, quiet hours, and compliance (opt-in/out, unsubscribe). Provides delivery status, link tracking for one-tap acceptance, and fallbacks between channels. Enhances clarity and trust while reducing support inquiries.

Acceptance Criteria
Offer Sent: Personalized, Localized Dispatch
Given a waitlist offer is created for a participant who has opted in to one or more channels When the offer is generated Then dispatch the offer notification to each opted-in channel within 10 seconds And include {class_name}, {class_datetime_local}, {hold_window_minutes}, and {accept_link} variables rendered in the participant’s locale and timezone And if a channel is within configured quiet hours, schedule that channel’s message for the first allowed send time and log the scheduled time And do not send to unsubscribed channels, logging the suppression reason
Offer Expiring Reminder Before Hold Window Ends
Given an outstanding waitlist offer with a defined hold window and a configured reminder offset When the reminder time is reached Then send a reminder only to participants who have not accepted or declined, via their opted-in channels And include {time_remaining_minutes} and {accept_link} And if the reminder time falls within quiet hours, schedule for the first allowed time that precedes offer expiry; otherwise suppress and log "missed due to quiet hours" And cancel the reminder if the offer is accepted, declined, or expires before send time
One-Tap Acceptance, Decline, and Seat Assignment Notices
Given the participant clicks the one-tap {accept_link} before expiry When the click is received Then record a tracked acceptance event with timestamp and source channel and atomically assign the seat to the participant And send a "seat assigned" confirmation via the same channel within 10 seconds including {class_name} and {class_datetime_local} Given the participant uses the decline link before expiry When the decline is received Then record a tracked decline event with timestamp and source channel and send a decline confirmation within 10 seconds And stop any pending reminders for that offer
Refund or Credit Issuance Notifications
Given a forfeited seat triggers an automatic transaction per policy (refund or account credit) When the transaction is completed Then send a notification within 30 seconds including {amount}, {currency}, {method}, and {reference_id} And format currency per the participant’s locale and include a link to account credit balance when method is credit And do not send if the participant has unsubscribed from all channels; log suppression
Template Customization, Variables, and Localization
Given an admin edits templates for rollover stages (offer sent, offer expiring, offer accepted, offer declined, seat assigned, refund issued, credit issued) When saving a template Then validate that required variables for the stage are present (e.g., {class_name}, {class_datetime_local}, {hold_window_minutes} for offer/expiring; {amount} for refund/credit) And prevent save with a clear error if required variables are missing or malformed And allow per-language versions; if a participant’s locale lacks a custom version, fall back to the default language template And provide a live preview with sample data rendered using the participant’s timezone
Compliance: Opt-In/Out and Unsubscribe
Given a message is composed for SMS or email When it is sent Then include legally required opt-out instructions for the channel and region (e.g., SMS STOP, email unsubscribe link) Given a participant opts out via STOP or unsubscribe When the opt-out is processed Then update consent immediately, suppress future sends on that channel, and log timestamp, channel, and region And do not send to any channel for which the participant lacks opt-in consent
Delivery Status Tracking and Channel Fallbacks
Given a notification is sent When provider webhooks respond Then record status transitions with timestamps: queued, sent, delivered, failed, suppressed, engaged (clicked) And when an SMS or email delivery fails or remains undelivered for more than 2 minutes Then attempt a single fallback send to the alternate opted-in channel and link the messages to prevent duplicate user notifications And if both channels fail, mark "Failed after fallback" with provider error codes And when the {accept_link} is clicked, record a click event with timestamp and source channel and associate it to the original message
Acceptance Timeouts & Fallback Logic
"As an operations manager, I want timeouts and fallback logic so that seats are filled quickly and fairly when offers are ignored."
Description

Configurable timers for how long an offer is held, with automatic progression to the next waitlisted person on expiry or decline. Supports proximity rules to class start (shorter windows), maximum cascade depth, and final fallback to public availability if the waitlist is exhausted. Includes seat locking to prevent oversubscription and visibility rules to show current offer status. Optimizes fill rate while maintaining fairness and predictability.

Acceptance Criteria
Configurable Offer Hold Timer
Given a default hold time H (minutes) is configured, When a waitlist offer is created at T0, Then the offer expiry is T0 + H minutes. Given a class-level override Hc exists, When an offer is created for that class, Then the expiry is T0 + Hc minutes and not H. Given H is outside the range [5, 2880], When the admin attempts to save the value, Then the system rejects it with a validation error and retains the prior value. Given the default or override is changed at time T1, When an offer created before T1 is still active, Then its original expiry remains unchanged. Given the system restarts, When it comes back online, Then previously saved H and Hc values are retained and applied to new offers.
Auto-Progress on Expiry or Decline
Given recipient A has an active offer, When A declines or the expiry timestamp passes, Then the next eligible waitlistee B is offered within 30 seconds. Given A’s offer is closed, When B is offered, Then A cannot accept and any attempt returns "Offer no longer available". Given notifications via SMS and/or email are enabled, When B is offered, Then B receives the notification within 60 seconds. Given a member was marked ineligible or skipped earlier in the cascade, When progression occurs, Then that member is not re-offered within the same cascade.
Proximity-Based Hold Time Rules
Given proximity rules [{threshold: 24h, hold: 30m}, {threshold: 2h, hold: 10m}] are configured and class start is at Tc, When an offer is created at T0 with Tc − T0 = 3h, Then the hold time is 30 minutes. Given the same rules and Tc − T0 = 1h 30m, When an offer is created, Then the hold time is 10 minutes. Given no proximity rule matches, When an offer is created, Then the hold time equals the default H. Given a computed expiry would be after Tc, When an offer is created, Then the expiry is capped at Tc (no later than class start).
Maximum Cascade Depth
Given max cascade depth D = 5 is configured, When a vacancy triggers a waitlist cascade, Then no more than 5 sequential offers are issued before stopping. Given all D offers expire or are declined, When the Dth offer closes, Then the system triggers the configured fallback action. Given the admin sets D outside [1, 100], When saving, Then the system rejects the value with validation and retains the prior D. Given an offer is accepted at depth k ≤ D, When acceptance is confirmed, Then the cascade stops and no further offers are issued.
Fallback to Public Availability
Given the waitlist is exhausted or max depth D is reached without acceptance, When the last offer closes, Then the seat is made publicly available within 30 seconds. Given the seat is public, When the booking page is refreshed, Then availability increases by 1 and the class is bookable by the public. Given the seat becomes public, When a public booking is completed, Then the seat count decrements and cannot be double-booked.
Seat Locking and Oversubscription Prevention
Given an active waitlist offer exists, When public users view the class, Then the reserved seat is not counted toward public availability. Given two separate cancellations create two distinct vacancies, When offers are created, Then exactly one seat is locked per offer and locked + confirmed seats never exceed capacity. Given two recipients attempt to accept in parallel such that only one seat remains, When the later payment intent confirms, Then it is rejected with "Seat no longer available" and no charge is captured. Given an offer expires or is declined, When the next offer is issued or fallback triggers, Then the prior seat lock is transferred or released within 30 seconds.
Offer Status Visibility and Privacy
Given an active offer exists, When the admin views the roster/waitlist, Then the UI shows the current recipient, time remaining (mm:ss), and cascade position k/D with updates at least every 10 seconds. Given an active offer exists, When non-recipient waitlisters view the class, Then they see "Offer in progress" without the recipient’s full name or contact details. Given the recipient views the offer page, When the countdown reaches zero, Then the page updates to "Offer expired" without requiring a manual refresh.
Audit Trail, Reporting & Reconciliation
"As a studio owner, I want a complete audit trail and reports so that I can verify outcomes and reconcile revenue."
Description

Comprehensive event logging for forfeitures, offers, acceptances, declines, timeouts, refunds, credits issued/redeemed, and admin overrides. Provides timeline views within bookings and waitlist entries, exportable reports (CSV) for finance and performance metrics (fill rate, time-to-fill, recovered revenue), and reconciliation views mapping refunds/credits to gateway transactions and ledger entries. Enables transparency, accountability, and data-driven optimization.

Acceptance Criteria
Event Logging for Waitlist Rollover Lifecycle
Given a class uses Waitlist Rollover and has bookings and waitlist entries When any of the following occurs: deposit forfeiture, waitlist offer sent, offer accepted, offer declined, offer timeout, seat auto-assigned, refund issued, credit issued, credit redeemed, admin override applied Then an append-only event record is created within 2 seconds of the action commit And the record includes: event_id, event_type, occurred_at (UTC ISO-8601 ms), actor_type, actor_id (nullable), source (app/api/admin), affected_booking_id (nullable), affected_waitlist_entry_id (nullable), previous_state (nullable), new_state (nullable), amount (nullable), currency (ISO-4217), gateway_transaction_id (nullable), ledger_entry_id (nullable), policy_id (nullable), correlation_id, idempotency_key, metadata (JSON), ip_address/user_agent for human actors And duplicate triggers with the same idempotency_key do not create additional events And events are retrievable by correlation_id and ordered by occurred_at then event_id for tie-break
Booking Timeline View
Given an admin opens a booking detail with rollover-related activity in the last 90 days When the Timeline tab is viewed Then all events correlated to that booking are listed chronologically with newest first And each row displays: label, occurred_at in venue timezone and relative time, actor, amount/currency if applicable, state-change badge, link to gateway/reconciliation when present And filters allow event type multi-select and date range; clearing filters resets to all And the view supports at least 500 events per booking via pagination or infinite scroll And clicking an event opens a side panel with full event payload (secrets redacted) with median response ≤ 300 ms And user permissions mask financial fields for unauthorized roles
Waitlist Entry Timeline View
Given a waitlist entry has received one or more offers for a class When the Timeline is viewed for that waitlist entry Then all related events (offers, accepts, declines, timeouts, seat assignments, refunds/credits) are shown in chronological order with newest first And each row includes occurred_at (local and relative), actor, offer window, outcome, and links to the related booking and class session And filters allow narrowing by offer outcome (accepted/declined/timeout) and by date And the view supports at least 300 events per waitlist entry and loads the first page in ≤ 400 ms P50 And financial fields are visible only to roles with Finance permission
CSV Exports for Events and Performance Metrics
Given an Owner selects a date range ≤ 92 days, optional location/class filters, and a report type When exporting "Events Ledger" Then a RFC 4180-compliant CSV is generated with columns: event_id, occurred_at_utc, occurred_at_local, event_type, booking_id, waitlist_entry_id, actor_type, amount, currency, gateway_transaction_id, ledger_entry_id, policy_id, correlation_id, source And up to 100,000 rows stream to the browser in ≤ 60 seconds with stable ordering by occurred_at_utc ASC When exporting "Performance Metrics" Then a CSV is generated with per-class-session metrics: capacity, seats_booked, seats_filled_via_waitlist, fill_rate_pct, offers_sent, offers_accepted, median_time_to_fill_minutes, recovered_revenue_amount, refunds_count, credits_issued_amount And metric calculations match glossary definitions and pass unit tests within ±0.5% tolerance And exporting identical inputs produces identical outputs (row counts and values)
Refund/Credit Reconciliation Mapping
Given refunds or account credits are issued due to waitlist rollover When viewing the Reconciliation tab for a selected date range Then each refund/credit row maps to one or more gateway transactions and one or more ledger entries with account codes And the sum of mapped amounts equals the refund/credit amount using banker’s rounding to 2 decimals And unmatched items are flagged as Unmatched and are filterable And selecting a row reveals booking/waitlist references, policy_used, fees, payout batch id/date, and gateway links And "Mark as reconciled" requires Owner or Finance role, captures reason, actor_id, timestamp, and creates an audit event And reconciliation summary totals by day match gateway payout summaries within ±0.01 of currency
Admin Override Capture and Guardrails
Given a user with Admin or Owner role attempts an override affecting rollover (e.g., force-assign seat, waive timeout, reverse forfeiture) When the override is submitted Then the action requires a justification of ≥ 10 characters and a two-step confirmation And an override audit event is created with before/after fields, policy_bypassed flag, actor_id, occurred_at, correlation_id And unauthorized roles cannot access override actions; attempts are denied and logged as access_denied events And overridden actions are labeled "Override" in timelines and exports
Access Control, Redaction, and Data Retention
Given role-based permissions (Owner, Manager, Instructor, Staff) When viewing timelines, reconciliation, or exports Then only roles with Finance permission see amounts, gateway IDs, and ledger IDs; others see "****" and cannot export And all export downloads are logged as audit events with requester_id, filters, and file checksum And PII (email, phone) is excluded from exports by default unless an Owner explicitly opts in via "Include PII" with a warning modal And event and report data are retained for ≥ 25 months and are immutable; corrections are appended as new events with linkage

Flex Forfeit Ladder

Create tiered forfeiture rules based on cancellation timing (e.g., 24h, 6h, 1h). We auto-calculate partial vs. full capture and show the schedule at checkout. Encourages timely cancellations and feels fair to clients.

Requirements

Tiered Forfeit Policy Builder
"As a studio owner, I want to configure tiered forfeiture rules per offering so that fees are fair and encourage timely cancellations without manual intervention."
Description

Provide an admin UI to create, edit, and assign multi-tier forfeiture rules (e.g., 24h/6h/1h) with per-tier actions such as percentage capture, fixed fee, or credit conversion. Support policy scoping by product (class type, course, private session), plan (drop-in, class pack, membership), location, and instructor. Include validation to prevent overlapping tiers, preview timeline relative to event start, and mobile-friendly management. Handle timezone selection, effective dates, cloning/templates, and versioning so historical bookings retain their original policy.

Acceptance Criteria
Create Multi-Tier Policy with Mixed Actions
Given I am an admin on the Tiered Forfeit Policy Builder When I create a new policy with a unique name and add tiers at 24h, 6h, and 1h before event start Then I can set per-tier actions as 50% capture, fixed fee $10, and 100% credit conversion and save successfully Given a sample session price of $40 is entered in the preview When I view the policy just saved Then the preview computes: at 24h => $20 charge; at 6h => $10 charge; at 1h => $0 charge and $40 credit Given I switch a tier action type (e.g., from fixed fee to percentage) When I save Then the system preserves the new action type and value and reflects it in the preview calculations
Tier Boundary Validation and Non-Overlap
Given a draft policy with tiers at 24h and 6h before start When I add another tier at 24h Then I see an inline error "Tier times must be unique" and Save is disabled Given a draft policy When I add tiers in any order (e.g., 1h, 24h, 6h) Then the UI auto-sorts tiers from furthest to nearest to start (24h, 6h, 1h) Given a tier time uses minutes (e.g., 1h 30m) When I save Then the value is accepted and displayed consistently in hours/minutes Given I enter an invalid action value When percentage capture > 100% or < 0% OR fixed fee < 0 OR credit conversion > 100% or < 0% Then the offending field shows an error and Save remains disabled until corrected
Timeline Preview and Timezone Handling
Given I select an event start of 2025-03-09 08:00 in America/Los_Angeles When I view the timeline preview Then each tier shows its exact wall-clock timestamp with timezone abbreviation and correct DST handling Given I change the timezone to Europe/Berlin When the preview refreshes Then tier timestamps update to the new timezone and maintain correct offsets from event start Given I change the event start datetime When I view the preview Then all tier timestamps and labels update instantly without page reload
Scoped Assignment by Product, Plan, Location, Instructor
Given I open Assignments for a policy When I select Class Types: Yoga, Pilates; Plans: Drop-in, Class Pack; Locations: Downtown; Instructors: Alex Kim Then the assignment summary displays the selected scopes and the count of affected offerings Given I remove a previously selected scope When I save Then the policy no longer applies to that scope and the summary updates accordingly Given no scope is selected When I attempt to save Then I see an error indicating at least one scope must be selected or the policy must be set as a default/global policy (if enabled)
Effective Dates and Versioning Preserve Historical Bookings
Given Policy V1 is active and booking B1 was created before any edits When I edit the policy and publish V2 with an effective start datetime in the future Then B1 remains linked to V1 and a read-only banner indicates its governing version Given V2 becomes effective When a new booking B2 is created after the effective start Then B2 is governed by V2 and references its version ID Given a prior version has governed any existing booking When I attempt to edit or delete that prior version Then the system prevents changes and deletion and indicates that immutable versions are locked due to historical bookings
Clone from Template and Safe Duplication
Given I choose Clone on an existing policy named "Standard 24/6/1" When the draft is created Then the new policy is named "Copy of Standard 24/6/1" (editable), includes identical tiers and actions, and has no scopes assigned by default Given I select a built-in template "24h/6h/1h" When the draft opens Then default tiers and suggested actions are prepopulated and require explicit Save to activate Given I attempt to save a cloned policy with a duplicate name When I click Save Then I see a uniqueness error on Name and Save is blocked until the name is unique
Mobile-Friendly Admin Management
Given I open the Policy Builder on a 375px-wide mobile device When I interact with all form fields and the timeline preview Then all controls are reachable without horizontal scrolling, tap targets are at least 44x44px, and the primary actions are visible and usable Given a simulated 4G network with cold cache When I load the Policy Builder Then the page reaches interactive state within 3 seconds and visual completion within 2 seconds Given the on-screen keyboard is open on mobile When I focus the last editable field Then the Save/Publish actions remain visible via a sticky footer and are not obscured
Real-time Forfeit Calculation Engine
"As a studio owner, I want the system to automatically compute the correct forfeiture amount at cancellation time so that charges are accurate and consistent."
Description

Implement a deterministic service that selects the applicable tier at cancellation/no-show time using event start, current timestamp, and policy version in the event’s timezone. Calculate capture amounts (partial/full) considering taxes/fees configuration, currency rounding, coupons, credits, and package/membership entitlements. Support partial capture via payment intents where available and fallbacks when gateways don’t support partial capture. Expose idempotent API and webhook events for downstream systems and ensure auditable inputs/outputs.

Acceptance Criteria
Tier Selection by Event Timezone and Policy Version
Given a booking linked to policy_version_id=v3 with tiers at 24h, 6h, 1h before event start in the event’s timezone When cancellation occurs at exactly start-24h local time Then the 24h tier is selected Given the same setup When cancellation occurs at exactly start-6h local time Then the 6h tier is selected Given the same setup When cancellation occurs at exactly start-1h local time Then the 1h tier is selected Given the same setup When cancellation occurs before start-24h local time Then no forfeiture tier is selected (0 capture) Given callers in different timezones submit the same absolute timestamp When the engine evaluates tier selection Then the same tier is deterministically selected using the event’s timezone Given the booking is associated with policy_version_id=v3 and the org default has moved to v4 When a cancellation is evaluated Then v3 is used for tier selection
Partial vs Full Capture with Taxes/Fees and Currency Rounding
Given a USD booking with base=80.00, tax=20.00 (exclusive), total paid=100.00 and a 50% forfeiture tier When capture is calculated Then capture amount=50.00 USD rounded to 2 decimals Given a USD booking with total paid=100.00 and a $2.00 non-refundable fee configured When a 50% forfeiture is calculated Then capture amount=52.00 USD (50.00 + 2.00) Given a JPY booking total paid=10000 and a 35% forfeiture tier When capture is calculated Then capture amount=3500 JPY rounded to 0 decimals Given currency-specific minor units are configured per ISO 4217 When capture is calculated Then rounding is applied to the currency’s minor unit and never exceeds total paid
Discounts, Credits, and Entitlements Application Order
Given a booking fully covered by a package/membership entitlement (net paid=0) When forfeiture is evaluated Then capture amount=0 Given a booking total paid=100.00 USD, with $20 coupon and $10 account credit applied, and a 50% tier When forfeiture is calculated Then net paid subject to forfeiture=70.00 and capture amount=35.00 USD Given a booking with a 100% off coupon (net paid=0) When forfeiture is evaluated Then capture amount=0 Rule: Entitlements reduce payable amount first, then credits, then coupons; capture is limited to net paid after these adjustments
Gateway Partial Capture with Fallback Behavior
Given a Payment Intent that supports partial capture with authorization=100.00 USD and computed capture=35.00 USD When capture is executed Then only 35.00 USD is captured and the authorization is closed without additional charges Given a gateway that does not support partial capture and authorization=100.00 USD with computed capture=35.00 USD When capture is executed Then full 100.00 USD is captured and 65.00 USD is immediately refunded so net merchant capture=35.00 USD Given an expired or missing authorization and computed capture=35.00 USD When capture is executed Then a new charge for 35.00 USD is created subject to gateway capabilities and risk controls Rule: Capture execution must not exceed computed capture amount and must report the final net captured amount
Idempotent Forfeit Calculation and Execution API
Given POST /forfeits with Idempotency-Key=K and a request body B When the call is retried with the same K and body B within the idempotency window Then the response status, body, and side effects (calculation and financial actions) are identical and no duplicate capture occurs Given POST /forfeits with Idempotency-Key=K and a different body B2 When the call is made Then a 409 Conflict is returned and no new side effects occur Rule: Idempotency scope is per merchant and endpoint; keys are stored for at least 24h or the longer of the gateway’s authorization window
Webhook Emission with Auditable Payloads
Given a successful calculation When processing completes Then a forfeit.calculated webhook is emitted containing booking_id, event_id, policy_version_id, tier_id, event_start_local, evaluated_at_local, currency, amounts_breakdown {paid, discounts, credits, entitlements_value, taxes, fees, capture}, rounding_mode, gateway_refs, idempotency_key, request_id, and version Given a successful capture execution When processing completes Then a forfeit.captured webhook is emitted with the final net captured amount and gateway charge references Rule: Webhooks are signed (HMAC SHA-256) with timestamped headers, retried with exponential backoff until 2xx, delivered in order per booking, and payloads are retained for audit
No-Show Auto Forfeit at Event Start Cutoff
Given a booking with no cancellation and no check-in When current time reaches event_start_local (or configured grace period elapses) Then the no-show tier is selected and capture is executed according to computation rules Given multiple workers process the same booking When no-show evaluation runs Then only one capture is executed due to idempotency and locking Rule: Evaluation uses the event’s timezone; if run late, the tier selection uses the actual evaluation timestamp and still selects the most severe applicable tier
Checkout Policy Disclosure
"As a client, I want to see the cancellation fee schedule before I pay so that I can make an informed booking decision."
Description

Display the forfeit ladder prominently during checkout and on booking confirmation: concise summary with thresholds and outcomes, expandable to full policy. Ensure clear, plain-language, localized copy, accessibility compliance, and mobile-first layout. Persist the policy summary in confirmation emails/SMS and calendar invites so clients can reference it later. Track acknowledgment for compliance and reduce disputes.

Acceptance Criteria
Mobile Checkout: Policy Summary Visible and Expandable
Given a user on the checkout page for a class with an active forfeit ladder When the page renders on a mobile viewport 320–414 px wide and 640 px height Then a concise policy summary listing each tier’s threshold and outcome is displayed above the primary pay button without requiring vertical scroll And a control labeled “View full policy” is visible and tappable And activating the control expands to show the complete ladder with thresholds expressed in the event’s local timezone and currency And the user can collapse the view to return to the summary state
Accessibility: Expandable Policy Control Complies with WCAG 2.2 AA
Given keyboard-only navigation When focus reaches the policy disclosure control Then the control is a button element with an accessible name matching the visible label And pressing Enter or Space toggles expansion, updating aria-expanded and aria-controls on the button And the expanded/collapsed state change is announced by screen readers And all summary and full policy text meets a contrast ratio of at least 4.5:1 against the background And the expanded content is reachable in a logical reading order and is dismissible via keyboard
Localization and Formatting of Policy Copy
Given the user’s locale and currency preferences and the event’s timezone are known When checkout renders Then all policy copy is served in the user’s locale with no untranslated keys or placeholders And time thresholds are formatted in the locale’s date/time style and include the event timezone abbreviation And monetary amounts and percentages use the event currency and locale number formatting And if the locale is unsupported, English is used as a fallback and the fallback is logged for telemetry
Post-Booking Surfaces Persist Policy Summary
Given a successful booking When the confirmation page loads Then the policy summary is displayed and references the exact policy version applied to the booking And the confirmation email includes the same summary and a trackable deep link to the full policy And the SMS confirmation includes a condensed summary (<=160 characters) and a trackable short link to the full policy And the calendar invite description includes the summary and link without truncation in major calendar clients
Acknowledgment Capture Blocks Payment Until Confirmed
Given the checkout page is ready When the user attempts to pay without acknowledging the policy Then the pay action is blocked and an inline message prompts acknowledgment When the user checks the acknowledgment box Then the pay action is enabled And upon successful payment, the system stores an acknowledgment record containing booking ID, user identifier (or anonymous flag), timestamp with timezone, policy version ID, locale, and IP address, and emits analytics events “policy_acknowledged” and “policy_version_applied”
Admin/Support Retrieval of Acknowledgment for Disputes
Given an admin opens a booking in the dashboard When viewing the policy and compliance section Then the policy acknowledgment record is visible with all captured fields and a render of the exact policy version shown to the client And the record can be exported to CSV and a shareable link with a redacted view can be copied And the acknowledgment view is accessible within two navigational clicks from the booking detail
Resilience: Policy Load and Fallback Behavior
Given a transient failure when fetching policy content When the checkout page loads Then a cached last-known-good policy for the event is rendered and marked as cached And if no cached policy exists, the pay action remains disabled and an error banner prompts retry until policy loads And an error with correlation ID is logged to monitoring and surfaced in admin diagnostics
Cancellation & Capture Flow
"As a client, I want to know exactly what I’ll be charged when I cancel and have it processed immediately so that there are no surprises."
Description

At cancellation, present a breakdown of the applicable fee/credit, require explicit confirmation, and process capture/refund/credit issuance immediately. Support edge cases: unpaid reservations, offline payments, gift cards, and insufficient funds with graceful fallback and dunning. Write receipts, update booking status, and sync with external calendars/CRM. Provide admin override with reason codes and optional grace application, all fully logged.

Acceptance Criteria
Cancellation Fee Breakdown and Explicit Confirmation
Given a booked class with an identified payer and an applicable forfeiture tier based on the cancellation timestamp When the customer initiates cancellation Then a confirmation modal displays: tier label and cutoff, forfeited amount, refundable amount, credit to be issued (if applicable), tax/fees breakdown, net total, and source of funds (card/credit/gift/offline) And the displayed timezone matches the class location’s timezone And the Confirm action remains disabled until the customer checks the explicit consent checkbox And selecting "View policy" opens the full forfeiture ladder details And dismissing the modal or navigating back does not change booking state or trigger any payment operations
Immediate Capture, Refund, or Credit Issuance on Confirm
Given the cancellation confirmation modal is displayed and the customer has provided explicit consent When the customer confirms cancellation Then the system deterministically computes capture/refund/credit per the active ladder tier for the exact server-side timestamp And submits all payment operations with an idempotency key and atomic ledger write And completes 95th percentile processing within 3 seconds and 99th percentile within 8 seconds And issues store credit immediately where configured; otherwise processes refund as applicable And writes an itemized receipt and sends email/SMS notifications to customer and admin within 60 seconds And updates booking status to one of: Cancelled-PaidForfeit, Cancelled-Refunded, or Cancelled-Credited accordingly And no duplicate charges or credits occur if the confirm action is retried within 10 minutes
Handling Unpaid, Offline, and Gift Card Payments at Cancellation
Given a booking with status Booked-Unpaid When the customer cancels Then no monetary transaction is attempted And booking status updates to Cancelled-Unpaid And a cancellation notice (without receipt) is sent Given a booking marked Paid-Offline with recorded amount and reference When the customer cancels and a forfeit is due Then no online capture is attempted And an admin task "Offline collection required" is created with due amount and booking reference And booking status updates to Cancelled-PaymentPending-Offline And the customer receives a cancellation confirmation without charge confirmation Given a booking paid with a gift card balance and optionally a card-on-file When the customer cancels Then the forfeited amount is deducted from gift balance first; any remainder is attempted on card-on-file And if no card is on file or capture authorization is not possible, booking status updates to Cancelled-Dunning and a secure pay link is sent to the customer
Insufficient Funds Declines and Dunning Flow
Given a booking with a required forfeiture capture on card When the PSP declines the capture due to insufficient funds or hard decline Then the booking status updates to Cancelled-Dunning And a secure pay link is generated and sent via email/SMS immediately And automated retries are scheduled (up to 3 attempts over 72 hours with exponential backoff) And each retry outcome and PSP code is logged with timestamp And on successful recovery, status updates to Cancelled-PaidForfeit and an itemized receipt is sent And after final failure, status updates to Cancelled-Uncollected and an admin alert is created
Admin Override with Reason Codes and Optional Grace
Given an authorized admin opens the cancellation management panel When the admin applies an override selecting one of: Waive fully, Set custom amount, Apply grace window; and selects a required reason code (with optional notes) Then the computed forfeiture is replaced per the override selection And the customer-facing confirmation reflects the override (e.g., fee waived or adjusted) And override metadata is captured and immutable: actor, timestamp, original amount, overridden amount, reason code, notes And if a positive amount remains, payment operations follow the same immediacy, idempotency, and receipt rules as standard cancellations And all override actions are fully auditable in the booking timeline
Receipts, Status Updates, and External Calendar/CRM Sync
Given a cancellation (standard or override) has been processed When payment operations and ledger entries complete Then an itemized receipt with transaction IDs is stored and accessible on the booking timeline And booking status, customer balance, and revenue ledger reflect the final amounts consistently And external calendars are updated within 2 minutes (attendee removed, availability restored, waitlist promotion triggered if applicable) And CRM/contact timelines receive a cancellation event with outcome and amounts; failures are retried with backoff up to 5 times And a manual Re-sync action is available to admins and is idempotent
Threshold Reminder Alerts
"As a client, I want reminders before fee thresholds so that I can cancel in time if my plans change."
Description

Send optional SMS/email alerts when a client approaches the next forfeit tier (e.g., “6h window closes in 30 minutes”). Respect user notification preferences, quiet hours, and rate limits. Localize send times to event timezone and suppress alerts after cancellation or transfer. Provide templated content editable by studios and track click/engagement for optimization.

Acceptance Criteria
Schedule Alerts at Tier Lead Times (Event Timezone)
Given an event has forfeit tiers at 24h, 6h, and 1h before start and an alert lead time of 30 minutes in the event timezone When a client has an active booking Then alerts are scheduled at [start - tier - lead time] for each tier not yet passed, computed in the event timezone Given a booking is created after the ideal send time for the next tier but before that tier closes When alert eligibility is evaluated Then the alert for that tier is sent immediately (within 1 minute), subject to quiet hours and rate limits Given a tier is edited or the event start time changes When the scheduler runs Then pending alert times are recalculated to the new [start - tier - lead time] and obsolete jobs are canceled
Suppress Sends During Quiet Hours
Given studio quiet hours are configured as 21:00–08:00 in the event timezone When an alert's computed send time falls within quiet hours Then the alert is not sent and the job is marked suppressed with reason=quiet_hours Given quiet hours would delay an alert past the tier close When quiet hours end Then the alert is not sent (no catch-up) and suppression is logged Given an alert's computed time is outside quiet hours When the scheduled time arrives Then the alert is sent normally
Honor Client and Studio Notification Preferences
Given the studio has enabled Threshold Reminder Alerts and selected channels When a client has Email=off and SMS=on Then only SMS is sent and no email job is created Given a client has opted out via STOP for SMS or unsubscribed from email When a tier alert would be sent Then that channel is skipped and suppression reason=opt_out is recorded Given a client has disabled all channels or has no verified contact info When a tier alert would be sent Then no alert is sent and suppression reason=no_opt_in is recorded
Rate Limit and De-duplicate Threshold Alerts
Given default limits of max 2 alerts per enrollment per rolling 24 hours and minimum 30 minutes separation between alerts per recipient across channels When multiple tier alerts would fire within the window Then only the first two are sent and additional alerts are suppressed with reason=rate_limited Given retries or duplicate scheduling occurs for the same enrollment-tier When sends are attempted Then only one alert is delivered (idempotency key=enrollment_id+tier_id) Given a manual resend is triggered within 30 minutes of the last send When attempted Then it is blocked with reason=rate_limited
Cancel/Transfer Suppresses Pending Alerts
Given a pending alert exists for an enrollment When the client cancels the booking before the send time Then all pending threshold alerts for that enrollment are canceled and none are sent Given a pending alert exists for event A and the client transfers to event B When the transfer completes Then pending alerts for event A are canceled and new alerts for event B are scheduled according to event B's tiers Given cancellation or transfer occurs within 1 minute of a planned send When the send worker executes Then no alert is sent for the original enrollment
Editable Templates and Merge Fields Render Correctly
Given the studio edits and saves SMS and email templates for threshold alerts When a new alert is sent Then the sent content uses the latest saved templates Given required placeholders {tier_close_at} and {manage_booking_link} are missing When saving the template Then saving is blocked with inline validation errors listing missing placeholders Given a valid template with placeholders {client_first_name}, {class_name}, {tier_name}, {tier_close_at}, {manage_booking_link} When an alert is sent Then placeholders are rendered with event timezone (including TZ abbreviation), SMS length <= 320 characters, and links are shortened and click-tracked Given a runtime template rendering error occurs When sending Then the system falls back to the default template for that channel and logs reason=template_fallback
Engagement Metrics Tracked per Alert
Given an email alert is sent When the recipient opens and clicks the tracked link Then events delivered=true, opened=1, total_clicks=1, unique_clicks=1 are recorded within the alert record Given the recipient clicks the link twice When analytics aggregate Then total_clicks=2 and unique_clicks=1 are shown for that alert Given an SMS alert is sent and the link is clicked When analytics are viewed Then delivered=true and click events are recorded; open is not tracked for SMS Given engagement data is collected When the studio views the dashboard for the past 7 days Then CTR and send/suppression counts per channel are displayed for threshold alerts
Waitlist Policy Alignment
"As a client receiving a last-minute waitlist spot, I want a fair grace period before penalties apply so that I’m not charged if I can’t respond immediately."
Description

Align forfeit ladders with live waitlist offers by recalculating thresholds based on acceptance time. Provide a configurable grace period for newly accepted spots to prevent unfair penalties, and exclude declined/expired offers from penalties. Reflect the updated countdown in offer notifications and booking details. Ensure parity across auto-offer and manual promotion flows.

Acceptance Criteria
Recalculated Ladder on Auto-Offer Acceptance
Given a class scheduled for 2025-10-01 18:00 (venue timezone) with a forfeit ladder: >24h: 0%, 6–24h: 50%, 1–6h: 75%, <1h: 100% And a waitlisted client receives an auto-offer at 15:30 and accepts at 15:35 And a grace period of 10 minutes is configured When the system recalculates forfeit thresholds from the acceptance time Then the penalty schedule is 0% from 15:35–15:45 (grace), 75% from 15:45–17:00, and 100% from 17:00–18:00 And canceling at 15:40 results in 0% capture, at 15:50 results in 75% capture, and at 17:05 results in 100% capture And the recalculated schedule is displayed in the booking details in the venue timezone
Configurable Grace Period on Accepted Spots
Given the studio sets a waitlist-acceptance grace period of 15 minutes And a client accepts a waitlist offer within any ladder window When the client cancels within 15 minutes of acceptance Then no forfeiture is applied and the payment capture is $0 When the client cancels after the 15-minute grace but before the next ladder threshold Then the penalty equals the percentage defined for the currently active ladder tier And setting the grace period to 0 minutes applies no grace and penalties begin immediately per the active tier And the applied grace period value is shown in the offer message and booking details
No Penalty for Declined or Expired Waitlist Offers
Given an auto-offer is sent at time T and the client declines before expiry When the offer is declined Then no booking is created, no cancellation record is generated, and no forfeiture is captured And the client’s payment method is not charged and no penalty appears in their account history Given an auto-offer expires without response Then no booking is created, no cancellation record is generated, and no forfeiture is captured And any subsequent offers to other clients are unaffected by the declined/expired outcome
Updated Countdown in Offer Notifications and Booking Details
Given an auto-offer is generated at 15:30 for an 18:00 class with a 10-minute grace period and the current ladder placing cancellations in the 1–6h tier until 17:00, then <1h thereafter When the SMS/email/push offer is rendered Then it states: “If you accept, you have 10m grace; after that 75% until 17:00, then 100% until 18:00,” with times in the venue timezone And after the client accepts at 15:35, the booking details show a live countdown to the next threshold and the list of upcoming penalty windows per the recalculated schedule And the countdown updates in real time and matches the applied penalties at cancellation time
Parity Between Auto-Offer and Manual Promotion Flows
Given an admin manually promotes a waitlisted client at 15:35 for an 18:00 class When the promotion is confirmed Then the system sets the acceptance time to 15:35 and applies the same grace period and recalculated ladder schedule used for auto-offer acceptance at 15:35 And the booking confirmation and booking details present the identical countdowns and penalty windows as the auto-offer flow And a subsequent cancellation at the same timestamps yields the same penalty amounts in both flows
Accurate Monetary Capture per Recalculated Ladder
Given a class price of $30.00 and a recalculated ladder where the active tier at cancel time is 50% When the client cancels within that tier window Then the captured amount equals $15.00, rounded to two decimals, in the class currency And if the active tier is 75% the captured amount equals $22.50; if 100% then $30.00; never exceeding the amount paid after discounts And the transaction record references the applied ladder tier and calculation basis (price x percentage) And the same amounts are applied identically whether the booking originated from auto-offer acceptance or manual promotion
Acceptance Within Strictest Tier Honors Grace Period
Given a client accepts a waitlist offer at 17:40 for an 18:00 class And a 10-minute grace period is configured and the strictest ladder tier (<1h) is 100% When the client cancels at 17:47 Then 0% is captured due to grace When the client cancels at 17:55 Then 100% is captured And the booking details and receipts reflect the grace application and the applied tier
Forfeit Reporting & Audit Trail
"As a studio owner, I want clear reporting and a complete audit trail for forfeiture charges so that I can reconcile payouts and resolve disputes confidently."
Description

Provide dashboards and exports for forfeiture revenue, avoided revenue, refunds, and dispute rates by policy, offering, instructor, and timeframe. Generate ledger entries with tier applied, timestamps, inputs (times, policy version), and calculation details for audit. Include override reason codes, user IDs, and communication artifacts to streamline dispute resolution and reconciliation with payouts.

Acceptance Criteria
Dashboard Aggregates by Policy/Offering/Instructor/Timeframe
Given ledger entries exist with forfeiture, avoided, refund, and dispute records across multiple policies, offerings, and instructors When the user selects a timeframe, grouping (policy/offering/instructor), and currency Then the dashboard shows Forfeiture Revenue, Avoided Revenue, Refunds, and Dispute Rate values that match a validated reference query within ±0.01 per currency And each grouping bucket sums only entries within the selected timeframe And the Dispute Rate = disputed_entries_count / total_forfeiture_entries_count for the bucket And applying any combination of filters (policy, offering, instructor, dispute status) updates the aggregates correctly And all aggregations use the selected timezone boundaries (including DST) applied to ledger timestamps stored in UTC And the dashboard refresh completes within 2000 ms for up to 100k ledger entries
Export With Filters and Required Columns
Given a user applies filters for timeframe, policy, offering, instructor, and dispute status When the user exports forfeiture data to CSV Then the CSV contains one row per ledger entry with these columns at minimum: ledger_entry_id, booking_id, client_id, instructor_id, offering_id, policy_id, policy_version_id, policy_version_label, forfeiture_tier_id, forfeiture_tier_label, tier_threshold_hours, scheduled_start_at_utc, scheduled_start_timezone, cancellation_at_utc, decision_type (capture_full|capture_partial|no_charge|refund), captured_amount, refunded_amount, avoided_amount, currency, calculation_details, override_flag, override_reason_code, override_by_user_id, created_at_utc, updated_at_utc, payout_id, dispute_id, communication_artifact_count, content_hash And all numeric amounts use 2 decimal precision and ISO 4217 currency codes And the exported rows reflect exactly the on-screen filters And exports over 50k rows are generated asynchronously and delivered via a secure download link with a 24-hour expiry
Ledger Entry Generation on Cancellation
Given a booking with a locked policy version and a Flex Forfeit Ladder of tiers When the client cancels relative to the scheduled start time Then the system selects the applicable tier based on the time delta and creates an immutable ledger entry with: tier applied, thresholds, scheduled_start_at, cancellation_at, decision_type, captured_amount/refunded_amount/avoided_amount, currency, calculation_details, created_at And the ledger entry stores both the original scheduled_start_at and the effective times used in the calculation And the operation is idempotent using a cancellation event key so duplicate events do not create duplicate ledger entries And if payment capture fails, the ledger entry records the failed attempt and no captured_amount until a retry succeeds, with all retries appended as child attempts
Override Append-Only with Reason Codes
Given a staff user with the required permission When they adjust a forfeiture decision or amount Then the system creates a new append-only ledger adjustment entry referencing the original entry and does not modify or delete the original And a reason_code from a predefined list is mandatory and a free-text note up to 500 chars is allowed And the adjustment entry records override_by_user_id, override_at, delta_amounts, and resulting net amounts And dashboard and exports reflect both original and adjustment entries and show net values per grouping And attempts to submit an override without a valid reason_code are rejected with a validation error
Policy Version Locking and Evidence
Given policies are versioned and visible at booking time When a booking is created Then the policy_version_id and version_label are locked on the booking record And when a later cancellation triggers forfeiture evaluation Then the system uses the locked policy version to select the tier regardless of newer policy versions And the ledger entry includes policy_version_id, version_label, and a URL to an immutable policy snapshot artifact And the applied tier, thresholds, and selection rationale are captured in calculation_details
Dispute Recording and Communication Artifacts
Given a client disputes a forfeiture charge When staff records a dispute against a ledger entry Then the system creates a dispute record with dispute_id, reason, channel, status, opened_at, and links it to the ledger entry And the ledger entry stores references to communication artifacts (notification_id, type, sent_at, content_hash) for booking confirmation, reminders, cancellation, and charge notices And when the dispute status changes (won/lost/refunded/partial) Then the dashboard Dispute Rate and refund totals update within 5 minutes and exports include the final status And all artifact content is immutable with SHA-256 hashes and is retrievable from the audit view
Payout Reconciliation by Period
Given payout batches exist from the payment processor When a user selects a payout_id or payout period and currency Then the system lists all included ledger entries and shows totals for captured forfeiture amounts minus refunds that equal the payout forfeiture component within ±0.01 And any variance is itemized by entry_id and reason (e.g., pending capture, chargeback, processor fee timing) And reconciliation is performed per currency with no FX conversion applied And a reconciliation CSV including payout_id, entry_ids, gross_captured, refunds, net, and variance is downloadable

Deposit Clarity

Upfront, localized policy microcopy at checkout and in reminders: exact deposit amount, when it’s released, and what triggers capture. Includes a pre-class “Still coming?” nudge to reduce no‑shows and support tickets.

Requirements

Localized Deposit Microcopy at Checkout
"As a student booking a class, I want deposit terms clearly shown at checkout so that I understand exactly what I will be charged and under what conditions."
Description

Surface concise, localized deposit terms on the checkout page, showing the exact deposit amount in the user’s currency, when it will be released, and what behavior triggers capture (e.g., late cancel, no‑show). Pulls from the merchant’s deposit policy and formats time and currency according to the shopper’s locale and class location time zone. Content adapts to booking context (single class vs multi-pass), supports A/B-tested phrasing, and renders consistently across mobile and web. Includes accessibility-compliant text, fallbacks for missing translations, and guards to keep copy in sync with policy revisions. Updates in real time as options change (e.g., switching class or participant count) and provides a clear link to the full policy.

Acceptance Criteria
Localized single-class deposit terms
Given the shopper locale is set (e.g., fr-FR), currency is derived (e.g., EUR), class location time zone is known, and the merchant policy defines deposit amount, release timing, and capture triggers When the shopper views checkout for a single class Then the microcopy displays the computed deposit amount in the shopper’s currency formatted per locale (e.g., "12,50 €") And the release timing is expressed in the class location time zone with locale-appropriate date/time format And capture triggers (e.g., late cancel, no‑show) are explicitly listed and sourced from the active policy And the content remains ≤ 240 characters without truncation or overflow And rendering is consistent across mobile (≤ 375px) and desktop (≥ 1024px) And values (amount, timing, triggers) match the backend policy preview API response
Context adaptation for multi-pass purchases
Given the shopper is purchasing a multi-pass (e.g., 5-session pass) and the merchant policy defines whether the deposit is per session or per pass, with release timing per attended session When the shopper views checkout for the multi-pass Then the microcopy states the scope clearly (e.g., "per session in pass" or "per pass") And it shows the computed deposit amount accordingly (per-session amount and, if applicable, total potential hold) And wording avoids single-class terminology (no references to "this class") And release timing is described relative to each session in the class location time zone And displayed values match the backend policy preview API for multi-pass context
Real-time microcopy updates on option changes
Given the shopper changes class selection, date/time, add-ons, or participant count When any change impacts deposit amount, release timing, or triggers Then the microcopy recalculates and updates within 200 ms without a page reload And the displayed amount equals the recalculated preview API value within 1 minor currency unit And no stale or contradictory values remain visible after the update And the update is announced via aria-live="polite" for assistive technologies
A/B-tested phrasing selection and tracking
Given an experiment service assigns variant A or B before microcopy render When checkout loads Then the microcopy uses the string keys for the assigned variant without flicker And the assigned variant remains consistent across the session and refreshes And analytics logs impression and interactions (e.g., policy link clicks) with experiment and variant IDs And if assignment fails, the control variant (A) is used and a recoverable error is logged
Fallback behavior for missing translations
Given the shopper’s locale lacks one or more translation keys When the microcopy renders Then the system falls back in this order: shopper locale -> merchant default locale -> en-US And currency/number formatting still uses the shopper’s locale when available; otherwise merchant default And no placeholder tokens or raw keys are shown to the shopper And a non-blocking telemetry event records the missing keys and selected fallback path
Policy revision synchronization guards
Given the merchant updates the deposit policy while the shopper is on checkout When the active policy version changes Then the microcopy refreshes to the new version within 5 seconds And cached or client-stored content from older versions is not displayed And the rendered microcopy includes the current policy version ID (for QA/analytics) and the policy link carries this version as a parameter And analytics logs the prior and current policy version IDs
Clear link to full deposit policy
Given the deposit microcopy is displayed When the shopper needs more detail Then a clearly labeled, localized link to the full deposit policy is rendered adjacent to the microcopy And the link is keyboard-focusable, meets WCAG AA contrast, and has descriptive text (no "click here") And following the link opens the policy in a modal or new tab per platform standard and records a click event And the destination URL resolves to the current merchant policy version and locale
Configurable Deposit Policy Settings
"As a studio owner, I want to configure deposit rules per class and account so that my policy matches my business and local regulations."
Description

Provide an admin UI and API for instructors/studios to define deposit rules: fixed amount or percentage, min/max caps, eligible class types, cancellation grace window, no‑show definition, and release timing. Allow account-wide defaults with per-class overrides and effective-date versioning. Include live preview of localized microcopy, validation (e.g., percentage cannot exceed 100%), and guardrails for compliance by region. Changes should be audit-logged and propagated to checkout and notifications in near real time. Support drafting and publishing states to avoid abrupt policy shifts mid-sale.

Acceptance Criteria
Admin UI: Fixed Deposit, Per-Class Override, Live Preview
Given account locale en-US and currency USD And account default deposit rule set to Fixed = 10.00, GraceWindow = 12h, NoShow = not checked-in within 15m of start, ReleaseTiming = at check-in When admin saves Draft version v1 Then Live Preview includes: deposit amount $10.00; release timing at check-in; capture trigger no-show (15m); cancellation window 12h before start, all formatted in en-US When admin creates Class "Vinyasa 7pm" with no override Then the resolved deposit policy for that class is account default v1 When admin adds a per-class override Fixed = 5.00 and Publishes effective Immediately Then Class "Vinyasa 7pm" uses the override ($5.00) while other classes continue using the account default ($10.00)
Percentage Deposit Caps, Validation, and Rounding
Given class price = $120.00 USD And account default deposit rule set to Percentage = 25%, MinCap = $10.00, MaxCap = $20.00 When policy is Published Then computed deposit equals $20.00 after rounding to $0.01 (min(max(120*0.25,10),20)) And validation blocks Percentage > 100% with error code POLICY_PERCENT_EXCEEDS_MAX And validation blocks MinCap > MaxCap with error code POLICY_CAPS_INVALID And for a $30.00 class, computed deposit equals $10.00 And for a $0.00 class, computed deposit equals $0.00
Effective-Date Versioning and In-Flight Checkout Protection
Given published policy v1 is active now And draft policy v2 has EffectiveAt = 2025-10-01T12:00:00Z And a buyer starts checkout at 2025-10-01T11:59:30Z When v2 auto-publishes at EffectiveAt Then the in-flight checkout continues using v1 until completion or 24h timeout, whichever occurs first And new checkouts started at or after 2025-10-01T12:00:00Z use v2 And the effective policy switch propagates across checkout and notifications within 60 seconds
Regional Compliance Guardrails Enforcement
Given account region profile "R-Strict" with constraints: MaxPercentage = 20, AllowFixed = true, AllowPercentage = true, AllowNoShowCapture = false When admin attempts to save Percentage = 30% with NoShowCapture = true Then save is blocked with HTTP 422 and error codes ["POLICY_PERCENT_EXCEEDS_REGION_MAX","POLICY_NOSHOW_CAPTURE_NOT_ALLOWED"], and Publish is disabled When admin adjusts Percentage = 20% and NoShowCapture = false Then save succeeds and Draft version vN is created
Near-Real-Time Propagation to Checkout and Notifications
Given a class scheduled for 2025-10-02T18:00:00-04:00 in timezone America/New_York And policy v2 is Published effective now with Fixed = $15.00, GraceWindow = 6h, NoShow = no check-in by start, ReleaseTiming = at check-in When a buyer opens checkout after publish Then the checkout microcopy displays deposit $15.00, release timing, capture trigger, and cancellation window consistent with v2 within 60 seconds And the pre-class "Still coming?" nudge scheduled 24h before start includes the same deposit details with all times shown in America/New_York
Audit Logging for Policy Lifecycle Events
Given user owner@studio.com updates Percentage from 20% to 25% and GraceWindow from 6h to 12h and clicks Publish Then an audit record is written with actor, UTC timestamp, action = publish, versionId, effectiveAt, scope (account or class id), and before/after diffs for changed fields And the audit record is immutable (cannot be edited or deleted) and is retrievable via UI and API And the audit entry includes request metadata (IP and User-Agent)
API CRUD and Optimistic Concurrency for Deposit Policies
Given API client with scopes policy:write and policy:read When client POSTs /v1/deposit-policies with Idempotency-Key = abc123 and valid body Then API returns 201 Created with versionId, status = draft, and persisted fields When client PUTs the same resource with If-Match: ETag v3 but the current ETag is v4 Then API returns 409 Conflict with error code VERSION_MISMATCH And GET /v1/deposit-policies?scope=class:{classId}&locale=fr-CA returns the resolved policy (override else default) with effectiveAt, status, and localized preview strings in fr-CA with CAD currency formatting
Deposit Hold and Capture Integration
"As a studio owner, I want deposits to be held and captured automatically according to my policy so that I reduce no‑shows without manual payment work."
Description

Integrate with supported payment gateways to securely place a deposit hold or store a payment method for later capture according to policy. Implement idempotent creation, capture, and release flows with webhook handling and retries. Manage hold expiration windows and fallbacks (e.g., tokenized off-session capture if holds cannot be extended). Support partial and multi-seat bookings, refunds for mistaken captures, and reconciliation with ClassNest’s ledger. All operations must be PCI-compliant, logged, and traceable to the booking and policy version in effect at checkout.

Acceptance Criteria
Idempotent Deposit Hold Creation Tied to Policy Version
Given a booking (booking_id) with deposit policy version (policy_version_id) and amount A in currency C When the backend requests a deposit hold or payment-method storage using idempotency key K Then exactly one gateway authorization (hold) or one payment-method token is created according to policy and gateway capability And subsequent requests with the same K return the same gateway transaction_id/token and do not create additional authorizations And the authorization/token is linked to booking_id, customer_id, and policy_version_id in persistence And the operation completes within 3 seconds at P95 with clear success/failure codes And no PAN/CVV is stored or logged; only gateway tokens and last4 are persisted and logs are redacted And an audit log entry is written with timestamp, actor=system, request_id, idempotency_key, and gateway response
Policy-Driven Capture and Release on Attendance Outcomes
Given an active deposit hold or stored payment token for a booking governed by policy P When an outcome event occurs (Attended, No-Show, Late Cancel) that per policy triggers capture or release Then the system executes capture or release within 2 minutes at P95 and records gateway transaction_id And capture amount equals the policy-defined deposit amount for the affected seat(s) and currency And on release, the hold is voided/uncaptured; on capture, remaining hold (if any) is released And off-session capture via token is attempted if no active hold exists; on SCA-required or hard-decline, status=payment_action_required and the customer is notified And duplicate outcome events are handled idempotently without double capture/release And audit and ledger entries are created referencing booking_id and policy_version_id
Hold Expiration Monitoring and Fallback to Tokenized Capture
Given a gateway hold with expiration at t_exp before policy capture decision time When t_now reaches t_exp - 10 minutes Then the system attempts to extend the hold if supported by the gateway and logs the result And if extension is not supported or rejected, the system ensures a valid stored payment token exists for off-session capture And if neither extension nor token is available, the system flags capture_risk, prompts the customer to re-authorize, and escalates to support And no deposit is lost silently: if the hold expires before capture, either capture occurred beforehand or off-session capture is attempted with retries (3 attempts over 24 hours with exponential backoff) And all transitions are idempotent and fully logged with gateway responses
Multi-Seat Booking and Partial Capture Allocation
Given a multi-seat booking of N seats with per-seat deposit rules defined by policy P When a subset of seats S are marked No-Show/Late Cancel and the remainder are Attended Then the system captures exactly the sum of deposit amounts for seats in S and releases deposits for seats not in S And per-seat state is persisted and visible in the booking timeline and export And ledger entries are created per seat to allow reconciliation by seat and policy_version_id And any seat transfer or partial cancellation before the penalty cutoff updates the capture amount accordingly, idempotently
Mistaken Capture Refund and Reversal
Given a captured deposit for booking_id B When a staff user with refund permissions submits a refund with reason=mistaken_capture and idempotency key K (full or partial) Then the system submits a refund to the original payment method within 1 minute and records gateway refund_id And duplicate refund submissions with the same K do not create additional refunds And the ledger posts a reversing entry that nets the original capture to zero for the refunded amount and links to refund_id And the booking timeline shows the refund with actor, timestamp, amount, and reason And audit logs include request_id, idempotency_key, and gateway responses And if the refund fails, the status reflects failure with actionable error and automatic retry for transient errors (up to 24 hours)
Secure Webhook Processing with Idempotency and Replay Protection
Given gateway webhooks for authorization/capture/release/refund/expiration events When a webhook is received Then the signature is verified and the timestamp is within an acceptable skew (<=5 minutes) And events are deduplicated by gateway event_id for at least 7 days and processed idempotently And event processing completes within 60 seconds at P95, with retries for transient errors (exponential backoff up to 24 hours) And invalid or replayed events are rejected with 4xx and logged to a dead-letter queue for review And booking, ledger, and audit records are updated consistently with the event outcome
Ledger Reconciliation and Audit Traceability
Given any deposit lifecycle operation (create hold, extend, capture, release, refund) When the operation completes or a webhook confirms the outcome Then exactly one corresponding ledger entry (or balanced set) is recorded with booking_id, customer_id, policy_version_id, amount, currency, gateway_transaction_id, and operation type And nightly reconciliation matches ledger entries to gateway settlements; discrepancies are flagged within 24 hours with a reconciliation report And an admin audit view shows a complete, ordered timeline of all deposit events for the booking with masked sensitive data And data at rest uses encryption for tokens; no PAN/CVV appears in logs or exports; access is role-restricted and auditable
Capture/Release Decision Engine
"As an operations manager, I want deposit outcomes determined consistently from clear rules so that customers are treated fairly and disputes are minimized."
Description

Create a rules-driven service that determines whether to capture or release a deposit based on booking events: attendance check-in, cancellation timestamp vs grace window, and pre-class confirmation signal. The engine must be time zone aware, idempotent, and support manual overrides with reasons. Decisions generate immutable audit records and notifications to the customer and merchant. Provide simulation tools to test policy outcomes before publishing and guard against race conditions (e.g., last-minute cancel vs auto-capture).

Acceptance Criteria
On-time cancellation within grace window — deposit release
Given a booking with authorized deposit amount D in currency C and event start time T in time zone Z and a policy grace window G minutes And the cancellation timestamp Tc <= T - G (evaluated in Z) When the engine evaluates the booking Then the decision is RELEASE with reason_code=CANCELED_WITHIN_GRACE And a void request for the authorization is issued to the payment processor within 30 seconds And customer and merchant notifications are sent within 60 seconds including D, C, Tc in Z, T in Z, policy link, and reason_code And an immutable audit record is appended within 5 seconds capturing booking_id, inputs (T, Z, G, Tc), evaluated rule, decision, reason_code, actor=system, and correlation_id
No-show without timely cancellation — auto-capture
Given a booking with authorized deposit D/C, event start time T in time zone Z, attendance window W minutes after T, and grace window G minutes And there is no attendance check-in by T + W And there is no cancellation at or before T - G (evaluated in Z) When the engine evaluates after T + W Then the decision is CAPTURE with reason_code=NO_SHOW And a capture for amount D is submitted using the original authorization within 30 seconds And customer and merchant notifications are sent within 60 seconds including D, C, T in Z, policy link, and reason_code And an immutable audit record is appended within 5 seconds with booking_id, inputs (T, Z, G, W), evaluated rule, decision, and correlation_id
Pre-class confirmation signal handling
Given a booking with a pre-class prompt window N hours before T and an attendance window W minutes after T And the learner responds "Still coming" at timestamp Ts where T - Ts <= N (in Z) And the learner does not check in by T + W and did not cancel at or before T - G When the engine evaluates after T + W Then the decision is CAPTURE with reason_code=PRECONFIRM_NO_SHOW and the audit includes Ts and channel And if the learner responds "Not coming" at timestamp Tr, that Tr is treated as the cancellation timestamp for policy evaluation And notifications include the applicable reason_code and the localized timestamps
Time zone and DST aware rule evaluation
Given an event scheduled in time zone Z (which may observe DST) with start time T and grace window G And a cancellation occurs at UTC timestamp Tc_utc When the engine computes the cutoff time and compares Tc_utc to T - G Then it converts Tc_utc to Z and applies Z's offsets (including DST) to determine whether Tc <= T - G And all timestamps in audit and notifications are rendered in ISO-8601 with offset for Z And for events on DST transition days, cancellations at Z-local (T - G) are RELEASE and at Z-local (T - G + 1 minute) are CAPTURE
Idempotent and race-safe decisioning under duplicates and near-simultaneous events
Given a booking with correlation/idempotency key K and event timeline including a cancellation near the cutoff and an auto-capture timer And the engine receives duplicate and out-of-order messages for booking_id with key K (e.g., cancellation at T - G and auto-capture at T - G) When the engine processes these messages Then it produces a single deterministic outcome based on event timestamps (earlier wins) and stable tiebreakers (K ordering) and persists only that decision And exactly one financial action is executed; subsequent duplicates are no-ops And the API returns the same decision for replays with idempotent=true and no additional capture/void requests are sent And the audit log has one decision entry plus dedupe entries referencing the original correlation_id
Manual override with reason and auto-decision lockout
Given a user with role Merchant.Admin initiates an override to CAPTURE or RELEASE for a booking with an optional notes field (max 500 chars) and required reason_code When the override is submitted Then the engine records the override with actor=merchant, decision, reason_code, notes, timestamp, and correlation_id And automatic rule processing for that booking is disabled thereafter; future evaluations return decision=OVERRIDDEN with reference to the override And notifications to customer and merchant are sent within 60 seconds reflecting the override and reason And the audit entry is immutable; any attempt to edit or delete returns HTTP 403 and no changes are made
Policy simulation tool — outcome preview without side effects
Given a draft policy with parameters (G, W, N, Z) and a supplied event timeline (cancellations, confirmations, check-ins) for a booking When the simulation endpoint is called Then it returns a predicted decision (CAPTURE or RELEASE), reason_code, the evaluated rule path, and the computed cutoff and attendance windows in Z And no financial actions, notifications, or production audit entries are created; the response includes side_effects=false And the simulator accepts race-condition inputs and returns a single deterministic outcome using the same ordering rules as production
Pre-class “Still Coming?” Nudge
"As a student, I want a simple pre-class prompt to confirm or cancel so that I don’t forget and can avoid losing my deposit if I can’t attend."
Description

Send a localized SMS/email nudge before class asking the attendee to confirm or cancel with one tap. Timing is configurable per policy (e.g., 24h and/or 3h before). A confirmation updates booking intent and can influence capture thresholds; a cancellation routes through the standard flow and shows any deposit impact. Respect communication preferences, opt-outs, rate limits, and channel fallbacks. Track delivery, clicks, and responses; surface to staff in the roster view to help predict attendance.

Acceptance Criteria
Policy‑Based Nudge Timing and Scheduling
Given a class has a nudge policy configured with send offsets (e.g., 24h and/or 3h before start) and the attendee has at least one allowed channel When the class reaches each configured offset in the attendee’s local timezone Then exactly one nudge is queued and sent per configured offset per booking And no nudge is sent if the booking is cancelled, the class has started, or the attendee has opted out of all channels And total nudges per booking do not exceed the configured offsets enabled for that policy And all nudge send times are logged with the computed timezone used
One‑Tap Confirmation Updates Intent and Capture Thresholds
Given an attendee receives a nudge for an active booking When the attendee taps the Confirm link before class start and the token is valid Then the booking intent is updated to Confirmed and the response timestamp is stored And the confirmation is reflected in downstream deposit capture threshold logic within 60 seconds And the confirmation link becomes single‑use; subsequent taps show Already confirmed without changing state And the response event (channel, time, booking, attendee) is recorded
One‑Tap Cancellation via Standard Flow with Deposit Impact
Given an attendee receives a nudge for an active booking When the attendee taps the Cancel link before class start and the token is valid Then the attendee is routed through the standard cancellation flow And the flow displays localized deposit impact (amount retained/refunded, when it’s released, and capture triggers) before finalizing And on confirm, the booking status is set to Cancelled and the seat is released per policy And the response event (channel, time, booking, attendee, deposit outcome) is recorded
Preferences, Opt‑Outs, Rate Limits, and Channel Fallbacks
Given an attendee has communication preferences and possible opt‑out status When a nudge becomes due Then the system honors per‑attendee channel preferences and legal opt‑outs And if SMS is disallowed or hard‑fails, an email nudge is sent within 5 minutes (if allowed) And if all channels are disallowed/opted out, no nudge is sent and the skip is logged with reason And STOP/UNSUBSCRIBE SMS replies set SMS opt‑out and suppress future SMS nudges And retries do not exceed one attempt per configured offset and do not create duplicate sends
Delivery, Click, and Response Tracking
Given nudges are sent via SMS and/or email When delivery reports and user interactions occur Then each message record includes message ID, booking ID, attendee ID, channel, locale, send time, and delivery status (queued, sent, delivered, failed) And each link click is tracked with timestamp, channel, and outcome (viewed, confirmed, cancelled) And confirmation/cancellation responses are linked to the booking and attendee And tracking data is retained per policy and is available for reporting/analytics
Roster View Attendance Signals
Given staff opens the roster view for a class When the roster loads Then each attendee row shows latest nudge timestamp and channel, delivery status, and response state (Confirmed, Cancelled, No response) And the roster shows a predicted attendance indicator derived from response state And roster data updates within 60 seconds of new deliveries or responses
Localization and Template Integrity
Given an attendee has a preferred locale and the studio has a default locale When a nudge or its linked confirmation/cancellation page is rendered Then all microcopy and currency amounts are localized to the attendee’s locale; otherwise fallback to the studio default And dynamic placeholders (class name, start time in attendee timezone, deposit amount/policy) render with correct values And templates validate required variables at send time so no raw placeholders or missing values are sent
Policy Details in Reminders and Receipts
"As a student, I want deposit terms reiterated in my reminders and receipts so that there are no surprises about charges."
Description

Embed a concise, consistent policy snippet in booking confirmations, reminder emails/SMS, cancellation confirmations, and post-class receipts. The snippet includes deposit amount, capture triggers, and release timing, localized for language, currency, and time zone. Use a single templating source of truth to avoid drift with checkout copy. Provide deep links to the full policy and support context-aware phrasing (e.g., after a late cancel, state the captured amount).

Acceptance Criteria
Policy snippet in booking confirmation (email and SMS)
Given a booking with deposit amount D in currency C, customer locale L, and time zone Z When the booking confirmation email and SMS are sent Then both messages include a policy snippet that states the exact deposit amount formatted per L/C, the capture triggers, and the release timing expressed in Z And both include a deep link to the full policy with bookingId and policyId query parameters And the SMS snippet length is <= 320 characters And the snippet values match the checkout copy by rendering from the shared template source
Policy snippet in pre-class reminder
Given a reminder scheduled R hours before class start for a booking with a deposit When the reminder email and SMS are sent Then each contains the localized policy snippet with deposit amount D, capture triggers, and release timing computed relative to class start in Z And each includes the deep link to the full policy And the SMS snippet length is <= 320 characters And the snippet text matches the shared template source used at checkout
Cancellation confirmation reflects actual capture outcome
Given a customer cancels a booking When the cancellation confirmation is sent Then if the cancel is after the cutoff that triggers capture, the message states the captured amount D (formatted per L/C) and the capture reason And if the cancel is before the cutoff, the message states that the deposit will be released and shows the expected release date/time in Z And all variants include the deep link to the full policy And amounts and outcomes match the booking’s ledger events
Post-class receipt shows deposit release or capture
Given a class ends and the booking is marked Attended or No-show When the post-class receipt is sent Then if Attended with no capture condition, the receipt states that the deposit is released (or the exact release schedule) with a timestamp in Z And if No-show or other capture condition applies, the receipt states the captured amount D (formatted per L/C) and the reason And the receipt includes the deep link to the full policy And amounts and timestamps match the payment processor records within 1 minute
Single source-of-truth templating prevents drift
Given the policy microcopy template version vN used at checkout When notifications (confirmation, reminder, cancellation, receipt) are rendered for the same policy Then all notifications reference template version vN and output identical deposit amount, capture triggers, and release timing text fragments as checkout And updating the template to vN+1 updates all surfaces within 10 minutes And automated tests detect zero diffs for the snippet across channels
Localization, currency, and time zone correctness and fallbacks
Given users with locales and environments: fr-FR/EUR/Europe-Paris, en-GB/GBP/Europe-London, pt-BR/BRL/America-Sao_Paulo, de-DE/EUR/Europe-Berlin, en-US/USD/America-Los_Angeles, and an unsupported locale nl-NL/EUR/Europe-Amsterdam When each message type is sent Then currency is formatted with the correct symbol, grouping, and decimal separator for the locale And local dates/times include the correct time zone abbreviation or offset And language is translated from the i18n dictionary with no English fallback for supported locales And for the unsupported locale, fallback is en-US language and ISO currency code with unambiguous formatting And the deep link domain and path remain consistent across locales
Deposit Analytics and Audit Trail
"As a studio owner, I want clear reporting and an audit trail of deposit decisions so that I can optimize policies and resolve customer questions with evidence."
Description

Deliver dashboards and exports showing deposit holds, captures, releases, late cancels, no-shows, and conversion impact from the nudge. Include per-class and per-instructor breakdowns, trends over time, and benchmarks. Maintain an immutable event log linking each decision to the booking, timestamps, user actions, gateway responses, and policy version. Provide search and filters for support agents to resolve disputes quickly.

Acceptance Criteria
Deposits Dashboard Shows Accurate Metrics and Trends
Given an admin selects a date range and organization timezone, When the Deposits Overview loads, Then tiles show counts for holds, captures, releases, late cancels, and no-shows and totals match the event log for that range. Given a payment gateway webhook or booking status change occurs, When 5 minutes have elapsed, Then the dashboard reflects the new data in tiles and charts. Given the user switches trend granularity between week and month, When charts render, Then time buckets align to the selected timezone and tooltips show counts and rates per bucket. Given filters for class, instructor, location, payment method, and policy version are applied, When the view updates, Then all metrics and charts reflect the filters and display active filter chips. Given up to 12 months of data and <=100k events, When loading the overview, Then first render completes in <=2s p95 and interactive filter changes complete in <=500ms p95.
Breakdowns and Drilldowns by Class and Instructor
Given a metric tile is visible, When the user clicks View breakdown, Then a table grouped by Class and Instructor appears with counts, rates, and revenue columns that are sortable and paginated. Given multiple classes or instructors share the same display name, When the table renders, Then grouping keys include class_id and instructor_id to prevent conflation. Given a specific row in the breakdown, When the user clicks the row, Then the event log opens pre-filtered to that class/instructor and date range. Given the user exports from the breakdown view, When CSV is generated, Then it contains the current filters, groupings, and a totals row matching the UI. Given a breakdown request over <=50k grouped rows, When sorting or paging, Then response time is <=800ms p95.
Nudge Conversion Impact Measurement
Given classes where a pre-class nudge was sent, When viewing Nudge Impact for a selected date range, Then the panel shows Nudges Sent, Confirmed After Nudge, No-Show Rate (With Nudge), No-Show Rate (Without Nudge), Delta (pp), and Revenue Retained with sample sizes. Given the attribution window is set to 0–12 hours after nudge, When a booking confirms within that window, Then it is counted in Confirmed After Nudge; confirmations outside the window are excluded. Given the user switches baseline between Prior 30 Days Same Instructor/Time Bucket and Control Cohort, When toggled, Then all nudge metrics recompute and labels reflect the selected baseline. Given a metric in the Nudge Impact panel is clicked, When drilldown opens, Then the event log is filtered to the corresponding cohort and attribution events. Given a class did not send a nudge, When computing With Nudge metrics, Then that class is excluded from the With Nudge cohort.
Immutable Deposit Event Log
Given an event is opened in the event log, When details are displayed, Then fields include booking_id, event_type, event_id, occurred_at (ISO-8601 UTC), actor_type, actor_id/null, gateway_transaction_id, gateway_response_code, gateway_response_summary, policy_version_id, policy_snapshot_hash, reason, and links to related events. Given an attempt is made to update or delete an existing event via UI or API, When the request is processed, Then the system rejects the change with HTTP 409 and creates a new event_mutation_denied audit event; the original event remains unchanged. Given a sequence of events for a booking, When integrity verification runs, Then each event contains previous_event_hash and the hash chain validates; any mismatch flags timeline_tamper_detected on the booking. Given the event log is filtered by booking_id, date range, event_type, instructor, class, gateway_transaction_id, or policy_version_id, When results load, Then only matching events appear and p95 response time is <=800ms for up to 50k events. Given gateway webhook payloads include sensitive fields, When shown in the UI, Then PII is redacted according to policy while retaining gateway_response_code and summary.
Support Agent Dispute Resolution via Search and Filters
Given a customer disputes a deposit capture, When a support agent searches by booking code, email, phone, or last 4 of card, Then the matching booking appears as the top result within 1s p95. Given the agent opens the booking timeline, When details render, Then the view shows policy_version_id, policy cutoff timestamps, the trigger for capture (no-show or late cancel), event timestamps, and gateway response details. Given the agent applies the Disputed Captures (Last 30 Days) preset, When results display, Then only relevant events appear and summary totals match the filtered set. Given the agent role has restricted PII access, When viewing booking and event details, Then masked fields are obscured and an access audit entry is recorded. Given the agent clicks Generate Case Packet, When processing completes, Then a downloadable file containing the event log, policy snapshot, timestamps, and gateway receipts is available within 30 seconds and retained for 7 days.
Data Export and Schema Consistency
Given an export of Deposit Events is requested for a date range, When the export completes, Then CSV and JSON files contain exactly the number of rows matching the filters and include a header with column names and data types. Given timestamp fields are exported, When inspecting the file, Then all timestamps are UTC ISO-8601 with Z suffix and an additional organization_timezone_offset_at_event column is present. Given monetary fields are exported, When inspecting, Then amounts are in minor units and each row includes a currency_code. Given an export over 200k rows is requested, When processing, Then the job runs asynchronously, sends completion notification, and finishes within 10 minutes p95. Given the export schema changes, When a new version is deployed, Then files include schema_version and the API/UI changelog link; older versions remain accessible for 90 days.
Benchmarks and Peer Comparisons
Given the Benchmarks tab is opened, When metrics render, Then the organization’s rates (no-show, late cancel, capture, release) display alongside peer percentile bands (p25, p50, p75) for the selected category and region with cohort sample size. Given the user toggles Exclude my org from benchmarks, When toggled off, Then the organization’s data is excluded from aggregate calculations within 24 hours and an exclusion indicator appears. Given the user changes category or region filters, When applied, Then percentile bands recompute and labels reflect the selected cohort definition. Given the peer cohort sample size is less than 30 organizations, When detected, Then percentile bands are hidden and a Not enough data state is shown for those benchmarks. Given the user hovers a benchmark metric, When the tooltip appears, Then it displays the exact formula definition used for the metric.

Chargeback Shield

Automatically compiles a dispute-ready evidence packet—timestamps, policy acceptance, reminder logs, and attendance status—plus clear statement descriptors for deposit holds. Lowers chargeback losses and saves admin time.

Requirements

Unified Evidence Data Capture
"As a studio owner, I want ClassNest to automatically record and link all chargeback-relevant evidence so that I don’t have to manually gather proof when a dispute occurs."
Description

Automatically capture and normalize all dispute-relevant signals across the booking lifecycle: booking timestamps, client IP/device fingerprint, policy display and acceptance, payment authorization/settlement IDs, invoice totals and deposit flags, reminder send logs (SMS/email with delivery/open status), waitlist auto-offer logs, cancellation/changes, and attendance check-ins. Store in a tamper-evident, queryable data model linked to booking and payment records with UTC timestamps and source attribution. Expose via internal service APIs for packet generation and reporting.

Acceptance Criteria
Capture Booking Creation Evidence
Given a client completes a booking via web or mobile When the booking is created Then the evidence store writes a record linked to booking_id containing: booking_created_at (UTC ISO-8601 with Z), client_ip, device_fingerprint_id, user_agent, policy_version_id displayed, policy_acceptance=true/false with accepted_at (UTC) and acceptance_ip, and source_channel And querying evidence by booking_id returns these fields with non-null timestamps and source_channel And all timestamps are stored in UTC and include timezone designator 'Z' And the write is confirmed durable before booking confirmation is returned
Normalize Payment Event Evidence
Given payment authorization and settlement events occur for a booking When the payment gateway webhook is received Then the system appends a normalized evidence record with: payment_provider, authorization_id, settlement_id (or charge_id), amount_total_minor_units, currency, deposit_flag (true/false), invoice_id, booking_id, payment_status, event_received_at (UTC), source='webhook' And duplicate webhooks for the same provider event_id are idempotently ignored (no duplicate evidence records) And amounts are stored as integer minor units and currency as ISO 4217 code And evidence is queryable by booking_id and invoice_id and returns exactly one record per unique provider event
Reminder Delivery and Open Logs
Given SMS and/or email reminders are scheduled for a booking When a reminder is sent Then an evidence record is created with: channel (sms|email), template_id, provider_message_id, scheduled_at (UTC), sent_at (UTC), delivery_status (queued|sent|delivered|failed), delivery_at (UTC if available), open_at (UTC for email if tracked), recipient_masked, booking_id, source='notification-service' And when provider callbacks arrive, new evidence status events are appended without overwriting prior records, preserving a full audit trail And querying by booking_id returns all reminder-related events ordered by timestamp ascending
Waitlist Auto-Offer Evidence
Given a client on a waitlist becomes eligible for an opening When the auto-offer is sent Then the evidence store records: offer_id, waitlist_entry_id, class_id, booking_id (if created), sent_at (UTC), expiry_at (UTC), offer_channel, accepted_at/rejected_at (UTC if applicable), status (sent|accepted|rejected|lapsed), auto_offer_engine_version, actor='system' And if no response occurs by expiry_at, a status=lapsed event is appended at expiry And all offer events are retrievable by waitlist_entry_id and class_id
Cancellation and Change Audit Trail
Given a booking is modified or cancelled When a change event is applied Then an evidence change record is appended with: change_id, booking_id, change_type (reschedule|cancel|update_participant|apply_credit|note), reason_code (enum or free_text), actor_type (client|staff|system), actor_id (if staff), changed_at (UTC), previous_values, new_values, policy_cutoff_at (UTC) and late_cancel_evaluation (before_cutoff|after_cutoff) when relevant And no evidence record is deleted; superseded values remain readable for audit And querying by booking_id returns a complete chronological sequence of all change events
Attendance Check-in Evidence
Given a session occurs for a booking When attendance is recorded or updated Then an evidence record is written per attendee with: attendance_status (present|no_show|late_cancel), check_in_method (instructor|kiosk|self_link|auto), check_in_at (UTC), recorded_by (user_id or system), location_geo (optional), notes, booking_id And if attendance_status changes, a new versioned record is appended with version increment and updated_at (UTC), preserving prior versions And querying by booking_id returns the latest status plus full version history on request
Tamper-Evident Storage and Evidence API
Given evidence records are written to storage When they are persisted Then each record contains content_hash (SHA-256 over canonical payload) and prev_hash to form a hash chain per booking_id; original records are immutable And any mutation results in a new version with version_id and prev_version_id; attempts to alter historical records are rejected and logged And the internal Evidence API provides: GET /internal/evidence?bookingId={id} returning all records within 300 ms for up to 200 records; POST /internal/packets generating a dispute packet JSON including all linked evidence And API requests require service-to-service authentication (mTLS or signed token) and return 403 for unauthorized callers And all API responses include server_generated_at (UTC) and per-record source attribution
Dispute Packet Auto-Assembly & Submission
"As a studio owner, I want one-click compilation and submission of a dispute packet so that I can respond quickly and consistently with all required proof."
Description

Compile processor- and network-ready dispute evidence packets that include a chronological timeline, policy acceptance proofs, reminder logs, attendance status, descriptor details, and deposit hold rationale. Generate structured JSON and a human-readable PDF with embedded snapshots/screenshots where applicable. Maintain templates by dispute reason code and locale, with versioning. Support auto-submission via payment provider APIs and offer a downloadable packet for manual submission.

Acceptance Criteria
Auto-Assembly Completeness and Evidence Coverage
Given a dispute event is received for a paid booking When the auto-assembly job runs for the dispute Then the packet includes a chronological timeline of booking creation, payment authorization or capture, policy acceptance, reminders sent by SMS and email, attendance status, and any refund or deposit hold actions with timestamps and sources And each timeline entry includes ISO 8601 UTC and venue-local timestamps, actor or source, and unique reference IDs And the packet includes policy acceptance proof with IP, user agent, timestamp, and accepted policy version And the packet includes the statement descriptor used and the deposit hold rationale text And all required artifacts for the selected reason code are present; otherwise the packet is marked Incomplete with a machine-readable missing_artifacts list and auto-submission is disabled
Template Selection by Reason Code and Locale with Version Pinning
Given a dispute has a reason code and a customer locale When the packet is assembled Then the system selects the matching template for the reason code and locale, or falls back to the default locale template when a localized template is unavailable And the selected template version ID is recorded in the packet metadata And subsequent re-generation for the same dispute uses the same pinned template version unless an admin override is specified
Provider-Ready JSON Generation and Validation
Given a selected payment provider for the disputed charge When the packet JSON is generated Then the JSON conforms to the stored JSON Schema for that provider and template version and passes validation with no errors And the JSON includes dispute_id, charge_id, reason_code, template_version, locale, timeline, policy_acceptance, reminders, attendance, descriptor, deposit_hold_rationale, and attachments And the JSON and attachment references comply with the provider’s configured evidence limits And a schema_version is recorded in the JSON for downstream verification
Human-Readable PDF Rendering with Embedded Evidence
Given a complete packet is generated When the PDF is rendered Then the PDF contains a cover page with dispute identifiers and summary, a chronological timeline section, policy acceptance snapshots, reminder logs, attendance status, descriptor details, and deposit hold rationale And embedded images or screenshots are legible at a minimum of 150 DPI and labeled with timestamps and sources And the PDF file size does not exceed the configured max_evidence_file_size for the payment provider and includes a table of contents and page numbers And the PDF passes an automated readability check with no missing fonts and selectable text for core sections
Auto-Submission via Payment Provider API with Idempotency and Retry
Given auto-submission is enabled for the organization and the packet status is Complete When a dispute event is processed Then the system submits the packet via the payment provider API within 5 minutes and records the provider submission ID and submission timestamp And requests are idempotent using a deterministic key per dispute And transient failures are retried up to 3 times with exponential backoff, after which status is set to Failed with captured error details And on success the dispute record status is updated to Submitted and a confirmation notification is sent to the organization owner
Manual Downloadable Packet with Access Controls and Packaging
Given a user with Disputes:Download permission views a dispute When they request a downloadable packet Then the system generates a ZIP containing the JSON, the PDF, and an attachments manifest, named <disputeId>_<reason>_<locale>_v<templateVersion>.zip And a pre-signed download link is created that expires in 24 hours and is scoped to the requester’s organization And the ZIP includes a SHA-256 checksum file and remains under the configured max_download_size And all download events are logged with user ID, IP, and timestamp
Audit Trail, Immutability, and Reproducibility
Given a packet has been submitted or downloaded When an auditor reviews the dispute record Then the record displays template_version, schema_version, packet_hash, submission_id if present, submitted_at, submitted_by, submission_method, and evidence completeness status And once submitted, the evidence packet is immutable and re-generation creates a new version linked to the original with a new packet_hash and change rationale And re-generating without underlying data changes produces an identical packet_hash for the same template version
Dynamic Statement Descriptors
"As a business owner, I want clear, compliant statement descriptors so that customers recognize charges and are less likely to file chargebacks."
Description

Provide configurable, compliant statement and soft descriptors per business, class, and booking type (e.g., deposit hold vs full charge) with dynamic fields (business name, class name, city, URL/phone). Enforce network/provider limits and character rules with validation and fallbacks. Preview expected cardholder rendering and propagate descriptors to receipts and evidence packets for consistency.

Acceptance Criteria
Per-Entity Descriptor Configuration (Business/Class/Booking Type)
Given an admin with Billing permissions is editing payment settings When they create a business default descriptor template using placeholders {business_name}, {class_name}, {city}, {url}, {phone} And they add a class-level override for a specific class And they add booking-type-specific templates for Deposit Hold and Full Charge Then the system saves all templates successfully And at transaction time the most specific applicable template is applied in priority order: booking type > class > business default And an audit log entry is recorded for each create/update with user, timestamp, and before/after values
Descriptor Validation Against Network/Provider Rules
Given I am entering a descriptor template in the admin UI When my input exceeds a network/provider max length or uses disallowed characters Then the UI blocks save, highlights offending segments, and shows per-network remaining character counts And the API rejects noncompliant templates with a 422 response including field, rule, and limit metadata And when the template conforms to the active network/provider rules, the UI allows save and the API returns 200
Dynamic Field Resolution and Fallbacks at Charge Time
Given a booking with business_name, class_name, city, url, and phone values exists When an authorization or charge is created Then placeholders in the selected template are resolved from booking data And disallowed characters are removed and casing normalized per provider requirements And if a field is missing, the descriptor falls back to business_name only And if the chosen template is empty/unavailable, a provider-safe default "<BusinessName> <City>" is used And the final resolved descriptor fits within provider/network limits via safe truncation and is stored with the transaction record
Cardholder Rendering Preview (Networks and Provider)
Given an admin is editing a descriptor template When they open the preview panel Then the UI shows live, side-by-side previews for Statement and Soft descriptors for major card networks and the connected provider And each preview applies network-specific truncation, character set, and formatting rules And the preview updates in real time as the template changes and clearly indicates any omitted fields due to limits
Propagation to Receipts and Dispute Evidence
Given a payment has been authorized or captured When customer receipts (email/SMS/PDF) are generated Then the exact resolved descriptor used on the transaction appears on the receipt And if a dispute is opened, the evidence packet automatically includes the resolved descriptor(s) with timestamps and policy acceptance references And the descriptor values match across gateway metadata, receipts, and evidence artifacts
Deposit Hold vs Full Charge Descriptor Mapping
Given booking types are configured with separate Deposit Hold and Full Charge templates When a deposit authorization is created Then the soft descriptor is populated from the Deposit Hold template and sent to the provider And when a full charge or capture occurs, the statement descriptor is populated from the Full Charge template and sent to the provider And audit logs record which template and values were used for each event
Provider Rejection Handling and Admin Alerts
Given the provider rejects a descriptor at authorization or capture due to compliance rules When the provider returns a descriptor-related error Then the system retries the operation once using a compliant default descriptor And the transaction proceeds without user intervention when possible And an admin alert is generated with the error details and fallback used, and the settings UI flags the offending template as noncompliant
Chargeback Alerts & SLA Tracker
"As an operations manager, I want real-time chargeback alerts and deadline tracking so that we always respond within SLA and maximize win rates."
Description

Integrate payment provider webhooks and alert feeds to ingest disputes in real time, open internal cases, attach linked evidence, and calculate response deadlines per network SLA. Provide timers, reminders, and escalation rules, and optionally trigger auto-assembly and submission based on configurable criteria. Track lifecycle states (needs response/submitted/won/lost) and reason codes for analytics.

Acceptance Criteria
Real-time Dispute Ingestion via Webhook
Given a valid payment provider webhook for a new dispute with unique dispute_id and payment_id When the webhook is received Then an internal dispute case is created within 60 seconds with status "needs_response" and linked to the payment_id And the source is recorded as "webhook" and the raw payload is stored with a normalized dispute record (amount, currency, network, reason_code, created_at) And repeat deliveries of the same webhook (same provider + dispute_id) do not create duplicate cases And parsing or validation failures are logged and retried per backoff policy up to the configured maximum retries
Alert Feed Polling Fallback
Given a provider account that supports dispute alert polling but not webhooks When the polling job detects a new dispute not previously ingested Then an internal dispute case is created with status "needs_response" and linked to the payment_id And the source is recorded as "alert_feed" and the raw payload is stored with normalized fields And disputes already ingested via webhook or prior polls are deduplicated using (provider, dispute_id)
SLA Deadline Calculation and Countdown
Given a new dispute case with network, created_at, and merchant timezone available When SLA templates for each network are configured in the system Then the response_deadline is calculated at case creation according to the network’s SLA template and merchant timezone And a countdown timer displays days/hours remaining and updates at least every minute And warning flags and notifications are scheduled at T-72h and T-24h before the deadline
Automatic Evidence Linking on Case Open
Given a dispute case linked to a booking and payment When the case is created Then the system auto-attaches available evidence artifacts: policy acceptance record, payment authorization details, reminder SMS/email logs, attendance/check-in status, and booking metadata And each artifact is recorded with a type, timestamp, and retrieval status (attached/not_found/error) And missing artifacts are flagged for manual follow-up without blocking the case
Timed Reminders and Escalation Rules
Given a dispute case with a computed response_deadline and an assigned owner When the time reaches T-72h and T-24h Then email and in-app reminders are sent to the owner and recorded in the case timeline And if a reminder is not acknowledged within 12 hours, the case escalates to the configured escalation target (e.g., role/team channel) and logs the escalation event And all reminders and escalations are suppressed after the case state becomes "submitted" or "closed"
Auto-Assembly and Auto-Submission Based on Criteria
Given auto-submit criteria are configured (e.g., amount < $50 and reason_code in [10.4, 13.1]) and provider supports API submission When a new dispute case meets the criteria and is more than 48 hours from the deadline Then the system assembles a dispute packet including all attached evidence and a generated cover letter And the packet is submitted via provider API, the submission_id is stored, and the case state changes to "submitted" And the exact packet snapshot is stored immutably for audit And on API failure, retries occur with exponential backoff up to N attempts and the owner is notified after final failure
Lifecycle States, Outcomes, and Analytics
Given a dispute case in the system When the case is submitted, won, or lost via provider callbacks or manual updates Then the case state transitions to "submitted", "won", or "lost" accordingly with timestamp and actor/source captured And the reason_code, network, amount, time_to_submit, and days_to_outcome are recorded for analytics And all state transitions are auditable in a read-only timeline and are included in export/reporting endpoints
Versioned Policy Consent Capture
"As a studio owner, I want verifiable, time-stamped proof of customers accepting my policies so that I can demonstrate contract compliance in disputes."
Description

Implement versioned cancellation/no-show and terms policies with immutable identifiers and stored snapshots of the exact text shown at checkout and waitlist opt-in. Capture time-stamped consent with IP/device, language, and display context. Surface policy version in receipts and include the snapshot and acceptance proof in dispute packets.

Acceptance Criteria
Checkout Consent to Versioned Policies
Given a customer is on the checkout page for a paid booking When the checkout page loads Then the latest active versions of the Cancellation/No-Show Policy and Terms are displayed with immutable policy_version_id and version number, and a View Policy link to the exact snapshot And the Pay/Confirm action is disabled until a single explicit consent checkbox is checked When the customer checks the consent box and completes payment Then an immutable consent record is created and linked to the booking/transaction containing: policy_version_ids, policy_snapshot_hashes, snapshot_storage_ids, consent_timestamp (UTC ISO 8601), ip_address, user_agent, device_fingerprint, language_code, display_context="checkout", booking_id, transaction_id, consent_method="checkbox" And the consent record is read-only and retrievable by booking_id and transaction_id
Waitlist Opt-In Consent to Versioned Policies
Given a visitor opts into a class waitlist When the waitlist opt-in form/modal is displayed Then the latest active versions of applicable policies are shown with immutable policy_version_id and version number and a View Policy link to the exact snapshot And the Join Waitlist action is disabled until consent is checked When the visitor confirms opt-in Then an immutable consent record is stored with: policy_version_ids, policy_snapshot_hashes, snapshot_storage_ids, consent_timestamp (UTC), ip_address, user_agent, device_fingerprint, language_code, display_context="waitlist_opt_in", waitlist_entry_id, consent_method="checkbox" And the waitlist confirmation message/email surfaces the policy version numbers and a link to the exact snapshot accepted
Immutable Policy Snapshot Storage
Given an admin publishes a new policy version When the version is activated Then the system assigns an immutable policy_version_id, stores a read-only snapshot of the exact rendered text (HTML/PDF), computes and stores a SHA-256 hash, and prevents edits (only new versions allowed) And all historical versions remain retrievable by policy_version_id with hash verification matching the stored hash Given a consent is recorded Then the referenced snapshot_storage_id resolves to the exact snapshot and its hash equals the stored policy_snapshot_hash
Forced Re-Consent on Policy Update
Given a customer previously consented to policy version N When policy version N+1 becomes active and the customer initiates a new checkout or waitlist opt-in Then the customer is shown version N+1 and must provide new consent before proceeding And past bookings remain linked to their original consent records for version N Given a checkout page has been open more than 15 minutes and a new policy version is activated When the customer attempts to complete payment Then the system blocks payment and requires review and consent to the newly active policy version before continuing
Policy Version Surface on Receipts
Given a booking with a recorded consent When the receipt is displayed in-app or emailed Then the receipt shows the policy names with version numbers and immutable policy_version_ids and includes a link or attachment to the exact snapshot accepted And the labels are shown in the language_code captured at consent, falling back to default if unavailable And retrieving a receipt by booking_id shows these elements present
Dispute Packet Includes Snapshot and Acceptance Proof
Given a chargeback is created for a booking with a consent record When the Chargeback Shield evidence packet is generated Then the packet includes: the exact policy snapshot (PDF/HTML), acceptance timestamp (UTC), ip_address, device_fingerprint, user_agent, language_code, display_context, policy_version_ids, policy_snapshot_hashes, snapshot_storage_ids, booking_id, transaction_id And all included file links resolve and hashes verify successfully And evidence generation completes within 5 seconds for 95th percentile cases
Multi-language Policy Rendering and Capture
Given the customer’s preferred/browser language is supported for the active policy version When policies are displayed at checkout or waitlist opt-in Then the rendered text is the canonical translation for that version and language, and language_code is recorded accordingly Given a translation is not available When the policy is displayed Then the default language is rendered, language_code reflects the fallback, and the stored snapshot is of the exact language variant shown
Attendance Verification & Audit Trail
"As an instructor, I want reliable attendance verification tied to bookings so that I can prove a customer attended or a no-show occurred during chargebacks."
Description

Offer multiple attendance verification modes (instructor check-in, client QR scan, geo-fenced self check-in, offline-capable roster) and record method, actor, device, location, and timestamp. Allow attaching notes and optional photo/signature proof. Link records to bookings and include them in evidence timelines to substantiate service delivery.

Acceptance Criteria
Instructor Roster Check-In Captures Full Audit Metadata
Given an instructor is authenticated and viewing the roster for a scheduled class with booked attendees When the instructor checks in a specific booking from the roster Then the system creates one attendance record linked to that booking_id and class_id And records method="instructor_check_in", actor={user_id, role="instructor"}, device={device_id, user_agent, app_version}, location={lat, lon, accuracy_m}, ip_address, timestamp={client_recorded, server_received} And responds 201 with attendance_record_id And the record appears on the class roster and the booking timeline within 5 seconds And a second check-in for the same booking and class within the same session is rejected with 409 Duplicate and no new record is created
Client QR Code Self Check-In with Duplicate and Window Controls
Given a unique session QR is active and the client is authenticated and has a valid booking When the client scans the QR within the configured check-in window (e.g., 15 minutes before to 30 minutes after start) Then the system validates booking ownership and session, prevents reuse of the same QR token, and creates an attendance record with method="client_qr_scan" and full metadata (actor, device, location, timestamps, ip) And the UI confirms check-in within 3 seconds And if the client is not booked, outside the window, or already checked in, the system returns an error (403/422) and no attendance record is created And all failed attempts are logged with reason for audit
Geo-Fenced Self Check-In within Configured Radius and Time Window
Given a venue geofence radius (e.g., 100 m) and check-in window are configured for the session When a booked client initiates self check-in using location services Then the system verifies device location accuracy <= 100 m and distance from venue <= configured radius and time within window And upon pass, creates attendance record with method="geo_self_check_in" including precise lat/lon, accuracy_m, and timestamps And if any check fails, the system blocks check-in, displays the failure reason (out_of_radius, low_accuracy, out_of_window), and logs the attempt without creating attendance And attempts where OS denies location permission are handled with a clear prompt and no record created
Offline Check-In Queue with Reliable Sync and De-Duplication
Given the instructor device is offline and the roster is available in offline mode When the instructor marks attendees as present Then offline attendance events are stored locally with offline=true, client_recorded timestamp, device and actor metadata And upon reconnect, events sync within 60 seconds, preserving client_recorded while adding server_received timestamp And the server de-duplicates by booking_id+class_id, keeping the earliest client_recorded and marking duplicates as duplicate_discarded without creating extra records And sync failures surface retriable errors to the user and are retried with exponential backoff up to 5 times
Notes, Photo, and Signature Attachments Stored with Attendance Record
Given an instructor completes a check-in When they add an optional note (max 500 chars), up to 2 photos (JPG/PNG, each <= 5 MB), and/or a signature capture Then the attachments are associated to the attendance record with hashes (SHA-256), mime_type, size, and timestamp metadata And uploads succeed or fail independently from check-in; check-in persists even if an attachment fails And on success, attachments are retrievable via secure URLs and appear in booking timeline and evidence export And on failure, the UI displays a retry option and logs the failure reason
Immutable Audit Trail with Versioned Edits and Reason Capture
Given an existing attendance record When a permitted user edits fields (e.g., method, note) or adds/removes attachments Then the system creates a new version with version_number incremented, links to prior_version_id, requires a reason (min 10 chars), and stores actor, device, timestamp And the original version remains immutable and readable; hard delete is disallowed; voiding creates status="voided" with reason and retains all versions And the audit log lists all versions in chronological order and is exportable
Evidence Timeline Export Includes Attendance and Links to Booking
Given a dispute evidence packet is requested for a booking linked to a class When the system generates the evidence timeline Then it includes the attendance record(s) with method, actor, device, location, timestamps, and any notes/attachments, in chronological order And the packet links the attendance entries to the booking and class metadata and includes a UTC time normalization and timezone of event And the export is available as PDF and JSON and generated within 10 seconds for sessions with up to 100 attendees And the exported files have stable URLs valid for at least 7 days
PII Redaction & Retention Controls
"As a compliance lead, I want redaction and retention controls on dispute evidence so that we can contest chargebacks without violating privacy regulations."
Description

Automate redaction of sensitive PII in exported evidence while retaining verifiable hashes internally. Provide configurable retention windows by region and data type with auto-purge and legal hold support. Enforce role-based access controls and audit logs for viewing/exporting evidence to meet privacy and compliance obligations.

Acceptance Criteria
PII Redaction on Evidence Export (UI & API)
Given a dispute record containing PII fields (full_name, email, phone, street_address, IP_address, card_pan, card_token, birthdate) And an organizational redaction policy is active When a user exports an evidence packet via the UI or Export API Then the export artifacts (PDF, ZIP, JSON) must replace those PII field values with the token "[REDACTED]" except card_pan which must be masked to "**** **** **** 1234" (last4 only) And IP_address must be redacted to "x.x.x.x" And street_address must be reduced to "city, region/country" only And no unredacted occurrences of those fields appear anywhere in the files (0 matches in case-insensitive search for original values) And a metadata manifest (manifest.json) in the export lists each redacted field by path with a redaction_reason and a reference hash_id And if redaction processing fails, the export is aborted, returns HTTP 422 with error code "redaction_failed", and no file is delivered And an audit log entry is created for the export containing actor_id, role, org_id, dispute_id, timestamp_utc, export_channel, redaction_profile_id, redacted_field_count
Internal Hash Retention and Verification
Given any field is redacted in an export When the export is generated Then the system stores a SHA-256 hash of the original field value combined with a tenant-specific secret salt, keyed by field path and record_id And the original plaintext is not stored in the export or manifest And an internal Verify API endpoint "/internal/evidence/verify-hash" allows authorized roles to submit record_id, field_path, and candidate_value and receives only "match: true|false" And all verification attempts are rate limited (max 10/min per user) and fully audited And hash records follow the "hashes" retention policy and are purged on expiry or DSAR erasure
Configurable Retention Windows by Region and Data Type
Given an Organization Owner accesses Retention Settings When they set retention days for each region (EU/EEA, UK, US, Other) and data type (PII_originals, evidence_exports, hashes, audit_logs) within 0–3650 days Then inputs outside bounds are rejected with inline validation And on save, settings are versioned with effective_from timestamp and actor details And all existing records receive a recalculated purge_at according to the new version within 1 hour And a daily purge job at 02:00 UTC deletes records with purge_at <= now for each data type, writing a purge summary audit record And after the job completes, no records remain older than configured retention for that region/data type in a sample of 1000 randomly checked records
Legal Hold Overrides Auto-Purge
Given a Compliance role user creates a Legal Hold with case_id, scope (org|dispute_ids), reason, and start_at (optional end_at) When the hold is active Then all scoped records are excluded from purge regardless of retention windows and are labeled "ON_LEGAL_HOLD" And exports from held records remain redacted as per policy And attempts to purge or hard-delete held records return error code "legal_hold_active" and are logged And when the hold ends or is released by an authorized user with justification, purge_at is recomputed and eligible records are deleted in the next scheduled purge, with a closure audit entry
Role-Based Access Control for Viewing and Exporting
Given system roles are defined (Owner, Compliance, Admin, Support, Instructor, Viewer) When viewing evidence in-app Then only Owner and Compliance may view unredacted PII; all other roles see masked values And all evidence exports are always redacted for all roles And API endpoints enforce the same permissions; unauthorized access returns HTTP 403 and is audited And permission changes are effective immediately and captured in the audit log with before/after role grants
Comprehensive Audit Logging and Tamper Evidence
Given any action occurs related to PII access, export, verification, retention change, purge, or legal hold When the action completes Then an immutable audit record is written with actor_id, role, org_id, action_type, target_id, fields_affected, redaction_profile_id, result, timestamp_utc, ip, user_agent, and purpose And audit records are append-only and chained by a hash of the previous record (hash_prev) to provide tamper evidence And authorized roles can query audit logs by time range, actor, action_type, and export matching results; exported audit logs redact PII field values And audit logs follow their own configurable retention window and are excluded from user-initiated purge while legal holds are active
Region Resolution and Policy Enforcement
Given a dispute has customer_country, booking_country, and org_region When determining which regional retention/redaction policy to apply Then the system selects the region in priority order: customer_country region -> booking_country region -> org_region -> global default And the resolved region is persisted on the evidence record and included in the audit for every export and purge And for EU/EEA/UK, IP addresses are dropped (not even hashed) in exports; for other regions, IP addresses are redacted to "[REDACTED]" And all scheduled purges and redaction behaviors respect the resolved region with 0 policy violations in automated tests across sample datasets

Smart Auto-Refill

Let clients opt in to automatically top up their Wallet pass when credits drop below a chosen threshold. They pick the refill size, set a monthly spend cap, and confirm with Apple/Google Pay. Result: regulars are always ready to book, checkout friction disappears, and studios enjoy steadier cash flow with fewer empty-balance hiccups.

Requirements

Auto-Refill Enrollment & Threshold Settings
"As a returning client, I want to enable auto-refill and set when and how much to top up so that I never run out of credits when booking."
Description

Provide an opt-in flow for clients to enable Smart Auto-Refill on a specific Wallet pass. Allow users to set a low-balance threshold (credits or currency), choose a refill amount from studio-configured increments, and view/edit settings from profile, wallet, and checkout contexts. Persist settings across devices and sessions, support multiple passes per user, and localize currency/units. Include enable/disable, preview of next trigger conditions, and guardrails (e.g., minimum threshold cannot exceed refill size). Ensure seamless mobile-first UX with clear consent copy and confirmation screens.

Acceptance Criteria
Enable Auto-Refill During Checkout for a Specific Wallet Pass
Given a logged-in client is checking out a specific Wallet pass and the studio has Smart Auto-Refill enabled When the client toggles "Enable Smart Auto-Refill" Then the Threshold, Refill Amount, and Monthly Cap inputs become visible and required, and the Confirm action remains disabled until all are valid Given valid selections are made When the client confirms with Apple Pay or Google Pay Then auto-refill is enabled for that pass, a consent record (pass ID, threshold, refill amount, monthly cap, payment method token, timestamp, IP) is stored, and a success confirmation screen is displayed within 2 seconds Given auto-refill is enabled via checkout Then the pass shows an "Auto-Refill On" badge and a "Next trigger when balance ≤ X" preview on the order confirmation and in Wallet Given a mobile device with viewport width 320–428 px Then the checkout auto-refill UI renders without horizontal scroll and primary CTAs remain visible without overlap
Configure Threshold and Refill Amount with Guardrails
Given a credits-based pass When configuring auto-refill Then the threshold input accepts only integer values ≥ 1 credit and the refill amount options are limited to studio-configured credit increments Given a currency-based pass When configuring auto-refill Then the threshold input accepts currency values ≥ 0.01 in the pass currency and the refill amount options are limited to studio-configured currency increments Given the threshold is greater than or equal to the selected refill amount in the same unit When attempting to save Then validation blocks save, focuses the offending field, and displays the message "Threshold must be less than refill amount" Given invalid input is corrected When saving Then the settings save successfully and the error message is cleared
Set Monthly Spend Cap and Capture Consent
Given the client enters a monthly spend cap When the cap is less than the selected refill amount Then validation prevents save and displays "Monthly cap must be at least one refill amount" Given a valid monthly cap is entered When the client confirms with Apple Pay or Google Pay Then a reusable payment token is stored and linked to the pass’s auto-refill mandate and the consent/terms text is displayed and accepted Given auto-refill charges occur over time When the sum of successful auto-refills in the current calendar month reaches the cap Then subsequent auto-refill triggers for that month are skipped, the client is notified via in-app and email/SMS within 5 minutes, and checkout shows "Auto-refill paused due to cap" if applicable
View, Edit, and Disable Auto-Refill from Profile and Wallet
Given auto-refill is enabled for a pass When the client opens Profile > Payments or Wallet Then the pass lists current settings (threshold, refill amount, monthly cap, payment method, next-trigger preview) and an Edit action Given the client edits settings with valid values When saving Then updates persist and are reflected across Profile, Wallet, and Checkout views Given the client selects Disable and confirms in the modal When processed Then auto-refill status becomes Off immediately, the payment mandate/token is revoked for auto-refill use, future triggers are prevented, and a confirmation banner is shown Given auto-refill is disabled Then the UI shows "Auto-Refill Off" and hides the next-trigger preview
Persist Settings Across Devices and Sessions
Given auto-refill settings are saved for a pass When the same client signs in on another device Then the latest settings load from the server within 1 second of opening Wallet on a typical 4G connection Given settings are updated on one device When another device opens or refreshes Wallet or Profile Then the updated settings are displayed within 5 seconds without re-enrollment Given the client logs out and back in When opening Wallet Then the previously saved auto-refill settings are present and accurate
Support Multiple Passes with Independent Auto-Refill Settings
Given a client owns multiple Wallet passes When enabling auto-refill on one pass Then the settings apply only to that pass and do not alter any other pass Given two passes each have auto-refill enabled with different thresholds, refill amounts, currencies, and caps When each pass balance falls to or below its respective threshold Then each pass triggers auto-refill independently according to its own settings Given the client edits auto-refill for Pass A Then Pass B’s auto-refill settings and status remain unchanged
Localized Currency/Units and Next-Trigger Preview
Given the client locale is en-US and the pass currency is USD When configuring and reviewing settings Then currency values display as $12.00 and dates/times use MM/DD/YYYY and 12-hour time Given the client locale is fr-FR and the pass currency is EUR When configuring and reviewing settings Then currency values display as 12,00 € and dates/times use DD/MM/YYYY and 24-hour time Given a credits-based pass When configuring settings Then values display as integer credits without currency symbols Given settings are saved Then the UI shows a next-trigger preview that states the exact condition and amount to be charged, e.g., "Next auto-refill: when balance ≤ 2 credits, charge €30,00"
Apple/Google Pay Authorization & Tokenization
"As a client on mobile, I want to authorize auto-refills with Apple/Google Pay so that top-ups complete instantly without re-entering payment details."
Description

Collect a reusable payment credential via Apple Pay or Google Pay during enrollment to support merchant-initiated top-ups. Implement network tokenization, SCA where required, and vaulting with provider-level PCI compliance. Handle retries, soft declines, expired tokens, and fallback to card-on-file when wallet pay is unavailable. Provide clear consent text for recurring/unscheduled MIT charges, display brand and last4, and issue itemized receipts. Expose a secure revocation path to remove the payment credential and disable auto-refill.

Acceptance Criteria
Enroll via Apple/Google Pay and Vault Network Token for MIT Top-Ups
Given a client enables Auto-Refill and selects Apple Pay or Google Pay as the payment method When the client authorizes the enrollment on the native wallet sheet Then a network token is provisioned and vaulted with the PSP, returning a reusable token reference that is marked active And strong customer authentication (SCA) is completed where required by region/issuer before enrollment is marked successful And the Auto-Refill settings display the payment brand (e.g., Visa, Mastercard) and last4 from the tokenized credential And no primary account number (PAN) is stored on ClassNest systems (only provider token references) And if tokenization or activation fails, the enrollment is aborted with a clear error message and no credential is stored And an audit log entry is recorded capturing user, method type, brand, last4, token reference (masked), and outcome
Capture and Store Explicit MIT Consent During Enrollment
Given the client is enrolling in Auto-Refill with Apple/Google Pay When the consent text describing recurring/unscheduled merchant-initiated top-ups, refill trigger, refill amount, monthly spend cap, and revocation path is presented And the client confirms consent via the Apple/Google Pay authorization Then a consent record is stored with user ID, timestamp, IP/agent, consent version, text hash, locale, payment method token ID, MIT type (unscheduled), refill amount, threshold, and monthly cap values And enrollment cannot complete unless consent is captured and persisted And the stored consent can be retrieved verbatim for audit and customer support And the UI surfaces a concise summary of the consent alongside brand and last4 in Auto-Refill settings
Soft Declines and Retry Policy for Merchant-Initiated Top-Ups
Given an Auto-Refill MIT top-up is triggered and the wallet token charge is declined with a retriable (soft) decline code When the decline occurs Then the system schedules up to 3 retries over 48 hours using exponential backoff (e.g., ~1h, ~12h, ~36h) And retries are canceled if the wallet balance is replenished by any other successful payment or if the user revokes the credential And hard declines (non-retriable) result in immediate failure with no retries and a user notification And on a successful retry, the top-up is applied, a receipt is sent, and Auto-Refill remains enabled And after final failed retry, Auto-Refill is automatically paused for that credential and the user is notified via email/SMS with re-enrollment instructions And all attempts, outcomes, and codes are captured in audit logs
Expired/Invalid Token Detection and Re-Authorization Prompt
Given a stored wallet network token becomes inactive, expired, or otherwise invalid When a MIT top-up is attempted or a scheduled token health check runs Then the attempt is blocked before authorization with a clear reason And the user is prompted to reauthorize via Apple/Google Pay to refresh the token And no additional retries are queued until reauthorization succeeds And an alert is sent to the user (email/SMS/push) within 5 minutes of detection And audit logs record the invalidation reason, timestamp, and notification status
Fallback to Card-on-File When Wallet Pay Is Unavailable
Given a user has an active card-on-file with stored MIT consent and has enabled fallback in settings And a wallet MIT top-up fails due to wallet unavailability or tokenization-specific errors (not hard declines) When the top-up is triggered Then the system automatically attempts the charge using the card-on-file token within the same amount and cap constraints And the receipt and activity log clearly indicate the fallback payment source (brand and last4) and reason And if no eligible card-on-file exists or MIT consent is missing for card-on-file, no fallback occurs and the user is notified to reauthorize And fallback attempts adhere to the same retry policy rules for soft vs hard declines
Itemized Receipt Delivery and Payment Method Disclosure
Given a top-up is successfully captured via Apple/Google Pay token or fallback card-on-file When the payment settles Then an itemized receipt is emailed to the user and available in-app within 1 minute of capture And the receipt includes line items (top-up amount, taxes/fees), total, MIT indicator, payment method brand and last4, masked token reference, merchant details, date/time, and unique receipt ID And refunds or reversals generate corresponding corrected receipts and are linked to the original receipt ID And the studio/merchant receives a copy or has dashboard access to the receipt and settlement reference
User-Initiated Revocation of Payment Credential and Auto-Refill
Given a user has Auto-Refill enabled with a stored wallet token When the user selects Remove Payment Method or Disable Auto-Refill in settings and confirms Then the system revokes the token via the PSP/provider where supported and marks the credential inactive And Auto-Refill is immediately disabled and any queued retries for that credential are canceled And brand and last4 are removed from the UI, leaving no active MIT credential on file And the user receives a confirmation email within 5 minutes summarizing the revocation And audit logs record who revoked, when, method, and provider revocation outcome And re-enrollment requires a fresh wallet authorization and consent capture
Monthly Spend Cap & Budget Safeguards
"As a cost-conscious client, I want to set a monthly spend cap so that I can control my budget while keeping auto-refill enabled."
Description

Allow users to set a monthly spend cap for auto-refills and show real-time usage against the cap. Enforce the cap by blocking additional auto-refill attempts once the cap is reached, with configurable studio-level minimum/maximum caps. Send warnings at configurable thresholds (e.g., 80% and 100%), and provide options to pause, adjust, or temporarily override caps with explicit confirmation. Clearly communicate booking outcomes when a cap prevents a refill, and offer alternate payment at checkout.

Acceptance Criteria
Set Monthly Cap and View Real-Time Usage
- Given a user with an active Wallet pass and auto-refill enabled, When they open Budget & Cap settings, Then they can set a monthly spend cap value in the wallet currency with a numeric input and save it successfully if it is within the studio’s configured min/max. - Given the user has a saved monthly cap, When the user views their Wallet, Then the UI displays current-month auto-refill spend used, remaining allowance, and percent used, calculated as sum of auto-refill charges posted in the current calendar month in the account time zone. - Given an auto-refill charge posts, When the transaction succeeds, Then the used amount and percent update in the UI within 5 seconds and reflect the new total.
Enforce Cap at Checkout Auto-Refill
- Given the user’s Wallet balance is insufficient for a booking and auto-refill is enabled, And the next refill would cause monthly spend to exceed the cap, When the user attempts to book, Then the system blocks the auto-refill charge and does not charge the user. - Then the checkout shows a clear message that the monthly cap prevents auto-refill, displays used/limit amounts, and offers alternate payment methods (Apple Pay, Google Pay, card on file, or new card). - When the user completes payment using an alternate method, Then the booking confirms successfully without modifying the Wallet balance or monthly cap usage. - When the user declines alternate payment, Then the booking is not created and the user sees a non-ambiguous failure reason; no pending reservation remains. - Given network retries, When the same blocked refill request is replayed within 2 minutes, Then the system remains idempotent and no charges are attempted.
Studio Min/Max Cap Configuration Enforcement
- Given a studio has configured a monthly cap minimum M and maximum N, When a user saves a cap value, Then the system validates and only accepts values where M <= cap <= N; otherwise shows inline error and prevents save. - When a studio updates M or N, Then new values take effect immediately for new saves, and users with existing out-of-bounds caps are prompted to adjust at next visit to the Cap settings before any changes can be saved. - Given an API client attempts to set a cap outside the allowed range, When the request is received, Then the API responds 400 with a validation error code and no change is persisted.
Threshold Warnings at 80% and 100%
- Given a user has a cap and a notification preference for budget alerts, When monthly usage crosses 80% of cap for the first time in the current month, Then the system sends one alert within 5 minutes via configured channels (email/SMS/push) containing current used amount, cap, remaining amount, and a link to adjust cap or set an override. - When usage later crosses 100% of cap, Then a separate alert is sent within 5 minutes with the same details and clear language that further auto-refills will be blocked. - Given usage fluctuates or multiple refills occur, When additional crossings of the same threshold happen in the same month, Then duplicate alerts are not sent. - When threshold values are changed by the studio (e.g., to 75% and 95%) mid-month, Then the next applicable crossing triggers alerts according to the new thresholds only.
Pause, Adjust, and Temporary Override
- Given a user has a cap, When they tap Pause Cap, Then cap enforcement status changes to Paused immediately, and auto-refills proceed without cap checks until the user resumes or any pause duration expires. - Given the user adjusts the cap value within the allowed studio range, When they save, Then the new cap takes effect immediately for subsequent refill evaluations and the used amount is unchanged. - Given a booking is blocked by the cap, When the user selects Allow one-time override at checkout, Then the system requires explicit confirmation (Apple Pay/Google Pay/3DS) and on approval processes exactly one auto-refill even if it exceeds the cap. - Then the override is consumed and enforcement resumes for subsequent refills; the UI shows that the override was used and the new monthly usage total.
Month Boundary and Time Zone Handling
- Given the platform stores an account time zone, When the clock reaches 00:00 on the 1st day of the month in that time zone, Then the monthly usage counter resets to 0 and the UI reflects the reset within 60 seconds. - Given a refill posts within ±5 minutes of the month boundary, When calculating usage, Then the transaction is attributed to the month determined by its settled timestamp in the account time zone, and calculations remain consistent across UI and invoices. - When the user changes their account time zone mid-month, Then the current month usage and reset date are recalculated based on the new time zone with a one-time notice explaining the change.
Audit Trail and Visibility
- Given cap-related events occur (cap set/changed/paused/resumed, threshold alerts sent, override granted/used, refill blocked), When an admin views the member’s billing activity, Then an audit log lists each event with timestamp, actor (user/system), amounts, booking/refill IDs, and channel used for alerts. - When exporting the month’s billing events as CSV, Then cap-related events are included with structured fields and match the on-screen totals for used and remaining amounts. - Given a user requests to see their budget activity, When they open Wallet Activity, Then they can see their own cap usage, alerts sent, and overrides used without other users’ data.
Just-in-Time Triggering & Checkout Integration
"As a client booking a class, I want my wallet to auto-refill just in time so that my booking confirms without extra steps."
Description

When a booking or waitlist auto-offer is initiated and the wallet balance is below the user-defined threshold, trigger an immediate top-up before confirming the reservation. Ensure atomic operations with idempotency keys and concurrency-safe locking to prevent double charges. If the refill succeeds, proceed to confirm the booking without additional steps; if it fails, present fallback payment and clear error messages. Support taxes/fees, currency rounding, and partial refill logic when the purchase requires more than one increment. Log all events to the wallet ledger for transparency.

Acceptance Criteria
JIT Top-Up on Booking and Auto-Offer
Given a client opted into Smart Auto-Refill with a saved Apple/Google Pay credential and configured threshold and refill increment And their wallet balance is below the threshold at the moment a booking or waitlist auto-offer reservation is initiated When the system evaluates wallet sufficiency prior to placing or accepting the hold Then it triggers an immediate top-up authorization and capture for one or more increments as needed within the monthly cap And on successful capture the reservation is confirmed in the same transaction without any additional user steps And the end user sees a single success confirmation referencing both the booking and refill amount And no reservation is confirmed until the top-up succeeds
Idempotent Charge and Atomic Reservation
Given duplicate trigger requests with the same idempotency key within 24 hours (e.g., client retries, webhook retries, or page refresh) When the top-up and booking flow is executed Then at most one charge is captured and at most one reservation is created And subsequent requests return the original result without creating new charges or reservations And if the charge succeeds but reservation fails, the charge is automatically voided/refunded within 60 seconds and no funds remain captured And the ledger records a single correlated success or a charge-then-void pair with matching correlation IDs
Concurrency-Safe Locking Under Parallel Triggers
Given two or more parallel triggers for the same client and time window (e.g., booking click and waitlist auto-offer) within 250 ms When the system processes them Then a concurrency lock prevents overlapping wallet mutations and charges And only one path proceeds to top-up and reservation; others receive an Already Fulfilled response And no deadlocks occur and all contending requests complete within 3 seconds And the final state shows one reservation, one captured charge (if needed), and a consistent wallet balance
Partial Refill and Monthly Cap Enforcement
Given a purchase total (including taxes/fees) that exceeds the current wallet balance and requires multiple increments And the client has configured a monthly spend cap When the system computes the refill amount Then it purchases the minimum number of increments necessary to cover the total without exceeding the monthly cap And if covering the total would breach the cap, it tops up to the cap limit and presents fallback payment for the shortfall And the booking is confirmed only if wallet balance after refill covers the total; otherwise it is held pending fallback and then released on expiry
Taxes, Fees, and Currency Rounding Compliance
Given currencies with ISO 4217 minor units (e.g., JPY=0, USD=2, TND=3) When calculating refill amounts, taxes, fees, and wallet debits Then all monetary values are rounded half up to the currency minor unit before capture and ledger posting And the sum of line items equals the captured total exactly in minor units And no discrepancy greater than 1 minor unit exists between processor capture and ledger entries And validation rejects transactions where computed totals and processor totals differ
Fallback Payment and Error Messaging
Given a top-up attempt fails due to processor decline, insufficient funds, or network error When the failure is detected Then the user is presented immediately with a fallback payment sheet (Apple/Google Pay and saved cards) without losing their place And a clear, localized error message with the decline reason code and next-step guidance is shown And if fallback succeeds within the hold window, the booking is confirmed and only the successful payment is captured And if fallback does not succeed before the hold expires, the booking is not confirmed and no charges are captured
Comprehensive Wallet Ledger Logging
Given any JIT top-up attempt (success, failure, or retry) and any reservation outcome When the flow completes or aborts Then the wallet ledger records immutable entries for: trigger, authorization, capture/void/refund, reservation confirm/cancel, and fallback attempts And each entry includes timestamp (UTC), actor, amount, currency, tax, fee, idempotency key, lock ID, booking/waitlist IDs, and correlation ID And entries are written within 1 second of each state change and are queryable by correlation ID And the ledger reflects a balanced sequence where totals captured equal wallet credits applied for successful flows
Notifications, Receipts & Alerts
"As a client, I want clear notifications about auto-refill activity so that I always know what I was charged and my current balance."
Description

Send configurable SMS/email notifications for enrollment, setting changes, successful refills, failures, and monthly cap warnings. Localize content, include the new wallet balance, remaining monthly cap, and a link to manage settings. Provide itemized receipts and support push notifications where available. Respect user communication preferences and compliance (e.g., TCPA/GDPR), and offer webhook callbacks so studios can mirror updates in external systems.

Acceptance Criteria
Enrollment Notification Dispatch on Auto-Refill Opt-In
Given a client enables Smart Auto-Refill and confirms payment via Apple Pay or Google Pay And the client has set a refill size, low-balance threshold, and monthly spend cap And the client has specified channel preferences for SMS, email, and/or push When the enrollment is saved Then send notifications only via enabled channels within 30 seconds And localize content to the client’s preferred locale with correct currency and time zone And include the wallet balance (post-enrollment), remaining monthly cap, selected threshold and refill size, and a secure link to manage settings And include required compliance footers (e.g., SMS STOP/HELP) where applicable And do not send SMS/email/push to any channel without explicit consent And emit a webhook event "auto_refill.enrolled" with event_id, timestamps, client_id, wallet_id, and enrollment parameters
Settings Change Confirmation for Auto-Refill
Given a client changes any Auto-Refill setting (refill size, threshold, monthly cap, payment method, or channel preferences) When the change is saved Then send a confirmation notification via enabled channels within 30 seconds And localize content to the client’s preferred locale And include the wallet balance (unchanged), remaining monthly cap, and a summary of the new values changed And include a secure link to manage settings And suppress notifications to channels the client has opted out of And emit a webhook event "auto_refill.updated" carrying old_value/new_value for each changed field
Successful Auto-Refill Receipt and Push Support
Given a client’s wallet balance falls below the configured threshold And Auto-Refill triggers and the payment is successfully captured When the transaction completes Then send an itemized receipt via enabled channels within 30 seconds And send a push notification if the client has an active push token and has opted in; otherwise fall back to email/SMS per preferences And include: refill amount, taxes/fees/discounts, total charged, masked payment method, transaction_id, new wallet balance, and remaining monthly cap And include a link to view the full receipt and manage settings And localize all monetary and date/time fields to the client’s locale; use studio currency And emit a webhook event "auto_refill.succeeded" with amounts, currency, transaction_id, balance_after, remaining_cap, and manage_url
Failed Auto-Refill Alert with Next Steps
Given an Auto-Refill attempt fails (e.g., card declined, network error, expired token) When the payment provider returns a failure Then send a failure alert via enabled channels within 30 seconds And send a push notification if available and permitted; otherwise fall back per preferences And include a high-level failure category (e.g., payment_declined, auth_failed, temporary_error) without exposing sensitive data And include current wallet balance, remaining monthly cap, and a link to update payment method/manage settings And include the next retry time if a retry is scheduled, or state that no retry will occur And localize content to the client’s locale and include required compliance footers And emit a webhook event "auto_refill.failed" with failure_category, retry_at (if any), and actionable links
Monthly Cap Warning and Cap-Reached Notices
Given a client’s monthly Auto-Refill spend is tracked against the configured cap When cumulative spend reaches 80% of the cap Then send a one-time 80%-warning notification via enabled channels including remaining cap, cap reset date, and manage-settings link When cumulative spend reaches 100% of the cap Then send a cap-reached notification via enabled channels within 30 seconds including remaining cap (0), cap reset date, and manage-settings link And prevent further Auto-Refill attempts until the cap resets or is increased And localize all values to the client’s locale and respect channel preferences And emit webhook events "auto_refill.cap_warning" and "auto_refill.cap_reached" with remaining_cap and cap_reset_at
Localization, Preferences, and Compliance Enforcement
Given notifications and receipts must respect locale, preferences, and regulations When generating any SMS/email/push content for Auto-Refill events Then use the client’s preferred locale; if unavailable, fall back to the studio default; finally fall back to en-US And format currency according to studio currency and locale; format dates/times in the studio’s time zone And ensure required placeholders are present and correctly populated: wallet_balance_after, remaining_monthly_cap, manage_settings_url And do not send SMS to recipients without SMS consent; include business name and STOP/HELP in SMS for US where required; include manage/unsubscribe links in email per jurisdiction And record message delivery attempt and consent status for auditability
Webhook Delivery, Security, and Idempotency
Given a studio has configured a webhook endpoint When any Auto-Refill notification event occurs (enrolled, updated, succeeded, failed, cap_warning, cap_reached) Then POST a JSON payload to the endpoint within 30 seconds containing: event_type, event_id (UUID), created_at (ISO 8601 UTC), studio_id, client_id, wallet_id, transaction_id (if any), amounts, currency, wallet_balance_after, remaining_cap, cap_reset_at (if applicable), locale, manage_url And include an HMAC-SHA256 signature header computed over the raw body with the studio’s shared secret And include an Idempotency-Key header equal to event_id and retry with exponential backoff for up to 24 hours on network errors or 5xx responses; do not retry on 2xx/4xx And deliver each event at-least-once; ensure receivers can deduplicate using event_id And log delivery status and last response code for support visibility
Studio Controls & Performance Reporting
"As a studio owner, I want to configure auto-refill rules and view performance so that I can optimize cash flow and reduce support overhead."
Description

Give studios configuration controls for default refill increments, allowable thresholds, min/max refill sizes, and whether auto-refill is enabled per pass type. Provide dashboards and exports showing adoption rate, refill revenue, failure rate, average refill size, and impact on no-shows/booking completion. Include filters by pass, time range, and cohort. Offer audit trails of client opt-ins/changes and quick actions to assist clients (pause, cancel, resend receipt) with appropriate permissioning.

Acceptance Criteria
Configure Auto-Refill Defaults per Pass Type
Given a studio admin with Manage Passes permission opens a Wallet pass type’s Auto-Refill settings When the admin sets the default refill increment, allowable credit thresholds, and min/max refill size and saves Then the values are validated (min <= default <= max; thresholds within 1–100% of pass credit capacity) and persisted And the auto-refill enable toggle per pass type can be set to Enabled or Disabled and is persisted And changes propagate to client opt-in flows for that pass type within 5 minutes And an audit log entry is created capturing previous values, new values, actor, timestamp, and IP
Enforce Min/Max and Threshold Constraints at Opt-In
Given a pass type with auto-refill enabled and studio-configured min/max refill sizes and allowable thresholds When a client opens the auto-refill opt-in screen for that pass type Then only the configured threshold options are selectable and any out-of-range value is blocked with an inline error message And the selectable refill sizes respect studio min/max; values outside range cannot be submitted (client or API), returning HTTP 400 with error_code=OUT_OF_RANGE And the Apple/Google Pay confirmation sheet shows the exact refill amount that matches the selected size and pass currency And the selected threshold, refill size, and monthly cap are stored with the client’s pass upon successful confirmation
Auto-Refill Performance Dashboard KPIs
Given a studio admin opens Analytics > Auto-Refill Dashboard When a time range is selected (Today, 7, 30, 90 days, Custom) Then the dashboard displays: Adoption Rate (% of active Wallet pass holders with auto-refill enabled), Refill Revenue (gross), Failure Rate (% of attempted refills failing), Avg Refill Size, Booking Completion Rate (auto-refill vs non-auto-refill), and No-Show Rate (auto-refill vs non-auto-refill) And KPI definitions are available via tooltip and match product documentation And all figures are computed in the studio’s timezone and reflect data updated within the last 15 minutes And chart tooltips and totals reconcile (sum of daily points equals the period total within 0.5%)
Filtering by Pass Type, Time Range, and Cohort
Given a studio admin applies filters for Pass Type (multi-select), Time Range, and Cohort When the Cohort filter is set to New (first booking within last 90 days) or Returning (>90 days since first booking) Then all KPIs, charts, and tables reflect the filtered subset and update within 2 seconds for datasets up to 100k events And the active filters are displayed as removable chips and encoded in the URL for shareable deep links And resetting filters returns the view to defaults (All passes, Last 30 days, All cohorts)
CSV Export of Auto-Refill Activity and KPIs
Given a studio admin clicks Export on the Auto-Refill Dashboard with filters applied When Export CSV is requested for the selected time range Then a CSV is generated following RFC 4180 with UTF-8 encoding and filename auto_refill_<studio>_<YYYYMMDD>_<range>.csv And the file includes columns: timestamp_iso, client_id, client_name, pass_type_id, pass_type_name, action (opt_in|opt_out|pause|resume|refill_success|refill_failure|config_change|receipt_resent), refill_size, currency, pre_credits, post_credits, payment_method, success, failure_reason_code, receipt_id, actor_type, actor_id, cohort_label And amounts are formatted in major units with ISO 4217 currency code and correct minor unit precision And the export respects all active filters and row counts match the UI event count for the same range And for up to 100k rows the file is delivered within 60 seconds; larger exports are queued and a secure download link is emailed to the requesting admin
Audit Trail of Client Opt-Ins and Setting Changes
Given a client opts in/out or changes threshold, refill size, or monthly cap, or an admin performs a quick action When the change is saved Then an immutable audit record is created with actor (client/admin), action, previous_value, new_value, timestamp (ISO 8601, studio timezone), source (web|mobile|api), and IP (if available) And the audit history is visible on the client profile > Auto-Refill tab to users with View Billing History permission, with search by date range, action, pass type, and actor And audit entries cannot be edited or deleted by any role; redactions (PII) are recorded as separate audit actions with reason
Admin Quick Actions with Permissioning
Given a support user with Assist Clients permission views a client’s auto-refill settings When the user clicks Pause, Cancel, or Resend Receipt Then a confirmation modal describes the effect; the action executes only if the user has the required permission and the pass type allows admin assistance And successful actions send the client an email and SMS confirmation (per studio notification settings) and create an audit log entry And Resend Receipt sends the most recent successful refill receipt; repeated requests are rate-limited to 1 per minute per client And Pause prevents further refills until Resume is executed; Cancel disables auto-refill entirely for that pass type
Consent, Compliance & Audit Logging
"As a compliance and support stakeholder, I want verifiable records of auto-refill consents and events so that disputes can be resolved and regulations satisfied."
Description

Capture explicit user consent with timestamp, IP/device metadata, consent text version, and payment credential fingerprint. Maintain immutable audit logs for enrollments, changes to thresholds/caps, payment method updates, and each auto-refill attempt. Provide exportable evidence packages for chargeback disputes and support requests. Align with PCI-DSS scope, PSD2/mit transaction requirements, and data retention policies while enabling data deletion requests without breaking financial records.

Acceptance Criteria
Enrollment Consent Capture
Given a logged-in client opts into Smart Auto-Refill via checkout or Wallet settings When they confirm using Apple Pay or Google Pay with SCA completed or an allowed MIT setup flow Then the system persists a Consent record containing: consent_id, user_id, wallet_id, timestamp (UTC ISO-8601), ip_address (IPv4/IPv6), user_agent, device_platform (iOS/Android/Web), app_or_browser_version, consent_text_version, locale, refill_amount, threshold, monthly_cap, payment_method_reference (token id), payment_method_fingerprint (stable hash), mandate_reference/mit_agreement_id, sca_status, and channel And the record is retrievable by consent_id and user_id within 200 ms p95 And no PAN or CVV is stored at rest
Immutable Enrollment and Consent Audit Log
Given a Consent record exists When any update to consent fields is attempted Then the original Consent record remains immutable and write-protected And an append-only AUDIT event "AUTO_REFILL_ENROLLED" is present with content_hash and previous_hash (hash chain) And any in-place update attempt is rejected with a 409 and a separate AUDIT event "CONSENT_UPDATE_BLOCKED" is recorded with actor_id, actor_role, timestamp (UTC), ip_address, device metadata, and correlation_id
Threshold/Cap Change Logging
Given an enrolled client updates threshold, refill_amount, or monthly_cap When the change is submitted and confirmed Then an append-only AUDIT event "AUTO_REFILL_SETTINGS_CHANGED" is created containing: consent_id, changed_fields with old_value/new_value, actor_id, actor_role, timestamp (UTC), ip_address, device metadata, channel, and correlation_id And the settings change is linked to the prior consent via consent_id and visible in the client's audit trail within 5 seconds p95 And no PAN/CVV is stored
Payment Method Update Logging & Fingerprint
Given an enrolled client switches the auto-refill payment method When the new method is confirmed via Apple/Google Pay Then an AUDIT event "AUTO_REFILL_PAYMENT_METHOD_UPDATED" is recorded with: consent_id, previous_payment_method_reference (masked), new_payment_method_reference, new_payment_method_fingerprint, sca_status, mandate_reference/mit_agreement_id (if updated), timestamp (UTC), ip_address, device metadata, channel, and correlation_id And the consent record remains immutable (no in-place edits) with linkage to the new method via events And no PAN/CVV or sensitive auth data is stored at rest
Auto-Refill Attempt Audit Trail
Given a wallet balance falls below the configured threshold When an auto-refill is attempted Then an AUDIT event "AUTO_REFILL_ATTEMPTED" is recorded with: attempt_id, consent_id, trigger_reason, pre_balance, refill_amount, expected_post_balance, processor_request_id, idempotency_key, mit_flag, sca_status, timestamp (UTC) And upon gateway response, an AUDIT event "AUTO_REFILL_RESULT" is recorded with: attempt_id, outcome (success/decline), response_code, decline_reason (if any), processor_response_id, retry_at (if applicable), final_post_balance And both events are hash-chained, correlated via correlation_id, and queryable by user_id, wallet_id, and date range
Exportable Evidence Package
Given an admin with dispute or support permissions requests an evidence export for a user or transaction When the request is submitted with a time range or transaction_id Then a downloadable package is generated within 60 seconds containing: consent record snapshot, settings change events, payment method update events, related auto-refill attempts/results, and associated processor response metadata And the package is a ZIP with JSON files and a human-readable PDF summary, plus a manifest with per-file SHA-256 hashes and an overall HMAC signature And redacted PII fields are clearly marked; PAN/CVV are excluded; package size is under 25 MB for a 12-month range or the export is chunked
Data Retention and Deletion Compliance
Given a data subject submits a deletion request When the request is approved and executed Then personal identifiers (e.g., name, email, phone, IPs) are removed or pseudonymized per policy while financial/audit records remain intact and referentially consistent via a non-identifying subject_key And an AUDIT event "DATA_SUBJECT_REQUEST_COMPLETED" is appended with timestamp (UTC), actor, and scope of deletion And subsequent evidence exports show redacted PII but complete financial/audit history And records are retained for the configured retention period and are purged thereafter with a verifiable purge report And PSD2 MIT mandate references are retained until the end of the retention window

Family Share

Allow a pass owner to share credits with selected family or teammates right from their Wallet pass. Set per-member limits and class eligibility; names appear at check-in so instructors know who used the credit. Increases pass appeal, fills more seats, and eliminates messy credit transfers.

Requirements

Family Member Management in Wallet
"As a pass owner, I want to add and manage family/teammates on my pass so that they can book using my credits under rules I control."
Description

Provide a pass-owner-facing management module within the Wallet that lets owners add, invite, and remove family or teammate members using name, email, and mobile; assign relationship labels; set default sharing settings per member; view active/invited status; and revoke access instantly. Include an invitation acceptance flow for beneficiaries to create or link a ClassNest account. Enforce roles (Owner vs Member), privacy boundaries, and data retention. Persist memberships at the pass level with support for multiple passes and multiple owners, and surface a mobile-first list with search and pagination.

Acceptance Criteria
Owner Adds and Invites a New Member From Wallet
Given a signed-in Owner on Wallet > Pass A > Family Share, When they tap Add Member and enter Name (2–50 chars), Email (valid), and Mobile (E.164), Then the Invite button is enabled. Given the Owner assigns a Relationship label (predefined or custom ≤20 chars) and taps Invite, Then a Member record is created with status "Invited" and an audit entry is recorded with timestamp and actor. Then an invitation email and SMS containing a single-use, 7-day token are sent within 60 seconds. Then the invited Member appears in the list with masked contact (e.g., joh•••@mail.com, +1•••1234) and the configured default sharing settings are saved.
Invitee Accepts Invitation by Creating or Linking Account
Given an invite link with a valid token, When the invitee opens it, Then they can choose Create Account or Sign In to link an existing ClassNest account. When the invitee completes one of the options and accepts terms, Then their membership for Pass A becomes "Active" and the acceptance timestamp is stored. Then the Owner’s list reflects status "Active" within 10 seconds of acceptance. Given an expired or already-used token, When opened, Then the user is shown an expiration message and the Owner can re-send the invite; membership remains "Invited (Expired)" until re-invited.
Owner Removes Member and Revokes Access Instantly
Given an Active Member on Pass A, When the Owner selects Remove and confirms, Then the membership status changes to "Removed" and the member loses access to Pass A immediately. Then any attempt by the Member to book using Pass A is denied within 5 seconds with reason "Access Revoked". Then the Owner’s list no longer shows the Member under Active/Invited; the record is visible under a Removed filter with masked PII. Then an audit log entry captures remover, timestamp, and previous sharing settings.
Per-Member Sharing Settings: Limits and Class Eligibility
Given an Active or Invited Member, When the Owner opens Edit Settings, Then they can set: monthly credit limit (0–999), per-visit limit (0–5), and eligible class categories/instructors via checklist. When the Owner saves, Then inputs are validated with inline errors for invalid values and the valid settings persist. Given the Member attempts to book with Pass A, When the booking would exceed limits or is not eligible, Then the booking is blocked with a clear error and a log entry citing the rule; otherwise the booking succeeds and decrements tracked limits accordingly. Then all bookings made by Members attribute usage to that Member and show the Member’s name at instructor check-in.
Mobile-First Member List with Search and Pagination
Given ≥50 members, When viewing Wallet > Pass A > Family Share on a 360–414 px wide device, Then the list renders responsive items with touch targets ≥44 px and status badges. When the Owner searches by name, email, or mobile, Then results filter server-side and return within 500 ms p95 for datasets ≤1,000 entries. When scrolling, Then items paginate or infinite-scroll in pages of 25 and the next page loads within 800 ms p95; scroll position is preserved when navigating back. Then each item shows Name, Relationship, Status (Active/Invited/Removed), and an actions menu accessible via keyboard and screen readers (WCAG 2.1 AA).
Role Enforcement: Owner vs Member Permissions
Given role Owner, When viewing Pass A > Family Share, Then they can add, invite, edit settings, resend invite, and remove members. Given role Member, When viewing their account, Then they can view their own membership status and sharing settings read-only and cannot see pass balance, other members, or Owner PII. Given any Member, When calling management endpoints (add/edit/remove) for Pass A, Then access is denied with HTTP 403 and no changes are persisted. Then no Member can book using Pass A unless status is Active; all actions are logged with actor ID and role.
Pass-Level Membership Persistence Across Multiple Passes and Owners
Given an Owner with Pass A and Pass B, When they add Member X to Pass A only, Then Member X is not present on Pass B unless explicitly added. Given Pass A has multiple Owners, When Owner1 edits Member X’s settings, Then Owner2 sees the changes on next fetch; concurrent edits made with a stale version are rejected with a conflict message and require retry. When Pass A expires or is deleted, Then associated memberships transition to "Inactive (Pass Ended)" and no longer permit bookings while remaining visible in history. Then data exports and API queries return pass-level membership associations with stable IDs for Owner and Member.
Per-member Credit Limits and Eligibility Rules
"As a pass owner, I want to set limits and class eligibility for each member so that my credits are used only how and when I intend."
Description

Enable per-member configuration of sharing rules: hard credit caps per day/week/month and overall, renewal alignment with the pass cycle, blackout dates, booking windows, and class eligibility filters by class type, instructor, location, and price tier. Provide templated presets and sensible defaults, with validation to prevent contradictory rules. Support inheritance from pass-level defaults with per-member overrides. Expose UI and APIs to edit rules, and ensure changes apply to future bookings while preserving existing reservations.

Acceptance Criteria
Per-Member Credit Caps Enforcement
Given a pass with Family Share enabled and a member with caps set to daily=1, weekly=3, monthly=10, overall=20 in the studio’s timezone When the member books the first class on a day where no other bookings exist Then the booking succeeds and the usage counts reflect daily=1/1, weekly=1/3, monthly=1/10, overall=1/20 When the same member attempts to book a second class on the same calendar day Then the booking is blocked and the error identifies the exceeded limit as Daily with current usage and limit When the same member books on different days but within the same ISO week until weekly usage reaches 3 Then the third booking succeeds and the fourth attempt is blocked with an error identifying the Weekly limit When the same member accumulates bookings across the cycle until overall usage reaches 20 Then further booking attempts are blocked with an error identifying the Overall limit And usage counts include all active future reservations made with this pass for this member
Caps Reset and Alignment with Pass Cycle
Given a monthly pass that renews on the 15th at 00:00 in the studio’s timezone and a member with caps daily=1, weekly=3, monthly=10, overall=20 When the pass renews on the 15th Then the member’s Overall and Monthly usage counters reset to 0 and limits remain unchanged And the next reset date for Monthly and Overall is shown as the next renewal date Given the same pass and a member who booked 1 class on Monday, Tuesday, and Wednesday When the next Monday local midnight passes Then the Weekly usage counter resets to 0 Given the same pass and a member who booked 1 class today When local midnight passes in the studio’s timezone Then the Daily usage counter resets to 0 And all reset calculations use the studio’s timezone and the pass’s cycle anchor
Blackout Dates Enforcement
Given a member with blackout dates set for 2025-10-10 through 2025-10-15 and recurring Fridays When the member attempts to book any class occurring on 2025-10-12 Then the booking is blocked with an error indicating the class is within a blackout date range When the member attempts to book any class on a Friday outside the explicit range Then the booking is blocked with an error indicating a recurring weekday blackout And blackout evaluation uses the studio’s timezone for date boundaries When new blackout dates are added after a reservation was already confirmed Then the existing reservation remains valid and only future booking attempts are blocked
Booking Window Enforcement
Given a member with an earliest booking window of 14 days and a latest booking cutoff of 60 minutes before start When the member attempts to book a class 15 days in advance Then the booking is blocked with an error indicating it exceeds the 14-day advance limit When the member attempts to book a class 30 minutes before its start time Then the booking is blocked with an error indicating it is inside the 60-minute cutoff And classes inside the allowed window can be booked successfully via UI and API And the UI disables dates outside the allowed window and the API returns a 422 with a machine-readable reason
Class Eligibility Filters by Type/Instructor/Location/Price Tier
Given a member allowed class types = [Yoga], excluded instructors = [Alex P], allowed locations = [Downtown], allowed price tiers = [Standard, Premium] When the member attempts to book a Pilates class Then the booking is blocked with an error indicating the class type is not eligible When the member attempts to book a Yoga class taught by Alex P Then the booking is blocked with an error indicating the instructor is excluded When the member attempts to book a Yoga class at the Uptown location Then the booking is blocked with an error indicating the location is not eligible When the member attempts to book a Yoga class at Downtown with price tier Premium Then the booking succeeds And if both allowlist and denylist include the same attribute value, the denylist takes precedence And if no per-member filters are set, pass-level defaults are applied
Rule Validation and Contradiction Prevention
Given an admin attempts to save per-member rules with negative caps or non-integer values When the rules are submitted Then saving is blocked with field-level errors specifying invalid cap values Given an admin sets a latest booking cutoff that is earlier than the earliest booking open window (e.g., open at 14 days, cutoff at 30 days) When the rules are validated Then saving is blocked with an error indicating contradictory booking window settings Given an admin configures class eligibility that results in zero eligible classes (e.g., allow types = [Yoga] and deny types = [Yoga]) When the rules are validated Then saving is blocked with an error indicating mutually exclusive filters Given an admin sets daily > weekly > monthly > overall in a way that violates period ordering (e.g., daily=5, weekly=3) When the rules are validated Then saving is blocked with an error indicating cap hierarchy violations (daily ≤ weekly ≤ monthly ≤ overall)
Defaults, Presets, Overrides, and Non-Retroactive Changes via UI and API
Given a pass with Family Share defaults configured and templated presets available (e.g., Kids 1/wk, Teammate Flex) When a new family member is added to the pass Then the member’s rules default to the pass-level defaults and display their source as Inherited When the admin selects a preset (e.g., Kids 1/wk) Then the member’s fields populate to the preset values and their source displays as Overridden When the admin clears an overridden field Then that field reverts to the pass-level default (source: Inherited) And the API GET returns each field value with metadata {source: inherited|overridden, presetId|null} When rules are updated (via UI or API) Then the changes apply only to bookings created after the change timestamp And existing confirmed reservations remain valid and are not auto-canceled And an audit log entry records who changed what and when
Booking Eligibility and Debit Engine
"As a beneficiary, I want my eligible bookings to automatically use the owner’s shared credits so that the process is seamless and I don’t have to manage transfers."
Description

Implement a rule-evaluation and debit engine that, at booking time, determines eligibility to use the owner’s pass, selects the correct funding source, reserves and deducts credits atomically, and prevents double-spend under concurrency. Respect member limits, class eligibility, pass balance, and expiration; fall back to the member’s own credits or payment when ineligible. Handle cancellations and no-shows with proper credit restore/forfeit logic per the pass policy. Integrate with waitlist auto-offer so accepted offers draw from the owner’s credits subject to the same rules. Provide idempotent APIs, clear error codes, and an auditable ledger of all share-related transactions.

Acceptance Criteria
Shared Credit Booking Eligibility with Member Limits
Given a pass owner has enabled Family Share for member M with a per-member monthly limit of 3 uses and the pass has a current balance of at least 1 credit When member M books an eligible class instance Then the engine selects the owner’s pass as funding, atomically reserves and debits 1 credit, increments M’s usage to 1/3 for the period, and confirms the booking with fundingSource=ownerPass Given member M has reached the per-member limit for the current period When M attempts to book using the owner’s pass Then the engine does not reserve/debit the owner’s credits and returns errorCode=SHARE_LIMIT_EXCEEDED with remainingLimit=0 Given member M is not included in the owner’s share list When M attempts to use the owner’s pass Then the engine rejects with errorCode=NOT_AUTHORIZED_FOR_SHARE and no ledger entry is created
Class Eligibility and Pass Expiration Enforcement
Given a pass that allows classTypes=[Yoga,Pilates] and excludes Workshops, and expires on 2025-10-31T23:59:59Z When a shared member attempts to book a Workshop class on 2025-10-15 Then booking with the owner’s pass is denied with errorCode=PASS_INELIGIBLE_CLASS and no credit is reserved or debited Given the same pass and a Yoga class on 2025-11-01T09:00:00Z When the member attempts to book using the owner’s pass Then booking is denied with errorCode=PASS_EXPIRED and no ledger entry is created Given an eligible Yoga class on 2025-10-20 and the pass is active When the member books Then the engine approves and debits 1 credit from the owner’s pass
Atomic Reserve-and-Debit with Concurrency Protection
Given the owner’s pass has a balance of 1 credit and is share-enabled for member M When two booking requests for M to the same class instance arrive within 50ms with different requestIds Then exactly one request succeeds with fundingSource=ownerPass and one fails with errorCode=INSUFFICIENT_CREDITS, and the ledger shows a single debit entry tied to the successful bookingId Given a single booking request is retried 3 times with the same idempotencyKey due to network timeouts When the engine processes the retries Then the response is identical across attempts and only one reserve-and-debit occurs Given two different classes are booked concurrently by M with total required credits greater than the pass balance When processed Then the engine honors the first committed transaction and prevents double-spend across both bookings
Fallback to Member Credits or Payment on Ineligibility
Given the owner’s pass is ineligible due to limit reached and member M has 2 personal credits When M books an eligible class Then the engine automatically uses M’s personal credits, debiting 1, and confirms booking with fundingSource=memberPass Given the owner’s pass is ineligible and M has 0 personal credits but a saved payment method When M books Then the engine routes to payment, authorizes the price, confirms booking on payment success with fundingSource=card, and leaves owner/member pass balances unchanged Given payment authorization fails When the engine attempts fallback payment Then booking is not created and errorCode=PAYMENT_AUTH_FAILED is returned
Cancellation and No-Show Credit Restore/Forfeit
Given a booking funded by the owner’s credits with cancelPolicy restoreWindow=12h and now=10h before class start When the member cancels Then the engine creates a ledger reversal, restores 1 credit to the owner’s pass, decrements the member-usage counter for the period, and updates booking status=canceled Given the same policy and now=2h before class start When the member cancels Then credits are not restored, a ledger forfeit entry is recorded, and booking status=canceled Given the member does not attend and the instructor marks no-show When no-show is recorded Then credits are not restored per policy, a forfeit entry exists if not already recorded, and the process is idempotent if the no-show event is received multiple times Given simultaneous cancel and no-show events are received within 1s When processed Then exactly one terminal outcome is recorded (restore or forfeit) according to policy priority, with no double-posting in the ledger
Waitlist Auto-Offer Debit Using Owner’s Credits
Given member M is on a waitlist for an eligible class and the owner’s pass is share-enabled with 1 remaining credit When an auto-offer is sent and M accepts within the offer window Then eligibility is re-evaluated at acceptance time, 1 owner credit is debited, and the spot is confirmed Given the owner’s credits were consumed by another booking before acceptance When M accepts the auto-offer Then the engine falls back to M’s personal credits or payment using the same rules; if neither is available, the acceptance fails with errorCode=FUNDING_UNAVAILABLE and the offer is not confirmed Given M does not respond before the offer expires When the offer window ends Then no debit occurs and the ledger shows no transaction for the offer
Idempotent APIs and Auditable Ledger Entries
Given clients send an Idempotency-Key header per booking, cancel, and no-show operation When the same operation is retried with the same key Then the engine returns the same result, with operationId and ledgerEntryId unchanged, and no additional debit/credit occurs Given a booking funded by the owner’s pass When querying the ledger by bookingId Then entries include: action (reserve, debit, reversal, forfeit), amount, currency, ownerId, memberId, passId, bookingId, timestamp, policyId, reasonCode, and signature/hash, and entries are immutable once committed Given an invalid request When processed Then the engine returns a typed error with one of the documented error codes: NOT_AUTHORIZED_FOR_SHARE, SHARE_LIMIT_EXCEEDED, PASS_INELIGIBLE_CLASS, PASS_EXPIRED, INSUFFICIENT_CREDITS, FUNDING_UNAVAILABLE, PAYMENT_AUTH_FAILED, INVALID_IDEMPOTENCY_KEY, and includes correlationId Given timezone-sensitive policies apply When evaluating restore windows and expirations Then the engine uses the policy-defined timezone and logs the resolved timestamps in ledger metadata
Beneficiary Booking Experience
"As a beneficiary, I want clear visibility and control over which pass pays for my booking so that I can avoid mistakes and confusion."
Description

Deliver a beneficiary booking UX that clearly indicates when an owner’s pass is available and selected, shows remaining shared quota for the member, and allows switching between payment sources when multiple are available. Display payer attribution in confirmation screens, receipts, and calendar/email/SMS reminders. Support web and mobile flows, accessibility standards, localization, and clear messaging when limits are reached or rules block a booking.

Acceptance Criteria
Owner Pass Visibility and Default Selection for Beneficiary
Given a beneficiary is logged in and viewing an eligible class And the owner has a shareable pass with remaining credits And the beneficiary has remaining member quota for that pass When the beneficiary reaches the booking payment step Then the owner's shared pass appears as an available payment source labeled "Shared by [Owner Name] — [Pass Name]" And it is preselected by default if no cheaper source exists And the UI displays the beneficiary's remaining share quota (e.g., "2 of 5 left this cycle") and the pass credit balance (e.g., "3 credits remaining") adjacent to the selector And the total updates to reflect pass usage before confirmation And an info tooltip explains why the pass is available
Switch Between Payment Sources During Checkout
Given multiple payment sources are available (shared owner pass, beneficiary pass, saved card) When the beneficiary switches the selected payment source Then the selection updates without a full page reload And pricing, fees, and taxes recalculate instantly (≤200ms) And any ineligible source shows an inline reason tooltip on hover/tap And the selected source persists when navigating back and forward in the flow And the booking is charged/consumed only against the final selected source
Quota and Eligibility Messaging on Blocked Booking
Given rules block the owner's pass for this booking (e.g., class not eligible, daily limit reached, member quota exhausted, pass credits 0) When the beneficiary opens the payment step Then the shared pass option is disabled with a specific reason label ("Class not eligible", "Daily limit reached", "Member quota 0/5", "No credits left") And a visible inline banner explains the block and offers CTAs to "Use another payment method" and "Choose a different date/class" And attempting to confirm with a blocked source is prevented with an accessible error message
Real-Time Quota and Credit Sync on Concurrency
Given the booking payment step is open And the owner's shared quota or pass credits change due to another action When the beneficiary attempts to confirm Then the system revalidates pass eligibility server-side And if availability decreased, the UI updates within 1s to show new remaining counts and disables the pass if no longer available And no charge/consumption occurs unless rules are satisfied at commit And the user remains on the payment step with an explanatory message if the booking cannot proceed
Payer Attribution Across Confirmation and Notifications
Given a beneficiary completes a booking using an owner's shared pass When the confirmation screen renders and notifications are sent Then the web confirmation displays "Paid by [Owner Full Name] via [Pass Name]" and "Booked for [Beneficiary Name]" And the email receipt, SMS reminder, and calendar invite include payer attribution and beneficiary name using the same labels And the beneficiary does not see the owner's sensitive payment details (only pass name and owner name) And the payer attribution is consistent across all channels and locales
Responsive Web and Mobile Parity
Given beneficiaries book on mobile (≤390px width) and desktop (≥1280px) And using iOS Safari 16+ and Android Chrome 120+ When interacting with pass selection and confirmation Then all elements are visible without horizontal scrolling And tap targets are ≥44x44px and content reflows without overlap And selector changes and pricing updates complete in ≤200ms on tested devices And feature behavior, labels, and states are consistent across web and mobile
Accessibility and Localization Compliance
Given beneficiaries using keyboard-only and screen readers in locales en-US and es-ES When navigating the booking and payment source selection Then all interactive controls are keyboard operable with visible focus states And screen readers announce owner name, pass name, and remaining quota for the selected option And error and status messages use aria-live polite and are announced And color contrast meets WCAG 2.2 AA (≥4.5:1) And all relevant strings are localized with correct pluralization and currency/date formatting
Check-in and Roster Attribution
"As an instructor, I want rosters to show whose credits were used for each attendee so that I can verify attendance and resolve questions quickly."
Description

Augment instructor check-in and rosters to show attendee attribution: attendee name, source pass owner, pass name, and share status indicator. Ensure attribution appears in live rosters, kiosk/QR check-in, and exports, without exposing sensitive owner contact details. Allow filtering and totals by share status for a class. Make the label visible enough to resolve front-desk questions quickly and maintain parity in API and reporting exports.

Acceptance Criteria
Live Roster Attribution Display
- Given a scheduled class with attendees using both self and family-shared credits, when the instructor opens the live roster on web or mobile, then each attendee row displays Attendee Name, Pass Name, Pass Owner Display Name, and a Share Status badge. - Share Status badge values are limited to: "Self" or "Shared". - Pass Owner Display Name renders as FirstName + LastInitial (e.g., "Jordan P.") and excludes email, phone, or address. - The four attribution fields are visible in each row without requiring expand/click. - For rosters up to 50 attendees, the roster renders with all four fields in under 1.5 seconds on a typical broadband connection.
Kiosk/QR Check-in Attribution Prompt
- Given an attendee checks in via kiosk or QR, when their booking is attributed to a shared pass, then the confirmation view shows Attendee Name, Pass Name, a "Shared" badge, and "Owner: {Pass Owner Display Name}" prior to finalizing check-in. - On successful check-in, the stored attendance record includes Attendee Name, Pass Name, Pass Owner Display Name, and Share Status and appears in the instructor's live roster within 5 seconds. - Owner email, phone, and address are never displayed on kiosk screens. - When the booking is not shared, the confirmation view shows a "Self" badge and no owner line.
Roster Filter and Totals by Share Status
- Given an instructor views a class roster, when they apply the Share Status filter to "Shared" or "Self", then only attendees with the selected status are displayed. - The roster header shows totals for the class: Self count, Shared count, and Overall total; totals update within 1 second of applying a filter. - Clearing the filter returns the full roster and restores the unfiltered list while keeping totals accurate. - On a test class with mixed statuses, the counts shown on-screen match the counts in the export and API for the same class (exact match).
CSV/Excel Export Includes Attribution Without PII
- When exporting a class roster or attendance, the file contains columns in this order: Attendee Name, Pass Name, Share Status, Pass Owner Display Name, Booking ID. - Share Status values are constrained to "Self" or "Shared"; no other values are emitted. - The export excludes Pass Owner email, phone, and address fields. - For a test class including at least one shared attendee, exported values for the above columns exactly match the live roster display for the same records.
API Parity for Attribution Fields
- Roster/attendance API responses include fields: attendeeName, passName, shareStatus, passOwnerDisplayName. - shareStatus is an enum of ["SELF","SHARED"]; requests/exports do not emit other values. - passOwnerDisplayName returns FirstName + LastInitial only; no owner email or phone fields are present in the response. - The OpenAPI/Swagger spec is updated to document these fields and enums. - For a representative class, API values for a sample of at least 5 attendees match UI and export values 1:1.
Visibility and Accessibility of Share Status Indicator
- The Share Status badge appears adjacent to the Attendee Name on the roster and kiosk confirmation screens. - Badge text meets a minimum contrast ratio of 4.5:1 against its background. - Badge font size is at least 12px on mobile and 14px on desktop; tap/click target is at least 32x24 px on mobile. - Hover (desktop) or tap (mobile) reveals an accessible tooltip/sheet with "Owner: {Pass Owner Display Name}" and no contact details. - The badge is announced by screen readers with aria-label "Share status: {Self|Shared}".
Notifications, Approvals, and Audit Trail
"As a pass owner, I want alerts and optional approvals for member bookings so that I can monitor usage and prevent unauthorized bookings."
Description

Provide configurable notifications to owners for bookings, cancellations, and limit-threshold events made by members, via push, email, or SMS. Support optional owner approval per member or per booking with timeouts and fallback behavior, including one-tap approve/deny. Notify members of approvals/denials with clear reasons. Maintain an immutable audit trail of actions for support, and expose notification preferences in the Wallet.

Acceptance Criteria
Owner notified when a member books using shared credits
Given the owner has Family Share enabled and "Member Booking" notifications enabled with push and email channels When a linked member successfully books a class using the owner's pass credits Then the owner receives a booking notification via push and email within 30 seconds And the notification includes the member's name, class title, date/time, location, credits used, remaining credits, and approval status (Approved or Pending) And an audit log entry with event type "member_booking_notification_sent" is created with timestamp, owner ID, member ID, booking ID, channels, and delivery status
Owner one-tap approve/deny for pending booking with timeout
Given the owner has configured "Requires approval" for a specific member or for the next booking When that member requests a booking using the owner’s credits Then the booking enters Pending state and a notification with one-tap Approve and Deny actions is sent to the owner immediately And the booking hold remains for the configured timeout between 5 and 60 minutes (default 15 minutes) When the owner taps Approve within the hold window Then the booking transitions to Confirmed, the credit is deducted, and the member is notified within 30 seconds When the owner taps Deny within the hold window and selects or enters a denial reason (max 120 characters) Then the booking is canceled, the credit is not deducted (or refunded if pre-held), the seat is released, and the member is notified within 30 seconds When the hold timeout expires with no decision Then the fallback Auto-Deny is applied and the member is notified within 30 seconds And all actions and outcomes are appended to the audit trail with actor, decision, reason, and timestamps
Member notified with clear reason after approval or denial
Given a booking using shared credits is pending owner approval When the owner approves the booking Then the member receives a notification via all enabled channels within 30 seconds indicating "Approved" with class details and no further action required When the owner denies the booking and selects a reason from a predefined list or adds a custom reason (max 120 characters) Then the member receives a notification within 30 seconds including the denial reason and a link to rebook or choose an alternate payment And an audit entry records the decision, reason, channels, and delivery outcomes
Owner notified on member cancellation with correct credit handling
Given a confirmed booking was made using the owner’s shared credits When the member cancels before the studio’s refund cutoff Then the credit is returned to the owner’s wallet immediately and the owner receives a cancellation notification within 30 seconds summarizing the refund When the member cancels after the refund cutoff Then credit handling follows studio policy (forfeit or partial refund) and the owner notification reflects the outcome And the cancellation, refund outcome, and notifications are appended to the audit trail
Per-member usage threshold and limit breach alerts
Given the owner set a per-member usage limit (e.g., N credits per calendar month) and an alert threshold at 80% When the member’s usage reaches 80% of the limit within the current period Then the owner receives a threshold warning notification within 30 seconds including current usage, remaining credits, and period end date, and only one warning is sent per period When the member attempts to exceed the limit Then the owner receives a limit-breached notification and the booking is either blocked with a clear message to the member or set to Pending if the owner enabled "Require approval when over limit" And threshold and breach events are recorded in the audit trail with counts and policy decisions
Owner manages notification preferences in Wallet
Given the owner opens Wallet > Family Share > Notification Preferences Then the owner can enable or disable event types: Member Booking, Member Cancellation, Threshold Warning, Limit Breached, and Approval Decisions And the owner can select delivery channels per event (push, email, SMS) with at least one channel required for any enabled event And the owner can add or update email and phone; verification is required before those channels can be used When the owner saves changes Then preferences persist and take effect immediately for subsequent notifications And email includes an unsubscribe link and SMS supports STOP to opt-out, which updates preferences And a preference-change audit entry is created with before/after values
Support-accessible immutable audit trail for notifications and approvals
Given any Family Share notification, approval, denial, cancellation, or threshold event occurs Then the system writes an immutable audit record capturing event type, actor (owner/member/system), UTC timestamp, owner ID, member ID, booking ID, pass ID, prior state, new state, decision reason (if any), and per-channel delivery outcomes And audit records are append-only and cannot be edited or deleted by any role And support users can query by owner, member, booking, date range, and export CSV of results And audit records are retained for a minimum of 24 months and are retrievable within 2 seconds at p95 for queries returning up to 100 records
Utilization Reporting and Billing Integrity
"As a studio owner, I want reports that break down shared credit usage by member and pass so that I can understand demand and reconcile payouts without extra admin."
Description

Add reports that attribute shared credit usage by pass, owner, member, class type, location, and time period, with export to CSV and studio dashboard widgets. Ensure revenue recognition and payout flows remain unchanged for studios; handle refunds, chargebacks, pass expiration, and retroactive cancellations consistently with existing policies. Provide data to analytics pipelines and tag share-related events for cohort analysis.

Acceptance Criteria
Run Utilization Report by Pass Owner and Shared Members
Given I have Reporting permission and select a date range And passes with Family Share usage exist in that range When I run the Utilization report with optional filters (pass, owner, member, class type, location) Then only matching usages are returned, one row per consumed credit And each row includes: pass_id, pass_owner_id, pass_owner_name, member_id, member_name, class_id, class_name, class_type, location_id, location_name, usage_timestamp (studio timezone), credit_id, share_indicator (true/false) And totals show count of usages grouped by pass, owner, member, class type, and location And the query responds within 5 seconds for up to 50k rows And data matches underlying attendance and wallet transactions 100% for the period
Export Utilization Report to CSV
Given I view the Utilization report results When I click Export CSV Then a CSV file downloads with UTF-8 encoding and RFC 4180-compliant quoting And headers match the on-screen columns in the same order And row count equals the on-screen total for the same filters And timestamps are in studio timezone with ISO-8601 format And numeric fields (counts, amounts) are not localized (use dot decimal) And exports complete within 30 seconds for up to 250k rows via asynchronous job with email link
Dashboard Widgets for Shared Credit Utilization
Given I have Dashboard permission When I add the Shared Utilization widgets Then I can see: total shared usages, % of total usages that are shared, top 5 passes by shared usage, and shared usage by class type and location And widgets respect the global date range and location filters And clicking a widget segment opens the pre-filtered Utilization report And widgets refresh at most 5 minutes stale data And users without Reporting permission cannot drill down to row-level data
Billing Integrity for Shared Credits
Given a shared credit is redeemed for a class When revenue recognition and payouts are computed Then the ledger entries and payout amounts are identical to the baseline policy for an equivalent non-shared credit And no additional fees or deductions are introduced by sharing And payout statements reconcile to wallet debits within ±$0.01 rounding tolerance And regression tests show zero deltas across all payout scenarios (drop-in, pass, package)
Adjustments: Refunds, Chargebacks, Expiration, Retro Cancellations
Given a class attendance tied to a shared credit is later refunded, charged back, expired, or retroactively cancelled per policy When the adjustment is processed Then the Utilization report reflects a reversal or adjustment entry with a reference to the original usage (original_usage_id) And net totals for the period reflect adjustments (e.g., negative counts where applicable) And payouts and revenue recognition adjust according to existing policy with no change in method due to sharing And adjustments are idempotent (re-processing does not duplicate entries) And CSV export and widgets reflect the updated state within 5 minutes
Analytics Pipeline Delivery and Event Tagging for Family Share
Given a shared credit is offered, accepted, redeemed, adjusted, or expired When events are emitted to the analytics pipeline Then events include tags: feature="family_share", share_indicator=true, pass_id, owner_id, member_id, class_id, class_type, location_id, event_type, usage_timestamp And delivery latency to the analytics sink is under 2 minutes p95 with 99.9% daily completeness And the event schema is versioned and documented, with backward-compatible changes And cohort queries can distinguish owner vs member behavior using provided identifiers And PII in analytics is limited to IDs (no emails/phone)

Pass Freeze

Give clients a simple "Pause Pass" option for travel, injury, or holidays. Admins define allowed freeze windows and documentation rules; expiry auto-extends and reminders adjust accordingly. Reduces refund requests, preserves goodwill, and keeps customers from churning during life events.

Requirements

Policy-Based Freeze Rules
"As a studio admin, I want to define clear rules for when and how clients can pause a pass so that freezes are fair, compliant, and require minimal manual intervention."
Description

Provide an admin-configurable policy engine to control pass freezes, including min/max duration, lead time, earliest start date, cooldown between freezes, maximum freezes per pass and per time period, blackout dates (e.g., promotional passes), and eligible pass types (class packs, unlimited, intro offers, recurring memberships). Allow reason codes with per-reason documentation requirements. Enforce rules consistently across mobile/web client flows and admin overrides, with clear validation errors. Respect studio time zones, surface a policy summary to clients, and expose settings in the admin UI and via API. This ensures fairness, prevents abuse, reduces support load, and aligns freezes with studio business rules.

Acceptance Criteria
Eligibility and Blackout Enforcement
Given a studio policy defining eligible pass types and blackout statuses When a freeze is submitted from mobile, web client, Admin UI, or Admin API for a pass that is ineligible or blacked out Then the system rejects the request, shows a validation error referencing the violated rule (e.g., "Not eligible" or "Blackout"), creates no freeze record, and makes no changes to expiry or reminders Given a pass that is eligible and not blacked out When a freeze is submitted Then the request proceeds to subsequent policy validations
Min/Max Freeze Duration Enforcement
Given policy minDurationDays M and maxDurationDays N and the studio time zone TZ When a user selects startDate and endDate for a freeze Then duration = daysBetween(startDate, endDate) in TZ with endDate exclusive And if duration < M or duration > N, the request is rejected with a message that includes M and N and no changes are persisted And if M <= duration <= N, the duration check passes
Lead Time and Earliest Start Date
Given policy leadTimeHours L and earliestStartOffsetDays E and the studio time zone TZ When a freeze startDateTime S is chosen Then S >= now(TZ) + L hours and date(S) >= today(TZ) + E days must both be true And if either condition fails, the request is rejected with a specific error indicating which condition failed
Cooldown and Freeze Limits
Given policy cooldownDays C, maxFreezesPerPass P, perPeriodLimit Q, and periodDays W When a new freeze is requested Then if the most recent freeze endDate is < C days ago in TZ, reject with "Cooldown not met" And if total freezes recorded for this pass >= P, reject with "Maximum freezes per pass reached" And if count of freezes whose startDate falls within the last W days >= Q, reject with "Period limit reached" And otherwise the limits check passes
Reason Codes and Documentation Requirements
Given a configured list of reason codes with documentationRequired, allowedFileTypes, and maxFileSizeMB When a user selects a reason that requires documentation Then an evidence upload is mandatory; only allowedFileTypes up to maxFileSizeMB are accepted; missing or invalid uploads block submission with a specific validation error And upon successful submission, the selected reason and document metadata are stored and retrievable via Admin UI and API
Studio Time Zone, Expiry Extension, and Reminder Adjustment
Given the studio time zone TZ and an approved freeze from startDate S to endDate E with duration D days When the freeze is recorded Then all date/time calculations use TZ; the pass expiry date is extended by exactly D days; and scheduled pass expiry/renewal reminders are shifted by D days And these adjustments are applied once per freeze and reflected in client and admin views and via API
Admin Policy Configuration, API Exposure, and Client Policy Summary
Given an admin with permission to manage policies When they open Pass Freeze Policy settings in the Admin UI Then they can view and set: eligible pass types, blackout flags, minDurationDays, maxDurationDays, leadTimeHours, earliestStartOffsetDays, cooldownDays, maxFreezesPerPass, perPeriodLimit with periodDays, and reason codes with documentation requirements; saving validates inputs and persists changes Given the Admin Policy API When a GET is called Then the current policy is returned with all configured fields and studio time zone When a PUT/PATCH with valid values is called Then the policy updates and takes effect for all channels within 1 minute; invalid payloads return detailed validation errors Given a client on mobile or web When they open the Freeze Pass flow Then a policy summary is displayed prior to submission including min/max duration, lead time, earliest start, cooldown, limits, eligibility/blackout notes, and documentation requirements Given any channel (mobile, web client, Admin UI, Admin API) When a freeze request violates policy Then the same error key and message template are returned/displayed across channels
Self-Service Pause Pass UI
"As a client, I want a simple Pause Pass option with clear eligibility and outcomes so that I can pause my access quickly without contacting support or losing value."
Description

Deliver a mobile-first client experience to initiate a freeze directly from pass details and booking pages. The flow shows eligibility, remaining freeze allowance, allowed start dates and durations, required documentation, and a live preview of the new expiry/renewal date and remaining credits. Support selecting a reason, uploading files/photos, acknowledging terms, and confirming the request. Provide instant confirmation and status tracking (pending/approved/denied) and make the entry accessible via the client portal and links in emails/SMS. Localize labels, support accessibility standards, and ensure performance on low-bandwidth devices.

Acceptance Criteria
Initiate Freeze from Pass Details and Booking Pages (Mobile)
Given an authenticated client with at least one active, freezable pass, when they open the Pass Details screen on a mobile device, then a prominent "Pause Pass" action is visible in the primary actions area and is enabled if eligible. Given an authenticated client on a booking page with an active, freezable pass selected, when the pass details sheet/dropdown is opened, then a "Pause Pass" action is available to launch the freeze flow for that pass. Given the pass is not freezable (e.g., allowance exhausted, admin disabled), when the screen renders, then the "Pause Pass" action is hidden or disabled with an explanatory tooltip/banner indicating the specific reason. Given the client taps "Pause Pass", when the flow opens, then the first step displays pass context (pass name, remaining credits, current expiry/renewal date) and a progress indicator for the steps. Given the client navigates back from the freeze flow, when they use the system back or on-screen back, then they are returned to the originating screen (Pass Details or Booking) with prior state preserved (e.g., selected pass, scroll position).
Eligibility and Remaining Allowance Display
Given the freeze flow is opened, when the eligibility panel loads, then it displays one of: Eligible or Not Eligible, along with a short reason if Not Eligible (e.g., minimum notice not met, allowance exhausted, pass type not supported). Given the pass has a remaining freeze allowance, when the panel renders, then it shows remaining allowance in correct units (days or occurrences) and the admin-defined maximums for this pass. Given the system retrieves server-calculated eligibility, when the UI renders, then the values shown (eligibility state, remaining allowance, reasons) match the API response exactly. Given the pass becomes ineligible due to server-side change during the session, when the client proceeds to submit, then the UI blocks submission and shows an inline error instructing the user to refresh, preserving entered data where possible.
Allowed Start Dates and Durations Validation
Given admin-defined freeze rules (min notice, allowed windows, max per-freeze duration, non-overlap), when the date picker opens, then dates outside the allowed range are disabled and show an explanation on attempt to select. Given the studio timezone is TZ_S, when the user selects a start date/time, then validation uses TZ_S and messages display dates formatted in the user's locale with TZ_S indicator where relevant. Given the user selects a duration, when the duration exceeds remaining allowance or max per-freeze, then the selection is prevented and an inline error specifies the limit. Given the selection would overlap an existing or pending freeze, when the user attempts to proceed, then submission is blocked and a message identifies the conflicting period. Given the user has made valid date and duration selections, when the form is evaluated, then the primary action (Continue/Submit) is enabled; otherwise it remains disabled.
Reason, Terms Acknowledgement, and Documentation Upload
Given admin-configured reasons exist, when the user opens the Reason field, then they can select from the list and optionally choose Other to input free text up to 250 characters. Given terms acknowledgment is required, when the user views the form, then a mandatory checkbox with a link to terms is present and submission is disabled until checked. Given admin rules require documentation for selected reasons or globally, when the user proceeds, then file upload is mandatory and the UI enforces accepted types (JPG, PNG, PDF), max size per file (10 MB), and max count (3 files), with progress indicators and the ability to remove/retry before submission. Given required fields are incomplete (reason, terms, or docs), when the user taps Submit, then the form prevents submission and focuses the first invalid field with a clear error message.
Live Preview of Adjusted Expiry/Renewal and Remaining Credits
Given the user selects a valid start date and duration, when values change, then a live preview updates immediately to show: freeze start–end dates, new expiry/renewal date, and remaining credits post-freeze. Given a credit-based pass, when a freeze is previewed, then remaining credits are unchanged and only the expiry date is extended by the freeze duration. Given a time-based/unlimited membership, when a freeze is previewed, then the renewal/expiry date is extended by the selected duration with day-level rounding at studio-local midnight. Given the preview is shown, when the server recalculation endpoint is called, then the displayed preview matches the server calculation exactly; on mismatch or failure, a fallback message appears and the user can retry. Given the freeze would adjust reminder schedules, when the preview renders, then a note indicates reminders will shift accordingly to the new dates.
Confirmation, Notifications, and Status Tracking via Portal and Message Links
Given the user submits a valid freeze request, when the request is accepted by the server, then the UI shows an instant confirmation with status Pending and a reference ID without requiring a page reload. Given the request is created, when notifications are triggered, then an email and SMS are sent within 60 seconds containing a deep link to the request detail; opening the link requires authentication or a time-limited secure token (valid ≥ 7 days). Given the client visits the Client Portal, when viewing Pass Details or the Requests/Freezes section, then the new entry is visible with status (Pending/Approved/Denied), selected dates, and submitted reason. Given an admin updates the request to Approved, when the client views the portal entry, then the status shows Approved and the pass expiry/renewal date reflects the approved freeze; the client also receives an email/SMS update. Given an admin updates the request to Denied, when the client views the portal entry, then the status shows Denied with the provided denial reason; the client also receives an email/SMS update.
Localization, Accessibility, and Low-Bandwidth Performance
Given the user's language preference is set to L, when the freeze flow loads, then all labels, errors, and system messages appear in L with fallback to English; dates and numbers are formatted per the user's locale. Given a keyboard or screen reader user, when navigating the flow, then all controls are reachable via keyboard, have accessible names/roles/states, dynamic updates (e.g., preview) are announced via ARIA live regions, focus is managed on step changes, and color contrast meets WCAG 2.1 AA (≥ 4.5:1). Given a mobile device on Fast 3G (~400 Kbps) and 150 ms RTT, when opening the first step of the freeze flow, then time-to-interactive is ≤ 3 seconds and additional downloaded payload for the flow is ≤ 200 KB gzipped. Given a device with width ≥ 320 px, when rendering the flow, then layout is responsive with tap targets ≥ 44×44 dp, CLS ≤ 0.25, and LCP ≤ 2.5 s for the first step. Given a file upload on a constrained connection, when uploading documentation, then the UI displays progress, allows cancel/retry, and remains responsive during up to 2 minutes of network latency.
Auto-Extension of Expiry and Credit Preservation
"As a client, I want my pass to be extended by the length of my pause so that I don’t lose paid time or sessions."
Description

Automatically extend applicable pass expiry dates by the freeze duration and preserve remaining credits when a freeze is approved. For class packs, stop credit consumption during the freeze and extend the expiry accordingly; for unlimited passes and memberships, disable access during the freeze and shift the end/renewal date. Handle partial-day rounding based on studio time zone, overlapping freezes, and pass state transitions (active, frozen, resumed). Ensure idempotent updates, accurate ledger entries, and integrity across dependent systems (reporting, client balances, APIs).

Acceptance Criteria
Class Pack Freeze: Extend Expiry and Preserve Credits
Given a client has an active 10-class pack with 5 remaining credits and an expiry date of 2025-10-15 in the studio’s time zone And an admin approves a freeze from 2025-09-28 00:00 to 2025-10-03 23:59 in the studio’s time zone When the approval is processed Then the pass state becomes Frozen from 2025-09-28 00:00 through 2025-10-03 23:59 And remaining credits remain 5 during and after the freeze And credit consumption is blocked during the freeze (bookings cannot deduct credits) And the expiry date is extended by 6 days to 2025-10-21 And the pass timeline records a Freeze Applied event with start/end and +6 days
Unlimited Pass Freeze: Shift Renewal and Disable Access
Given a client has an active monthly unlimited pass with next renewal on 2025-11-01 in the studio’s time zone And an admin approves a freeze from 2025-10-10 00:00 to 2025-10-20 23:59 in the studio’s time zone When the approval is processed Then the pass state becomes Frozen for that period And booking, waitlist, and check-in validations deny access for this pass during the freeze And the next renewal date shifts by exactly 11 days to 2025-11-12 And no usage is recorded for the pass during the freeze window
Partial-Day Rounding by Studio Time Zone
Given the studio time zone is America/New_York And a freeze is requested from 2025-10-01 15:30 to 2025-10-03 09:15 local time When the freeze is approved Then the freeze duration is rounded to 3 calendar days (Oct 1, 2, 3) And expiry/renewal shifts by exactly 3 days And a freeze requested from 2025-10-01 10:00 to 2025-10-01 18:00 local time rounds to 1 calendar day extension
Overlapping Freezes: Merge and Single Extension
Given a pass has an approved freeze from 2025-10-01 to 2025-10-05 inclusive And a second freeze from 2025-10-04 to 2025-10-07 is approved When both freezes are applied Then the effective frozen window is 2025-10-01 through 2025-10-07 And the expiry/end date extension is 7 days total (no double-counting overlap) And the pass state remains Frozen continuously across the merged window
State Transitions and Auto-Resume
Given a pass is Active with an approved future freeze from 2025-10-10 00:00 to 2025-10-12 23:59 in studio time When the clock reaches 2025-10-10 00:00 in studio time Then the pass state switches to Frozen and access controls are enforced When the clock reaches 2025-10-12 23:59:59 in studio time or an admin resumes early Then the pass state switches back to Active And the new expiry/end date reflects the extension calculated for the freeze And duplicate transition events are not created
Idempotent Processing and Retries
Given a freeze approval with id ABC123 for 2025-10-01 to 2025-10-03 is submitted And the approval job is retried or the webhook is delivered more than once When processing runs multiple times with the same id and parameters Then the pass expiry/renewal is extended only once by 3 days And only one ledger/audit entry exists for this freeze with correlation id ABC123 And the resulting pass state and dates are identical across retries
Ledger, Reporting, Balances, and API Consistency
Given a freeze is approved and applied to a pass When the transaction commits Then a ledger entry of type PASS_FREEZE is created with pass_id, start_at, end_at, delta_days, actor, and correlation_id and zero monetary impact And client credit balance for class packs remains unchanged And reporting datasets and dashboards reflect the new expiry/end date within 60 seconds And public and partner APIs return the pass with status=frozen during the window and updated expiry/end date and freeze periods And GET/POST/PUT endpoints are consistent with the ledger entry and no integrity violations occur
Booking Conflict Resolution and Waitlist Reallocation
"As a studio admin, I want bookings during a client’s freeze handled automatically and vacated spots reallocated to the waitlist so that operations remain smooth and fair to other clients."
Description

Detect and resolve conflicts when freezes overlap existing bookings. Apply studio policy to auto-cancel affected bookings without penalty, notify impacted parties, and free the spots. Trigger the live smart waitlist to send auto-offers and update queues atomically to prevent double-booking. Block new bookings during active freezes and display informative messaging. Synchronize with attendance/no-show rules to avoid unintended penalties and maintain consistent capacity. Log all actions for traceability.

Acceptance Criteria
Conflict Detection on Freeze Activation
Given a client has 1 or more confirmed future bookings and an eligible pass When a pass freeze is activated or its date range is edited to overlap those bookings Then the system detects all bookings whose start time overlaps the freeze window in the class’s local timezone (inclusive of boundaries) And the system flags each detected booking as pending freeze resolution within 1 second And the detected set excludes past-started or completed sessions
Auto-Cancel Overlapping Bookings Without Penalty
Given bookings are flagged as pending freeze resolution for a client When auto-resolution runs for the freeze Then each affected booking is canceled with reason code "freeze" and penalty=false And no late-cancel or no-show penalties are applied regardless of studio late-cancel rules And the booking no longer counts against pass usage or attendance tallies And session capacity is incremented accordingly for each canceled booking And all reminder/attendance automation tied to the canceled bookings is voided
Atomic Waitlist Reallocation After Freeze Cancellations
Given a class gains capacity because of freeze-driven cancellations and has a waitlist When the system reallocates freed spots Then the system sends an auto-offer to the next eligible waitlisted client(s) per studio-configured priority And each offered spot is held exclusively for the offer window duration per studio setting (e.g., 15 minutes) And accepting an offer creates a confirmed booking and immediately revokes all competing offers for that same spot And if an offer expires or is declined, the system atomically advances to the next in queue until capacity is filled or waitlist is exhausted And concurrent acceptance attempts result in exactly one success and deterministic declines for others (no double-booking)
User and Staff Notifications for Freeze Conflict Resolution
Given bookings are canceled due to a pass freeze When cancellation completes Then the client receives notification(s) via configured channels (SMS/email) within 60 seconds containing class name, date/time with timezone, location, and reason "Pass Freeze" And the instructor/staff receive a summary notification of freed slots without exposing client health details And notifications include next-step guidance (e.g., manage freeze, join waitlist, contact studio) and relevant links And no reminder or attendance messages are sent for the canceled instances
Block New Bookings During Active Freeze
Given a client has an active freeze covering a target class time When the client attempts to book via web, mobile, or API Then the booking action is blocked and the UI disables the primary booking CTA with an informative message showing freeze dates and a link to manage the freeze And the API returns a 409 Conflict (or equivalent) with error code "client_frozen" and the freeze window boundaries And deep links and calendar quick-book flows are equally blocked And removing or ending the freeze immediately re-enables booking eligibility for future classes outside the freeze window
Attendance/No-Show Rule Synchronization
Given a freeze overlaps one or more future bookings for a client When the system processes the freeze Then any affected future bookings are canceled before attendance state is finalized And no attendance or no-show events are recorded for those canceled instances And past or in-progress sessions are not modified by the freeze And studio analytics reflect unchanged no-show and attendance rates for the client due to freeze cancellations
End-to-End Audit Logging and Traceability
Given freeze-related conflict resolution, notifications, and waitlist reallocations occur When any action is executed Then an immutable audit log entry is recorded with timestamp (UTC), actor (system/user), clientId, passId, bookingId(s), classId, actionType, reasonCode "freeze", from/to booking statuses, capacity before/after, notificationId(s), and waitlistOfferId(s) And all related entries share a correlationId to trace the end-to-end flow And admins can search and export these logs by date range, client, class, and correlationId via UI and API And the audit log is retained for at least 12 months
Documentation Upload and Approval Workflow
"As an admin, I want to request and review documentation for certain freeze reasons so that I can confirm eligibility and prevent misuse."
Description

Enable optional documentation collection and review for freeze reasons that require proof (e.g., injury). Clients upload documents securely within the request flow; files are encrypted at rest, virus-scanned, and flagged for admin review. Provide an admin queue with request details, document previews, and actions to approve or deny with notes. Pending freezes do not take effect until approved; denials restore normal pass status. Enforce role-based access to sensitive documents, apply retention policies, and record decisions for auditability.

Acceptance Criteria
Client Upload: Required Documentation in Freeze Flow
Given a freeze reason configured as "requires documentation" and a client with an active pass When the client initiates a freeze request Then the documentation upload step is displayed before the request can be submitted Given allowed file types are PDF, JPG, PNG, HEIC and max file size is 10 MB per file When the client attempts to upload a disallowed type or an oversized file Then the UI blocks the upload and displays a clear validation error Given upload begins When network interruptions occur Then the client can retry the upload without losing entered form data Given a freeze reason requires documentation When the client attempts to submit the request without at least one valid document Then submission is prevented and a validation message indicates documentation is required Given documents are uploaded successfully When the client submits the request Then the system associates the clean, scanned documents with the freeze request and sets status to "Pending Approval"
Security: Virus Scan and Encryption at Rest
Given a document is uploaded When the upload completes Then the file is virus-scanned within 30 seconds and not made available to reviewers until the scan returns clean Given the virus scan detects malware When the result is received Then the file is discarded, the client is shown an error explaining the rejection, and an audit event is recorded; the file never appears in the admin queue Given a file is scanned clean When it is stored Then it is stored with server-side encryption using KMS-managed keys and is only retrievable via short-lived, signed URLs generated after authorization checks Given a transient scanner failure occurs When processing the file Then the system retries up to 3 times with exponential backoff; after final failure, the client is informed and may retry the upload
Admin Review Queue with Document Preview and Actions
Given a user has the "Pass Freeze Reviewer" role When they open the review queue Then they see a list of pending requests with client name, pass ID, reason, requested freeze dates, submission timestamp, and document count Given a reviewer selects a pending request When the request details are opened Then supported documents render as safe inline previews and each file has a download option (if permitted by RBAC) Given a reviewer is making a decision When they click Approve Then the decision is saved with reviewer ID, timestamp, and optional note, and the request leaves the pending queue Given a reviewer is making a decision When they click Deny Then a note is required, the decision is saved with reviewer ID and timestamp, and the request leaves the pending queue
Freeze Activation and Pass/Reminder Adjustments Post Decision
Given a pending freeze request with a start date in the past or today When an admin approves the request Then the pass status becomes "Frozen" immediately, the pass expiry is extended by the approved freeze duration, and all scheduled reminders are recalculated to exclude the freeze period Given a pending freeze request with a future start date When an admin approves the request Then a scheduled job activates the freeze at 00:00 of the start date in the pass's timezone, extends the expiry accordingly, and updates reminders to skip the freeze window Given a pending freeze request When an admin denies the request Then the pass remains or returns to "Active", no expiry extension is applied, any scheduled freeze activation is canceled, and reminders remain or are restored to the pre-freeze schedule
Client Notifications on Approval or Denial
Given notifications are enabled for the client When a freeze is approved Then the client receives an email and SMS within 2 minutes containing the approved freeze dates, the updated pass expiry date, and any reviewer notes Given notifications are enabled for the client When a freeze is denied Then the client receives an email and SMS within 2 minutes containing the denial reason and suggested next steps Given a notification fails to deliver When a transient error occurs Then the system retries delivery at least 3 times over 15 minutes and logs delivery status for audit
Role-Based Access and Auditability of Sensitive Documents
Given role-based access control is enforced When a user without the "Pass Freeze Reviewer" or "Owner" role attempts to view or download a document Then access is denied, no document content is exposed, and the attempt is audit-logged Given a reviewer views, downloads, approves, or denies a request When any of these actions occur Then an immutable audit log entry is recorded with user ID, role, action, request ID, timestamp, and source IP Given an auditor queries logs When filtering by date range and request ID Then the system returns all related access and decision events without modification capability
Retention Policy and Secure Deletion
Given a documentation retention period of 180 days after decision is configured When a request reaches the retention threshold Then associated documents are automatically and permanently purged, an audit log entry is created, and the request record remains without document binaries Given documents are scheduled for purge When the date is 7 days before purge Then admins with reviewer or owner roles see a banner on the request allowing a one-time 30-day extension with a required reason Given an authorized user deletes a document manually When deletion is confirmed Then the document becomes immediately unrecoverable, previews/downloads are disabled, and the UI displays a "Document deleted per retention policy" placeholder
Membership Billing and Renewal Alignment
"As a studio owner, I want membership billing to align with freezes so that clients aren’t charged for unusable time and our revenue schedules stay accurate."
Description

Align recurring membership billing with freeze periods using configurable strategies: push the next renewal by the freeze duration or continue billing while extending access time equivalently. Integrate with payment gateways to adjust subscription cycles, invoices, and proration while preserving revenue recognition rules. Handle mid-cycle freezes, maximum pause limits imposed by gateways, and edge cases such as failed payments or card updates with idempotent webhook processing and retries. Reflect the new renewal date consistently across client portal, admin views, and receipts.

Acceptance Criteria
Push Renewal by Freeze Duration
Given a member with a recurring monthly membership renewing on 2025-10-15 and the freeze strategy "Push renewal by freeze duration" And an approved freeze from 2025-09-30 to 2025-10-09 inclusive (10 days) When the freeze is activated Then the next renewal date updates to 2025-10-25 (10 days after 2025-10-15) And no invoices or payment attempts occur during the freeze window And the billing period end in the payment gateway is shifted by 10 days to match the new renewal date And the client portal, admin view, and gateway subscription all display the same new renewal date And audit logs record the original and new renewal dates with the freeze reference ID
Continue Billing with Access Extension
Given a member with renewal on 2025-10-15 and the freeze strategy "Continue billing, extend access equivalently" And an approved 10-day freeze within the current cycle When the freeze is activated Then the upcoming invoice remains scheduled for 2025-10-15 with the same amount and no proration line items And the member’s access end date is extended by 10 days beyond the paid-through date And receipts and the client portal display "Access extended by 10 days due to freeze" for the affected cycle And revenue recognition remains aligned to the original billing cycle dates And reminder emails/SMS for access expiry shift to the extended end date
Mid-Cycle Freeze Proration and Invoice Alignment
Given a 30-day billing cycle started on 2025-09-15 with 12 days consumed and 18 remaining When a mid-cycle freeze of 7 days is approved Then under "Push renewal" strategy, the renewal date shifts by 7 days and no credits or refunds are generated And under "Continue billing" strategy, the current invoice remains unchanged and access end date extends by 7 days And in both strategies, the payment gateway reflects the correct next charge date and/or access period without duplicate or overlapping cycles And the member can book classes only outside the frozen dates unless an admin override is applied
Gateway Max Pause Limits Enforcement
Given the configured gateway imposes a maximum single-freeze length of 90 days and a maximum of 3 freezes per rolling 12 months When an admin submits a freeze that exceeds these limits Then the system blocks submission with a validation error detailing which limit(s) would be exceeded And if the admin opts to auto-adjust, the system splits the request into the maximum permissible segments and displays the resulting schedule before confirmation And the stored freeze records and gateway subscription configuration remain within gateway limits And the attempt is logged with outcome "blocked" or "auto-adjusted" including limit metrics
Idempotent Webhook Processing and Retries
Given the payment gateway sends duplicate webhooks for the same subscription event (same event ID) When the system processes these webhooks Then only one state change is applied, subsequent duplicates are acknowledged and marked "ignored_duplicate" without side effects And all outbound gateway API calls for freeze/phase updates use idempotency keys and are safe to retry And on transient gateway errors (HTTP 5xx, timeouts), the system retries with exponential backoff up to 5 attempts and surfaces a "Pending Gateway Sync" status in the admin And on permanent errors (HTTP 4xx), the freeze operation is rolled back and the admin is notified with actionable error details
Failed Payment and Card Update During Freeze
Given the strategy is "Continue billing" and a payment fails during an active freeze When the member updates their card details Then the system retries the payment immediately and resumes the dunning schedule per configuration And the freeze dates remain unchanged and access stays frozen until payment succeeds And no duplicate invoices are created; the original invoice is paid and closed upon success And under the "Push renewal" strategy, failed payment at the pushed renewal date follows the same dunning flow aligned to the pushed date
Renewal Date and Reminders Consistency Across Surfaces
Given any freeze is applied that alters the next renewal or access end date When the change is saved Then the new next renewal date is identical across client portal, admin membership detail, receipts, and confirmation emails And "upcoming renewal" reminders are rescheduled relative to the new date and previous reminders for the superseded date are canceled And the waitlist and booking eligibility logic use the updated dates consistently And an automated integrity check runs within 5 minutes to confirm surface parity; discrepancies raise an alert to admins
Communications and Reminder Rescheduling
"As a client, I want timely notifications about my pause and return so that I know what to expect and don’t miss my first session back."
Description

Automate client and staff communications tied to the freeze lifecycle: request received, approval/denial, upcoming freeze start, freeze active, and pre-resume reminders. Suppress class reminders and promotional campaigns for clients during active freezes and reschedule them to resume after unfreeze. Provide customizable email/SMS templates per studio, localization, and respect opt-in preferences. Emit events for integrations and ensure message sends are deduplicated and observable for support.

Acceptance Criteria
Client Freeze Request Acknowledgment
- Given a client submits a freeze request that is stored with a freeze_id, When the request is accepted by the system, Then send a "Freeze Request Received" notification within 2 minutes to each channel the client is opted into and the studio has enabled. - Then the content uses the studio template key "freeze_request_received" with locale resolution client.locale -> studio.locale -> en-US, and renders variables: client_name, pass_name, requested_start, requested_end, freeze_id. - And deliver exactly once per channel per freeze_id using idempotency key "freeze_request_received:{freeze_id}:{channel}"; retries and webhook replays do not create duplicates. - And emit integration event "pass.freeze.request.received.v1" with freeze_id, client_id, pass_id, studio_id, timestamp, channels_sent, idempotency_key. - And record a message log and timeline entry visible in Support with status (queued|sent|failed|suppressed), channel, locale, template_version, and render_checksum.
Freeze Approval and Denial Notifications
- Given an admin approves or denies a freeze request, When the decision is saved with decision_at and decision_reason (optional), Then send the corresponding notification ("Freeze Approved" or "Freeze Denied") within 2 minutes to all opted-in, enabled channels. - Then approved messages include approved_start, approved_end, new_pass_expiry, and freeze_id; denied messages include denial_reason and next_steps if provided. - And messages use studio templates (keys: "freeze_approved", "freeze_denied") with locale resolution client.locale -> studio.locale -> en-US; fallback is recorded. - And deliver exactly once per channel per decision using idempotency key "freeze_decision:{freeze_id}:{decision}:{channel}"; duplicates are suppressed. - And emit integration events "pass.freeze.approved.v1" or "pass.freeze.denied.v1" with correlation_ids. - And log outcomes and provider response codes for Support visibility.
Upcoming Freeze Start and Freeze Active Notifications
- Given a freeze has approved_start, When now reaches approved_start minus 24 hours, Then queue a "Freeze Starting Soon" reminder; if approval occurs <24h before start, schedule the reminder at approval_time + 1 minute. - When now reaches approved_start, Then send a "Freeze Active" notification within 2 minutes. - All sends respect channel opt-ins and studio enablement; templates ("freeze_starting_soon", "freeze_active") use locale resolution client.locale -> studio.locale -> en-US. - If approved_start is updated, Then previously scheduled messages are cancelled and rescheduled accordingly; no duplicate reminders exist for the same freeze_id and stage. - For each message, emit events "pass.freeze.starting_soon.v1" and "pass.freeze.active.v1" and log observable delivery metadata.
Pre-Resume (Unfreeze) Reminder
- Given a freeze with approved_end, When now reaches approved_end minus 48 hours, Then send a "Freeze Ending Soon" reminder; if lead time <48h, send at min(approved_end - 1 hour, now + 1 minute) but never after approved_end. - Messages respect channel opt-ins and use studio template key "freeze_ending_soon" with locale resolution client.locale -> studio.locale -> en-US. - If approved_end changes, Then previously scheduled reminders are cancelled and rescheduled; ensure at most one pre-resume reminder exists per freeze_id. - Emit integration event "pass.freeze.ending_soon.v1" with correlation_ids and record delivery observability data.
Suppression and Rescheduling of Class Reminders During Freeze
- Given a client has an active freeze [approved_start, approved_end), When a class reminder job for that client would fire within this window, Then do not send it while the freeze is active. - If the associated class_start is after approved_end, Then reschedule the reminder to the earliest valid time >= approved_end that preserves the original offset when possible and still occurs before class_start; otherwise skip and log reason "missed_due_to_window". - If class_start occurs within the freeze window, Then cancel the reminder and log reason "class_during_freeze"; no reschedule occurs. - Ensure zero class reminders are delivered during the freeze window; after unfreeze, ensure at most one reminder per class is sent. - All actions are logged on the client timeline with old_scheduled_at, new_scheduled_at (if any), reason, and job_id, and are deduplicated by key "class_reminder:{class_id}:{client_id}".
Suppression and Rescheduling of Promotional Campaigns During Freeze
- Given a client enters an active freeze, When a promotional campaign send targeting the client is scheduled within [approved_start, approved_end), Then suppress the send during the freeze. - If the campaign has a valid window after approved_end, Then reschedule the client's delivery to the first eligible slot after approved_end while respecting campaign frequency caps and quiet hours; otherwise mark as "expired_during_freeze" and do not send. - Ensure exactly zero promotional sends are delivered to the client during the freeze window; post-freeze, ensure at most one send per campaign is delivered and deduplicated by key "campaign:{campaign_id}:{client_id}". - All sends respect opt-in preferences and studio channel enablement; locale and template resolution follow client.locale -> studio.locale -> en-US. - Record suppression/reschedule actions and outcomes for Support and emit "marketing.suppressed_due_to_freeze.v1" and "marketing.rescheduled_post_freeze.v1" events with correlation to freeze_id.
Observability, Idempotency, and Integration Events for Freeze Communications
- For every freeze lifecycle stage (request_received, approved, denied, starting_soon, active, ending_soon, resumed), emit an integration event "pass.freeze.<stage>.v1" with stable event_id, freeze_id, client_id, pass_id, studio_id, occurred_at, and message_channel_outcomes. - Message sends use deterministic idempotency keys per stage and channel; retries, replays, and reprocessing do not create duplicate end-user deliveries. - Support Console exposes a per-freeze communications timeline with filters by channel and status, showing message_id, template_key, template_version, locale, queued_at, sent_at, provider_message_id, delivery_status, error_code, retry_count. - Webhook delivery implements exponential backoff with min 30s initial delay, jitter, and max 10 attempts; failures are visible in Support with last_error and next_retry_at. - System exports metrics (counts, dedup rate, failures, latency p50/p95) per stage and channel for monitoring.

Bonus Boosts

Offer automatic bonuses for larger pack purchases or usage milestones (e.g., buy 20, get +2; attend 10, unlock +1). Time-limited boosters (seasonal promos, rain-day specials) create urgency, with a Wallet badge that shows progress. Raises average order value and encourages consistent attendance.

Requirements

Bonus Rules Engine
"As a studio owner, I want to configure buy-and-attend bonus rules so that I can lift average order value and reward loyal attendance without manual tracking."
Description

A configurable rules engine for defining bonus offers based on purchase packs and usage milestones. Admins can create rules like “buy N get +M” or “attend N unlock +M,” set validity windows, eligibility (class types, instructors, locations), per-user caps, stacking behavior with other promos, and bonus expiration. Rules support segmentation (new vs returning clients), time-of-day/day-of-week constraints, and minimum price thresholds. Provides preview/simulation, versioning, and API endpoints for validation at checkout. Integrates with ClassNest checkout, Wallet, and reporting; enforces idempotency and conflict resolution when multiple rules apply.

Acceptance Criteria
Buy N Get M Rule — Award on Checkout with Caps and Expiry
Given an active rule R configured as buy N=20 of eligible pack types with bonus M=2 credits, validity window within 2025-10-01..2025-12-31, per-user cap=1, bonus expiration=30 days, eligible class types/instructors/locations selected, and minimum price threshold=$200 pre-discount And a user U who meets the rule’s segmentation And a cart with one eligible pack priced $220 purchased on 2025-11-10 When the Checkout Validation API is called before payment Then the API responds 200 with rule R marked applicable and computed bonus=2 And when payment for order O succeeds once Then exactly one bonus grant of 2 credits is written to U’s Wallet with expiry date = grant_time + 30 days and source=R, order_id=O And the Wallet badge progress updates to reflect the grant And a reporting event BonusGranted is emitted with {rule_id, user_id, order_id, bonus_amount, expiry_date} And if the payment webhook retries with the same idempotency key, no duplicate grant is created
Attend N Unlock M — Post-Attendance Award and Progress
Given an active rule R configured as attend N=10 eligible classes unlock M=1 credit, per-user cap=2, validity window active, eligible class types={Yoga, Pilates} And a user U with 9 attended sessions (status=Attended) in eligible classes and 2 no-shows and 1 cancelled session When U’s next eligible class attendance is marked Attended by the instructor Then U’s attended count reaches 10 and exactly 1 bonus credit is granted to U’s Wallet tied to attendance record A and rule R And no-shows and cancelled sessions did not count toward the milestone And the rule progress indicator shows 10/10 then resets to 0/10 for the next milestone (remaining cap=1) And a reporting event MilestoneReached is emitted with {rule_id, user_id, attendance_id, milestone=10, bonus_amount=1} And if the attendance status is toggled Attended->Cancelled after grant, the grant remains and an audit log records the change
Time Windows, Day-of-Week, Time-of-Day, and Timezone Enforcement
Given a rule R limited to Mon-Fri, 06:00–09:00 in the location’s timezone America/New_York and validity dates 2025-10-01..2025-10-31 And an eligible purchase occurs at 08:30 local time on a Monday When the Validation API evaluates eligibility Then R is applicable And for a purchase at 09:01 local time or on Saturday Then R is not applicable and the API returns reason="outside_time_window" And during a DST transition day, window evaluation uses local wall time so that 06:00–09:00 remains a 3-hour window And the simulation tool shows local times and timezone used in the decision
Segmentation and Minimum Price Thresholds
Given a rule R segmented to New Clients only (definition: users with 0 completed attendances) and minimum price threshold=$100 pre-discount, taxes/fees excluded And User U1 has 0 completed attendances And User U2 has ≥1 completed attendance And a pack with list price $120 and an applied 20% discount (final price $96) When U1 validates a checkout for the undiscounted pack at $120 Then R is applicable for U1 When U1 validates a checkout for the discounted price $96 Then R is not applicable with reason="below_min_price" When U2 validates a checkout at $150 Then R is not applicable with reason="segment_mismatch" And the simulation output shows matched/unmatched segmentation and threshold details
Stacking Behavior with Other Promotions
Given rule R1 configured as non-stacking with other promos and rule R2 configured as stackable with discounts; both have min price threshold evaluated pre-discount And a cart with an eligible pack list price $200 and a 10% promo code applied (final $180) When evaluating R1 and R2 Then R1 is excluded with reason="non_stacking_conflict" And R2 is applicable because the pre-discount price is $200 ≥ threshold And the API response lists applied_rules=[R2], excluded_rules=[{id:R1, reason:"non_stacking_conflict"}] And the Wallet grant reflects only R2 And per-user caps are enforced independently per rule
Conflict Resolution and Priority Across Multiple Applicable Rules
Given three rules R1, R2, R3 applicable to the same checkout And R1 and R2 are mutually exclusive by priority (R1 priority=100, R2 priority=80) and R3 is stackable When the Validation API evaluates Then R1 is selected over R2 due to higher priority and R3 also applies And the API returns applied_rules ordered by priority with total_bonus computed And the decision log includes {considered:[R1,R2,R3], applied:[R1,R3], excluded:[{id:R2, reason:"lower_priority"}]} And granting is idempotent across retries using idempotency_key so duplicate grants do not occur
Admin Preview/Simulation, Versioning, and Safe Rollout
Given an admin A with permission to manage Bonus Rules When A creates rule R v1 (Draft) and uses Preview with a sample user/cart context Then the preview shows whether R would apply, matched constraints, computed bonus, and reasons if not When A publishes v1 (Active) and later edits to create v2 (Draft) with a future start date Then v1 remains Active until v2 start, and awards granted under v1 remain unchanged after v2 activation And pausing R stops future awards without revoking past awards And all changes write to an immutable audit log with {actor, timestamp, before, after} And the Preview and Validation APIs return version_id in responses
Auto-Bonus Issuance & Ledger
"As a student, I want my earned bonus credits to appear instantly in my Wallet so that I can use them right away without contacting support."
Description

Automated issuance of bonus credits to the user’s Wallet when rule conditions are met, with real-time confirmation in the booking flow. Creates immutable ledger entries linking the bonus to the qualifying transaction or attendance event, tracks expiration, and supports partial consumption. Handles edge cases (refunds, cancellations, no-shows, chargebacks) with automatic reversal/adjustment logic. Ensures atomicity with payments, idempotent processing, and concurrency safety. Exposes admin tools for manual grant/revoke with audit trail.

Acceptance Criteria
Pack Purchase Bonus Issuance
Given an active bonus rule "Buy 20 get +2" applies to a qualifying pack and the member purchases that pack in a single order And the payment is successfully captured When the order transitions to paid Then exactly 2 bonus credits are issued to the member’s Wallet atomically with the base credits And a ledger entry is recorded with type=bonus_issue, quantity=2, rule_id, order_id, wallet_id, issued_at timestamp And the booking flow displays a confirmation within 2 seconds stating the bonus grant And processing is idempotent so any retried events do not create duplicate bonus entries
Attendance Milestone Bonus Issuance
Given a milestone rule "Attend 10 unlock +1" is active for the member And the member has 9 qualifying attended check-ins (excluding cancellations and no-shows) When the 10th check-in is marked as attended Then 1 bonus credit is issued to the member’s Wallet within 5 seconds And a ledger entry is recorded with type=bonus_issue, quantity=1, rule_id, attendance_event_id, wallet_id And duplicate processing of the same attendance event does not create additional bonus entries
Expiration Tracking and Partial Consumption
Given a bonus batch of 2 credits with expiration date E exists in the member’s Wallet When the member books a class requiring 1 credit Then the system deducts from the earliest-expiring bonus batch first (FEFO) And records a ledger entry type=bonus_consume for quantity=1 linked to the booking_id and bonus_batch_id And the remaining quantity in the batch is updated to 1 And on or after E, any unused credits in the batch are marked expired with a ledger entry type=bonus_expire and cannot be applied to bookings
Reversals for Refunds, Cancellations, No-Shows, and Chargebacks
Given a bonus was issued due to a qualifying purchase or attendance milestone When the qualifying purchase is fully refunded or chargebacked Then all unconsumed bonus credits tied to that qualification are revoked and a ledger entry type=bonus_revoke is created linking to refund_id or dispute_id And if some bonus credits were consumed, compensating ledger entries are created to reverse up to the revoked amount without allowing a negative Wallet balance And when a booking is cancelled within a penalty-free window and returns the base credit, milestone counts and related bonus eligibility are recalculated without duplicating or orphaning bonuses And no-show events do not count toward attendance milestones
Payment Atomicity and Failure Handling
Given a qualifying pack purchase is attempted When payment authorization fails, is voided, or capture does not occur Then no bonus credits are issued and no bonus ledger entry is created And if the payment later succeeds via retry, exactly one bonus issuance is recorded And if the order is partially refunded such that the pack quantity falls below the rule threshold, all remaining unconsumed bonus credits issued for that purchase are revoked with a linked adjustment ledger entry
Concurrency and Idempotency Under Parallel Processing
Given the same qualifying event (order_paid or attendance_attended) may be delivered multiple times or processed concurrently When up to 5 concurrent processing attempts occur within a 10-second window Then exactly one bonus issuance ledger entry is created for that unique event And a stable idempotency key derived from event_id and rule_id prevents duplicates And the Wallet balance reflects a single change with eventual consistency within 5 seconds
Admin Manual Grant/Revoke with Audit Trail
Given an admin with permission wallet.manage initiates a manual grant of 3 bonus credits to a member When the admin provides reason, note, and optional expiration date and submits Then the credits are added to the Wallet respecting the provided expiration and a ledger entry type=manual_grant is recorded with actor_id, reason_code, note, wallet_id, timestamp And manual revokes subtract only from unconsumed bonus credits without driving the balance below zero and record a ledger entry type=manual_revoke (linked to the original grant when provided) And all manual actions appear in the admin audit log filterable by member, actor, action type, and date range
Booster Campaigns Scheduler
"As a studio owner, I want to schedule time-limited booster campaigns so that I can create urgency and fill classes during slow periods."
Description

Scheduling and management of time-limited boosters (e.g., seasonal promos, rain-day specials). Admins can define start/end times, recurrence, target audience segments, eligible products/classes, and bonus payloads. Supports campaign caps (budget, redemptions per user), pause/resume, manual trigger, and conflict resolution priority across overlapping campaigns. Provides previews, countdown timers for booking pages, and integration with notifications and Wallet for urgency signals.

Acceptance Criteria
Schedule, Preview, and Auto-Activate a Time-Limited Booster
Given an admin creates a booster campaign with a start datetime and end datetime, When current system time reaches the start datetime, Then the campaign status transitions to Active without manual intervention. And When current system time reaches the end datetime, Then the campaign status transitions to Expired and the campaign no longer applies bonuses to transactions initiated after the end datetime. And Given the campaign is scheduled, When the admin clicks Preview, Then the booking page preview shows the campaign banner and a countdown timer to start or end that matches campaign timings to the second. And Given a notification window is configured (e.g., 3 hours before end), When that threshold is reached, Then notifications are dispatched to the targeted segment via configured channels and delivery logs are available in the campaign activity log.
Recurrence Windows Within Campaign Dates
Given a campaign defines recurrence windows (e.g., Saturday 08:00–12:00), When current time falls within any defined window between the campaign start and end dates, Then the campaign is considered Active for eligibility; otherwise, it is Inactive. And When the admin updates recurrence rules, Then the new rules take effect for eligibility checks within 60 seconds and are reflected in the campaign calendar view. And Given overlapping recurrence windows are defined, When evaluating eligibility, Then the system treats overlapping ranges as a single continuous active window.
Targeting Segments and Eligible Products/Classes
Given a campaign targets specific audience segments and lists eligible products/classes, When a user belongs to any targeted segment and purchases an eligible product or books an eligible class during an active window, Then the bonus payload is eligible to be applied. And When a user is excluded by segment rules or the item is not on the eligible list, Then no bonus is applied and a reason code (e.g., segment_mismatch, item_ineligible) is recorded with the transaction. And Given include and exclude segment rules, When both match a user, Then exclude rules take precedence and the evaluated segment snapshot is stored with the transaction record.
Conflict Resolution Across Overlapping Campaigns
Given two or more Active campaigns are eligible for the same transaction, each with a priority value and an exclusivity flag, When evaluating bonuses, Then campaigns are processed in descending priority order and the first exclusive campaign applied prevents lower-priority campaigns from applying. And Given campaigns are non-exclusive, When stacking is allowed, Then multiple campaigns may apply up to the configured stack limit, and the order of application is deterministic (priority, then earliest start datetime, then lowest campaign ID). And When a campaign lists other campaigns in a "cannot stack with" list, Then those campaigns are skipped regardless of relative priority and a skip reason is logged. And After evaluation, Then the transaction log records which campaign(s) applied, which were skipped, and the resolution path for auditability.
Bonus Payload Application and Wallet Updates
Given a campaign defines a bonus payload (e.g., +2 credits), When a qualifying transaction is completed successfully, Then the user's Wallet is credited with the bonus within 5 seconds and the Wallet badge reflects the new balance/progress. And When the qualifying transaction is refunded or voided within the refund window, Then the associated bonus is automatically revoked and the Wallet badge updates within 5 seconds. And Given retries or duplicate callbacks occur, When processing the same campaign-user-transaction tuple, Then the system is idempotent and does not issue duplicate bonuses, recording a duplicate_ignored reason if applicable.
Campaign Caps and Per-User Limits Enforcement
Given a campaign has a total budget cap and a per-user redemption cap, When issuing a bonus would exceed either cap, Then the bonus is not issued and a cap_reached reason is logged while the transaction proceeds without the bonus. And When the total budget cap is reached, Then the campaign shows a Cap Reached indicator in admin and no further bonuses are applied to new transactions. And Given concurrent qualifying transactions, When caps are enforced, Then no over-issuance occurs and the final issued total does not exceed the configured caps.
Pause, Resume, and Manual Trigger Execution
Given an Active campaign, When an admin clicks Pause, Then the campaign immediately stops applying to new transactions and displays status Paused in the admin UI. And When an admin clicks Resume on a paused campaign, Then eligibility checks resume according to schedule and recurrence within 60 seconds and the status updates to Active. And Given a campaign within its date range, When an admin selects Manual Trigger for a specific transaction ID or selected cohort and confirms, Then the system immediately evaluates and applies the campaign to the target(s), respecting caps and conflict rules, and logs a manual_trigger event with a preview diff of expected vs. applied bonuses.
Wallet Progress Badge & UI
"As a student, I want to see clear progress toward my next bonus so that I stay motivated to attend and purchase more."
Description

Mobile-first UI elements that display users’ progress toward the next bonus across Wallet, booking pages, and receipts. Includes a progress bar, badge states (locked/approaching/unlocked), tooltips explaining eligibility, and deep links to qualifying classes/packs. Updates in real time post-purchase or check-in, supports localization and accessibility (WCAG AA), and degrades gracefully on low bandwidth. Integrates with existing ClassNest Wallet and instant booking pages.

Acceptance Criteria
Mobile Cross-Surface Progress Display
Given a signed-in user with at least one active bonus program and a device width ≤ 375px When they open Wallet, a qualifying booking page, and a receipt for a qualifying purchase Then each surface displays the progress bar and badge above the first screenful without scrolling And the percent complete and remaining-until-bonus values are identical across all three surfaces (±0.5%) And the component uses existing Wallet data endpoints and session with no additional authentication prompts And layout, spacing, and typography match the design tokens on mobile
Badge State Logic and Visuals
Given a user has a next-bonus target defined When progress < 60% and remaining-until-bonus > 0 Then the badge state is Locked, the progress bar uses neutral color, and no claim CTA is shown When 60% ≤ progress < 100% or remaining-until-bonus ≤ 2 units Then the badge state is Approaching, the progress bar uses accent color, and microcopy shows "X to go" When progress ≥ 100% and the bonus is unclaimed Then the badge state is Unlocked and a "Claim bonus" CTA or auto-applied confirmation is shown When an unlocked bonus is claimed or auto-applied Then the badge resets to the next bonus cycle and progress is recalculated within 3 seconds
Tooltip and Deep Links for Eligibility
Given the user taps or focuses the badge or info icon When the tooltip opens Then it explains eligibility in plain language including required quantity, qualifying item types, and the expiry date/time in the user’s time zone And it contains a "See qualifying options" deep link that opens the booking page filtered to qualifying classes/packs And the tooltip can be dismissed by tapping outside, pressing ESC, or using system back And the tooltip appears within 300 ms and is announced by screen readers
Real-Time Progress Update After Purchase or Check-in
Given a user completes a qualifying purchase or is checked in to a qualifying class When the backend confirms the transaction Then the progress bar value and badge state update on the current screen within 3 seconds without full-page reload And other open Wallet, booking, and receipt screens reflect the update within 5 seconds or on next visibility/focus And if confirmation does not arrive within 60 seconds, an error toast appears and the UI reverts to the last confirmed value
Localization and Internationalization
Given the user’s locale is set (e.g., en-US, es-ES) When the badge, progress, and tooltip render Then all strings are sourced from i18n keys with correct translations and pluralization And numbers, dates, and times are formatted per locale and time zone And labels do not truncate or overlap on devices as small as 320×568 points with up to 30% string expansion And if a translation is missing, English is used as the fallback
Accessibility WCAG AA Compliance
Given users navigate with screen readers, keyboard, or switch controls When interacting with the badge, progress bar, and tooltip Then each interactive element has an accessible name/role and is reachable in a logical focus order And the progress bar exposes aria-valuemin, aria-valuenow, and aria-valuemax and a textual percentage And text/icon contrast meets WCAG AA (≥ 4.5:1) and state is not conveyed by color alone And touch targets are at least 44×44 dp and visible focus indicators are provided
Low-Bandwidth Graceful Degradation
Given a Slow 3G network (≈400 kbps down, ≈400 ms RTT) or temporary offline state When loading Wallet, a booking page, or a receipt Then primary text and a skeleton of the badge/progress render within 2.5 seconds And if the progress value is not retrieved within 3 seconds, a cached value is shown if available or a "Tap to refresh" control with explanatory text is displayed And non-critical images/icons are deferred; the progress bar degrades to percentage text-only if assets fail And no element shifts position by more than 100 px after initial paint
Bonus Notifications & Reminders
"As a student, I want timely notifications about nearing and unlocked bonuses so that I don’t miss rewards or expirations."
Description

Automated SMS/email notifications for milestone progress and bonus unlocks, plus pre-expiry reminders for time-limited boosters and bonus credits. Template-driven, localizable content with dynamic variables (remaining classes to unlock, expiry date, eligible classes) and deep links to book. Respects user consent/preferences, includes send throttling and quiet hours, and supports A/B testing. Leverages ClassNest’s existing messaging infrastructure and logs delivery/engagement for analytics.

Acceptance Criteria
Milestone Progress Notification Trigger
Given a student with an active attendance-based milestone (e.g., attend 10 unlock +1) and channel consent enabled When an eligible class attendance is recorded in ClassNest Then a progress notification is enqueued within 5 minutes to the student's preferred channel (SMS or email) And the message includes dynamic variables: remaining_to_unlock, current_progress (e.g., 7/10), and a deep link to book And if within configured quiet hours, the send is deferred to the next allowed delivery window in the student's timezone And if the daily throttle limit is reached, the message is skipped with a throttle reason logged
Bonus Unlock Notification
Given a student unlocks a bonus (e.g., buy 20 get +2 or attend 10 unlock +1) and has at least one allowed messaging channel When the bonus unlock event is committed to the wallet Then exactly one unlock notification is sent within 5 minutes And the message includes: bonus_amount, bonus_type, expiry_date (if any), eligible_classes, and a deep link to book And duplicate notifications for the same unlock_id are prevented for 24 hours And if preferred channel is not consented, the system falls back to the next consented channel; otherwise, the send is skipped with reason recorded
Booster/Bonus Pre-Expiry Reminders
Given configuration defines reminder windows at 72h and 24h before expiry And a student has a time-limited booster or bonus credits with remaining_credits > 0 When the reminder scheduler runs Then one reminder is sent at the 72h mark and one at the 24h mark (max 2 per asset) And messages include: remaining_credits, expiry_date_time, eligible_classes, and a deep link to book And sends respect user consent and quiet hours; deferred sends occur at the next allowed window And no reminder is sent if remaining_credits = 0 or the asset is already expired
Template, Localization, Dynamic Variables, and Deep Links
Given the student's locale and the active A/B test variant assignment When rendering a notification Then the localized template matching the channel and variant is used (HTML+text for email; GSM-safe text for SMS) And required dynamic variables {remaining_to_unlock, current_progress, bonus_amount, expiry_date, eligible_classes, deep_link} are validated; if any required variable is missing, the send is aborted and an error is logged And the deep link routes to the booking page, includes contextual parameters (e.g., user token, bonus_id/booster_id), and UTM/campaign tags identifying scenario and variant And right-to-left locales render with correct directionality and punctuation spacing
Consent, Preferences, Quiet Hours, and Time Zones
Given per-channel consent and notification preferences are stored for the student When preparing to send any progress, unlock, or reminder notification Then the system sends only via channels with active consent and matching preference category And if the preferred channel is not consented, it falls back to any other consented channel; if none, it skips and records the skip reason And messages are not sent during configured quiet hours for the student's timezone; they are deferred to the next allowed window And if the student's timezone is unknown, the studio's timezone is used as fallback
Send Throttling and Event Deduplication
Given throttle configuration of max 1 progress notification per user per 24 hours and max 3 total notifications per user per 7 days for this feature When multiple eligible events occur for a student Then the system enforces the per-event and weekly caps and logs throttle decisions And events with the same idempotency key (e.g., attendance_id, unlock_id) are deduplicated for 24 hours And if a progress event and an unlock event occur within 10 minutes, only the higher-priority unlock notification is sent
Delivery, A/B, and Engagement Analytics Logging
Given an outbound message is attempted via the existing messaging infrastructure When the message lifecycle progresses (enqueue, send, deliver/bounce, open, click, conversion) Then the system logs: message_id, user_id, scenario_type (progress|unlock|expiry), channel, template_id, variant_id, locale, timestamps, quiet_hours_deferral, throttle_applied, and provider response codes And email opens, link clicks, SMS STOP/unsubscribe, and booking conversions within 7 days of click are captured and attributed to the message and variant And A/B testing supports configured splits including a holdout; variant assignment is stable per user for 30 days and recorded on each event And all logs are accessible in ClassNest analytics and export API; no new messaging provider is introduced
Bonus Performance Analytics
"As a studio owner, I want to analyze the performance of bonuses and campaigns so that I can optimize offers and spend."
Description

Dashboards and exports to measure Bonus Boosts impact: average order value uplift, attach rate, redemption rate, attendance frequency lift, no-show change, revenue and margin contribution, and campaign-level performance. Provides filters by time, location, instructor, pack type, and segment. Supports cohort analysis, funnel views from exposure to redemption, and CSV export/API for BI tools. Aligns with existing ClassNest analytics, ensuring consistent attribution to purchases and attendances.

Acceptance Criteria
AOV Uplift and Attach Rate with Filters
Given I am viewing Bonus Boosts analytics with an applied date range and selected org time zone When I apply filters for location, instructor, pack type, and customer segment Then the dashboard displays AOV uplift (%) = round(((AOV_exposed - AOV_baseline) / AOV_baseline) * 100, 2), where AOV_exposed = revenue from purchases in sessions exposed to a Boost / number of such purchases, and AOV_baseline = revenue from purchases not exposed to a Boost in the same filter context / number of such purchases And the dashboard displays attach rate (%) = round((purchases_with_boost / eligible_exposed_purchases) * 100, 2) And numerator and denominator values for each metric are available via tooltip or drilldown and match the counts shown in detailed tables And if any denominator equals 0, the metric displays as "—" and is excluded from aggregates and trend lines And changing or clearing filters immediately recomputes metrics to reflect the selected scope using the selected time zone boundaries
Exposure-to-Redemption Funnel
Given funnel stages are defined as Exposed, Boost Attached at Checkout, and Bonus Redeemed at Attendance When I load the funnel view for a selected period and filters Then each stage count equals the distinct customer-event count per stage using ClassNest identity resolution, with no double-counting across multiple sessions in the same stage And stage-to-stage conversion rates are computed as round((stage_n / stage_(n-1)) * 100, 2) with denominators > 0, otherwise display "—" And clicking a stage enables drilldown to a list of customers/events that exactly sum to the stage count under the current filters And the same funnel counts for the same filters match an exported CSV and the analytics API response within 0.1%
Attendance Frequency Lift and No‑Show Change
Given a cohort of customers with at least one Boost exposure in the selected period When I compare pre vs post metrics using a 28‑day window before first exposure (pre) and a 28‑day window after first exposure (post) Then attendance frequency lift (%) = round(((avg_sessions_post - avg_sessions_pre) / avg_sessions_pre) * 100, 2), where averages are per-customer session counts in the respective windows And no‑show change (pp) = round((no_show_rate_post - no_show_rate_pre) * 100, 2), where no_show_rate = no_shows / scheduled_attendances And customers without sufficient pre data (scheduled_attendances_pre = 0) are excluded from frequency lift and labeled "N/A" in drilldowns And results respect current filters and match ClassNest baseline attendance analytics when Boost filters are removed
Revenue and Margin Contribution Attribution
Given revenue and margin definitions are aligned with existing ClassNest analytics When I view revenue and margin contribution for Bonus Boosts under any filter set Then revenue attributed to Boosts equals the sum of recognized revenue from purchases where a Boost was attached plus any priced redemptions as defined in baseline analytics And margin contribution equals baseline margin calculation applied to the same set of purchases and redemptions, using the organization’s configured cost assumptions And totals for revenue and margin by day and by campaign exactly equal the sum of their respective detailed rows (within rounding), with rounding to 2 decimals and a displayed currency symbol per org settings And for the same time range and filters, totals reconcile to within 0.1% of the existing ClassNest revenue/margin reports
Campaign‑Level Performance for Time‑Limited Boosters
Given multiple Bonus Boost campaigns with defined start and end dates When I open the campaign performance view and select a campaign Then the view shows AOV uplift, attach rate, redemption rate, revenue, margin contribution, and active dates specific to that campaign under current filters And metrics include numerator/denominator and date stamps, and honor campaign time windows (no data outside start/end) And expired campaigns are labeled "Ended" and future campaigns are labeled "Scheduled" And switching campaigns or filters updates metrics and the change history while preserving the same calculation definitions
Cohort Analysis by First Exposure Date
Given cohorting is set to First Boost Exposure month When I view the cohort grid and select intervals (Week 1, Week 2, Week 4, Week 8) Then each cohort row aggregates customers first exposed in that month and shows cumulative redemption rate and average attendances per customer at each interval And cohort sizes, rates, and averages match drilldown lists and exported data for the same filters And changing cohort basis to First Boost‑Attached Purchase re-slices cohorts and recomputes metrics using the same formulas
CSV Export and Analytics API Parity
Given I have permission to export analytics When I export CSV for the current view Then the file includes a header row with stable column names, data types consistent with the data dictionary, ISO‑8601 timestamps in the selected time zone, and one row per grain (e.g., day x campaign or customer x event) matching the on‑screen table And the row count and metric totals in the CSV match the UI within 0.1% And the analytics API endpoint returns the same data as the CSV for the same filters, with documented pagination, filter parity, and HTTP 429 on rate‑limit exceed And exports over 100k rows are delivered via download link or email with status tracking, and partial exports are not produced
Fraud & Abuse Safeguards
"As an admin, I want safeguards that detect and prevent bonus abuse so that promotions remain fair and profitable."
Description

Controls to prevent gaming and unintended costs: per-user and per-period caps, device/email/phone deduping, exclusion rules for staff/comped sessions, velocity checks, and anomaly detection on redemptions. Adds admin flags for review, override tools, and alerts on suspicious patterns (e.g., rapid refunds post-bonus). Ensures bonuses don’t stack with incompatible discounts, and that waitlist auto-offers and rescheduled sessions are treated consistently.

Acceptance Criteria
Per-user and per-period bonus caps enforced
Given a user has reached the configured per-period bonus cap When an action would grant an additional bonus Then the bonus is not granted, a message "Bonus cap reached for this period" is shown, and an event bonus_cap_block is logged with userId, period, attemptedBonusId, and timestamp Given period boundaries are defined by the account timezone (e.g., calendar month) When a new period starts Then the user’s bonus counters reset and the Wallet badge progress reflects the new period within 5 minutes Given an admin updates cap values When changes are saved Then new caps apply to subsequent grants within 60 seconds and changes are versioned for audit with actorId and changeSet Given a bonus attempt is denied due to cap When viewing the Wallet and transaction history Then the attempt appears as "Blocked by cap" with reason code and no change to balance
Identity deduping across device, email, phone, and payment instruments
Given two accounts share a verified phone number or payment card fingerprint When either account attempts to earn a person-level bonus within a 30-day window Then the bonus is denied and both accounts are flagged with reason identity_dedupe_match and match signals are stored Given the same device fingerprint creates three or more new accounts within 24 hours that attempt bonus redemptions When the third attempt occurs Then the attempt is blocked and an admin alert is generated with deviceId, IP, and related accountIds Given a user changes email but retains the same verified phone When evaluating bonus eligibility Then the system treats the user as the same identity for deduping and prevents duplicate earning Given an admin allows a previously flagged match When the same identity pair recurs Then bonuses are not blocked for that pair while the allowlist entry is active and the decision is audit-logged
Exclusion of staff, test, and comped sessions from bonus accrual
Given a booking is marked staff, instructor, test, or comped per configuration When the session is completed Then it does not contribute to bonus accrual or milestone counts and is labeled excluded in reports and Wallet history Given a user holds a comped pack with price $0 When they hit a milestone threshold Then no bonus is granted and the Wallet badge excludes comped usage from progress Given exclusion rules are updated When reprocessing is triggered Then historical bonus accruals for the last 30 days are recalculated and any affected bonuses are reversed with clear reversal entries
Velocity checks for rapid accruals and redemptions
Given thresholds are configured (e.g., >3 bonuses granted per user in 10 minutes or >5 redemptions per device in 15 minutes) When a threshold is exceeded Then subsequent grants/redemptions are blocked for a configurable cooling period and an admin flag is created with thresholdId, counts, and window Given a block is active When the user attempts further actions Then the UI shows "Temporarily blocked due to unusual activity" and attempts are logged without changing balances Given the cooling period elapses with no further triggers When the user retries Then processing resumes and the block is cleared with an unblock event
Anomaly detection and alerts for suspicious bonus patterns
Given a pack purchase generated a bonus and the pack is refunded within 48 hours When the refund is processed Then the granted bonus is automatically revoked or frozen per configuration, the case is flagged, and a real-time alert is sent to admins with purchaseId and bonusId Given a user’s redemption rate deviates by more than 3 standard deviations from their 30-day baseline When the deviation is detected Then the account is flagged for review and future bonuses enter a pending state until cleared Given an anomaly flag is resolved by an admin When the resolution is saved Then pending bonuses are either released or voided accordingly and an audit trail entry is recorded with actorId, decision, and rationale
Admin review and override with full audit trail
Given a flagged fraud/abuse case exists When an admin opens the case Then they can view a timeline of actions, linked users/devices, triggered rules, and proposed actions (block/revoke/freeze) in a single view Given an admin decides to override and grant or deny a bonus When the action is confirmed Then the Wallet balance is updated within 60 seconds, the user is notified if configured, and before/after values are audit-logged with immutable eventIds Given a reversal or freeze is executed When audit logs are exported Then the events are present with filters by date range, rule, userId, deviceId, and outcome
Non-stacking with incompatible discounts; consistent waitlist and reschedule handling
Given a booking has a discount or promo marked incompatible with Bonus Boosts When calculating benefits Then only the single highest-value benefit applies, the applied ruleId is stored, and no additional bonuses are applied Given a user accepts a waitlist auto-offer and attends When counting toward milestones Then the attendance counts identically to a standard booking and respects caps and exclusions Given a user reschedules within the allowed window When evaluating period caps and milestone eligibility Then the original booking date is used for evaluation and duplicate accrual is prevented Given a no-show or late cancellation per policy When tallying milestone counts Then no accrual is added and any pre-applied attendance-tied bonus is rolled back with a reversal entry

Offline Tap Sync

Enable check-in and credit decrements even without internet. The instructor’s device validates Wallet passes offline and syncs usage once reconnected—preventing double use with secure, single-use tokens. Classes run smoothly in basements, parks, and patchy venues.

Requirements

Device Key Provisioning & Secure Storage
"As a studio owner, I want to enroll and secure my instructors’ devices for offline check-in so that only authorized devices can validate passes without internet."
Description

Implement a device enrollment flow that issues a device-specific cryptographic identity and securely stores keys (Secure Enclave/Android Keystore). The server provisions and can revoke device credentials. Enforce integrity checks (e.g., jailbreak/root detection, hardware-backed storage) and key rotation policies. This enables the device to validate passes and sign offline redemptions, ensuring only authorized devices can operate Offline Tap Sync within ClassNest. Expected outcome: hardened device trust, revocation controls, and minimal risk if a device is lost.

Acceptance Criteria
Successful Hardware-Backed Device Enrollment
Given a supported device with internet connectivity and a signed-in instructor When the instructor taps “Enable Offline Tap” Then the app generates an asymmetric key pair in a hardware-backed keystore/Secure Enclave with a non-exportable private key And the app obtains platform attestation for the key and sends attestation plus the public key to the server And the server validates the attestation and registers the device, returning a device credential ID and offline capability metadata valid for up to 24 hours And the app securely stores the credential ID and associates it to the key alias And enrollment completes within 30 seconds and the UI confirms Offline Tap is enabled
Enrollment Blocked on Compromised or Unsupported Device
Given a device that is rooted/jailbroken, running in an emulator, has an unlocked bootloader, or lacks hardware-backed keystore support When the user attempts to enroll for Offline Tap Then attestation validation fails and the server declines registration And the app retains no usable key material and displays an error explaining the device is unsupported for Offline Tap And an audit event is recorded with a reason code without exposing sensitive device details
Server-Initiated Credential Revocation Enforcement
Given an enrolled device with active offline capability When an administrator revokes the device credential in the console Then upon next network contact or revocation-list refresh (whichever occurs first and within 5 minutes of connectivity), the app blocks new offline redemptions and marks the device as revoked And the app deletes or permanently disables access to the private key and clears offline capability metadata And any redemptions signed after the revocation timestamp are rejected on sync with a clear error status shown to the user
Key Rotation on Schedule and On-Demand
Given an enrolled device whose key age exceeds 90 days or the server issues an immediate rotate directive When the app is online Then the app generates a new hardware-backed key pair and submits attestation; the server activates the new public key and schedules deactivation of the old key after a 7-day overlap And offline redemptions signed by the old key during the overlap are accepted; signatures with the old key after deactivation are rejected And rotation completes end-to-end within 60 seconds with no more than 30 seconds where offline signing is unavailable
Offline Redemption Signed and Verified on Sync
Given the device is enrolled, has a valid offline capability window, and is offline When the instructor checks in a pass Then the app validates the pass locally, ensures it is unused in the local cache, and signs a redemption payload containing device credential ID, class/session ID, pass ID, a cryptographically strong nonce, and a UTC timestamp And upon reconnection the server verifies the signature against the registered public key, enforces nonce uniqueness and timestamp within +/- 5 minutes skew, and rejects any duplicate or replayed redemption And successful redemptions are applied to bookings/credits; failures are surfaced with actionable error messages
Offline Operation TTL and Lost/Stolen Device Containment
Given the device has been operating offline When the time since the last successful capability refresh exceeds 24 hours or the cached revocation list is expired Then the app prevents new offline redemptions until connectivity is restored and capability is refreshed And offline signing is refused while the device is marked revoked locally or after 5 consecutive failed device unlock/biometric attempts within 10 minutes
Secure Key Access Controls and App Preconditions
Given the app is installed on a supported device When creating or using the device private key Then the key is hardware-backed and non-exportable, and cryptographic operations require the device to be unlocked And if the device has no lock screen enabled or a debugger/emulator is detected, key creation/use is blocked with a clear error and no keys are persisted And uninstalling the app or clearing app data removes stored credential IDs and renders the key unusable for future signatures
Offline Pass Validation (Wallet & QR)
"As an instructor, I want to scan a student's pass and see immediately if it's valid even with no signal so that check-in lines move quickly."
Description

Enable on-device verification of Apple Wallet/Google Wallet passes and ClassNest QR codes without internet. Cache issuer public keys and pass metadata, verify signatures, check validity windows, class/product mapping, and basic entitlements. Provide immediate pass status (valid, expired, out-of-credits, revoked) and block reuse attempts where applicable. Integrates with ClassNest’s booking and package models to reflect eligibility while offline. Expected outcome: sub-second scan latency and accurate pass validation in no-connectivity environments.

Acceptance Criteria
Offline Wallet Pass Validation at Class Check-In
Given the device has no internet connectivity and has cached issuer public keys and pass metadata updated within the last 30 days When an Apple Wallet or Google Wallet pass for the current class session is tapped/scanned Then the pass signature is verified locally using the cached key, the product/class mapping matches the session, and entitlements are checked And a result is displayed as "Valid" in ≤ 700 ms with visual/haptic confirmation And no network calls are attempted during validation And an offline validation event with an idempotency key is persisted to the outbox queue
Offline QR Code Validation at Class Check-In
Given the device is offline and holds the ClassNest QR signing public key and pass metadata cached within the last 30 days When a ClassNest QR code is scanned for the current class session Then the payload is decoded and its signature/HMAC is verified locally, class/product mapping is validated, and entitlements are checked And the result is displayed as "Valid" in ≤ 700 ms And validation uses the offline path only (no network attempts) And an offline validation event with an idempotency key is persisted to the outbox queue
Prevention of Offline Pass Reuse via Single-Use Token
Given a pass/QR represents a specific class instance with a single-use token When it is successfully validated once offline Then the token is atomically marked consumed locally and bound to device, with timestamp and class instance And any subsequent scans of the same token while offline are rejected as "Already used" in ≤ 400 ms And upon reconnection, the first successful validation is accepted server-side; duplicates are discarded via idempotency; no double credit decrement occurs
Offline Entitlement and Credit Decrement for Packages
Given a student holds a package with N remaining credits cached locally and eligible for the class When their pass is validated offline Then 1 credit is decremented atomically in the local cache and shown as "Credits left: N-1" And if N = 0 prior to scan, the pass is rejected as "Out of credits" in ≤ 700 ms And local storage prevents negative balances and is crash-safe (decrement durable within 100 ms) And the decrement event is queued with an idempotency key for later sync
Offline Detection of Expired, Revoked, or Out-of-Window Passes
Given cached validity windows, class/product eligibility, and a revocation list timestamped within the last 30 days When a pass is scanned offline Then if current device time is outside the pass validity window (±5-minute drift tolerance), the pass is rejected as "Expired/Not yet valid" And if the pass ID exists in the cached revocation list, it is rejected as "Revoked" And if class/product mapping does not match, it is rejected as "Not eligible for this class" And rejection results are displayed in ≤ 700 ms and logged to the outbox
Cached Key and Metadata Freshness and Grace Behavior
Given issuer public keys, QR signing keys, metadata, and revocation lists are cached with a 30-day TTL and a 72-hour grace window When offline validation occurs and cache age ≤ 30 days, validation proceeds normally And when cache age is > 30 days and ≤ 33 days (grace), validation proceeds with a warning banner and events marked "grace-mode" And when cache age > 33 days, scans return "Cannot validate: stale keys" in ≤ 1 s without attempting network calls And upon next connectivity, a refresh is attempted automatically within 30 seconds
Deferred Sync and Reconciliation After Reconnection
Given there are offline validation and credit decrement events in the outbox When the device regains connectivity Then the client syncs queued events within 60 seconds, preserving order per class instance, using idempotency keys And the server applies decrements/attendance exactly once; duplicates are ignored And conflicts are handled: if a pass was revoked during the offline period, the server marks the event "revoked post-use", rolls back the credit decrement, and notifies the device to update UI within 60 seconds And the local outbox is cleared only after 2xx acknowledgments; failures are retried with exponential backoff up to 24 hours
Single-Use Token Generation & Redemption
"As a student, I want my pass to be securely redeemed once per class so that it cannot be reused if scanned again offline."
Description

Support secure, single-use redemption by consuming a unique, signed token per check-in while offline. Tokens include pass ID, nonce/counter, device ID (optional), validity window, and signature. The app maintains a pre-fetched pool and marks each token as spent upon scan; spent tokens cannot be reused on the same or other devices. Upon reconnection, the server reconciles spent nonces to prevent double-dips. Expected outcome: offline check-ins decrement credits exactly once, eliminating duplicate use even across patchy venues.

Acceptance Criteria
Offline Check-In Consumes Single-Use Token
Given the instructor device is offline and has an unspent token for Pass ID P When the instructor taps to check in Student S for Class C using Pass ID P Then the app verifies the token signature and validity window locally And marks the token as spent atomically before showing success And decrements the local remaining credits for Pass ID P by 1 And prevents reuse of the same token on that device And completes from tap to confirmation in ≤300 ms Given the app is immediately force-closed and reopened When the same token is presented again Then the app rejects it as already spent
Token Pool Prefetch, Depletion, and Auto-Refresh
Given the device is online and the token pool low-water mark is 10 and target size is 50 per pass When an instructor opens the roster for upcoming classes Then the app prefetches tokens to reach the target size for all eligible passes Given the device goes offline When the remaining tokens for a pass drop to 0 Then the app blocks new check-ins for that pass and displays “No offline tokens available—reconnect to continue” And it never issues duplicate or placeholder tokens Given the device comes online again When the remaining token count for any pass is below the low-water mark Then the app automatically replenishes up to the target size in the background
Cross-Device Double-Use Prevention and Reconciliation
Given Devices A and B each hold pre-fetched tokens for the same Pass ID P with 1 remaining credit When A spends a token at 10:00 and B spends a different token at 10:01 while both are offline Then upon first reconnection, the server accepts the earliest redemption up to available credits and rejects the later one with reason “insufficient credits” And both devices receive reconciliation results and update the check-in statuses to Accepted/Rejected accordingly And the final server-side credit balance for Pass ID P decrements exactly once Given a token with nonce N for Pass ID P is reported as spent by Device A When Device B attempts to redeem a token with the same nonce N Then the server rejects Device B’s redemption as duplicate during reconciliation And Device B displays “Already redeemed” after sync
Token Validity Window and Clock Skew Handling
Given a token includes not-before T1 and not-after T2 and device time skew tolerance is ±5 minutes When the device local time is within [T1−5m, T2+5m] Then offline redemption is allowed When the device local time is outside that window Then offline redemption is denied with “Token not yet valid” or “Token expired” And denial occurs in ≤200 ms
Device-Bound Token Enforcement (Optional Mode)
Given organization settings enable device binding And Device ID is embedded in tokens When Device B attempts to redeem a token bound to Device A while offline Then the app rejects the redemption with “Token bound to a different device” Given organization settings disable device binding When Device B redeems a valid token generated for any device Then offline redemption proceeds, subject to later server reconciliation
Tamper Detection and Secure Local Spend Marking
Given a token’s payload is altered (e.g., pass ID or validity window modified) When scanned offline Then signature verification fails and the app denies check-in with “Invalid token” And no credit is decremented Given a token is marked spent locally When the app is killed or the device restarts before sync Then the token remains marked spent and cannot be reused And the spent state survives process death and device reboot
Sync on Reconnection Updates Credits and Audit Log
Given the device has K=100 offline redemptions queued When it reconnects to the internet with connectivity ≥1 Mbps Then it uploads all spent nonces with timestamps, device ID, and class IDs And receives per-item Accept/Reject results And applies all updates locally within ≤10 seconds And displays a sync summary with counts Accepted, Rejected, Retried, and last sync time And retries transient failures up to 3 times with exponential backoff
Local Offline Ledger & Idempotent Sync
"As an instructor, I want my offline check-ins to be safely stored on my device so that nothing is lost before I regain connectivity."
Description

Record each offline action (scan, credit decrement, undo) in an append-only, encrypted local ledger with unique operation IDs, timestamps, token nonces, device ID, and operator context. Ensure operations are idempotent and resilient to app restarts and power loss. Provide a short undo window with proper compensating entries. Data remains private and is only transmitted during sync. Expected outcome: durable offline records that reconcile cleanly without duplicates or data loss.

Acceptance Criteria
Append-Only Encrypted Ledger With Required Fields
Given the device is offline and an instructor records a check-in/credit decrement When the app writes the operation to local storage Then it appends a new ledger entry without modifying or deleting any prior entry And the entry includes: operation_id (globally unique), op_type (scan|credit_decrement|undo), created_at (UTC ISO-8601), token_nonce, device_id, operator_id, class_id/session_id And the ledger at rest is encrypted such that plaintext is not readable via filesystem inspection And tampering with an entry causes integrity verification to fail and the entry is not applied
Idempotent Sync With At-Least-Once Delivery
Given one or more pending ledger entries exist and connectivity is restored When the app initiates sync and must retry due to transient failures Then each submitted operation includes its operation_id for idempotency And re-submission of the same operation_id produces no duplicate effect on the server And the server returns acknowledgements per operation_id And the client retains entries until acknowledged, then marks them committed And re-running sync after acknowledgement does not re-apply effects
Offline Duplicate-Use Prevention via Single-Use Nonces
Given the device is offline and a pass is scanned When the token_nonce has not been recorded locally Then the app records the operation and decrements credit once And a second scan with the same token_nonce while offline is blocked with an "already used" error and no new ledger entry is appended And the local spent-nonce cache survives app restart and device reboot until sync reconciliation confirms final state
Crash/Power-Loss Durability
Given an operation is recorded while offline When the device experiences an unexpected app crash or power loss before sync Then upon restart the ledger contains the operation exactly once And the app can continue to append new entries without corruption And when connectivity returns, sync applies the operation exactly once server-side despite any client retries
Short Undo Window With Compensating Entries
Given a credit decrement was recorded less than 2 minutes ago When the instructor taps Undo Then the app appends a compensating undo entry referencing the original operation_id And the original decrement entry remains immutable in the ledger And the net effect on credits/attendance after sync is zero And if more than 2 minutes have elapsed, the Undo action is disabled or returns an error and no undo entry is created
Privacy and Controlled Sync Transmission
Given the device remains offline When actions are recorded into the ledger Then no ledger data is transmitted until a sync is explicitly initiated And during sync only the minimum required fields are transmitted (operation_id, op_type, created_at, token_nonce, device_id, operator_id, class/session identifiers) And personally identifiable data beyond these fields is not included in the payload And all sync traffic is encrypted in transit
Background Sync & Conflict Resolution
"As a studio admin, I want offline usage to reconcile automatically when devices reconnect so that balances and attendance stay accurate without manual effort."
Description

Implement an automatic sync engine that detects connectivity, batches and uploads ledger entries, and handles partial successes with retries and exponential backoff. The server de-duplicates by operation ID and token nonce, applies credit adjustments, and returns reconciliation results. The client updates local state accordingly, flags conflicts (e.g., already-redeemed tokens), and surfaces any required operator action. Show last sync time and queue size. Expected outcome: seamless, reliable reconciliation that keeps balances and attendance accurate.

Acceptance Criteria
Auto Connectivity Detection & Sync Trigger
Given the device has queued ledger entries (check-ins/credit decrements) and is offline, When connectivity changes to online or the app returns to foreground, Then the client initiates background sync within 5 seconds without manual action. Given the device is online and the queue size is 0, When the sync engine evaluates, Then no upload request is sent and the UI shows status "Up to date". Given connectivity drops during an in-flight sync, When the current request fails, Then the client persists unsent and failed entries, records failure reason, and schedules a retry per backoff policy.
Batching, Partial Success, and Exponential Backoff
Given 237 queued ledger entries, When sync runs, Then the client uploads in batches of at most 100 entries per request and waits for a server response before sending the next batch. Given a batch response contains both successes and failures, When processing the response, Then successful operationIds are removed from the queue and failures are retained with original operationId and tokenNonce for retry. Given a retryable error occurs (e.g., network timeout, 5xx), When scheduling retries, Then the client uses exponential backoff starting at 1s and doubling each attempt with jitter, up to a max delay of 5 minutes, then repeats at the max interval until connectivity is restored or app is closed. Given a non-retryable error occurs (e.g., 400 validation), When processing the entry, Then the entry is marked Failed, excluded from further retries, and moved to a conflict/attention list.
Server Idempotency & Client De-duplication
Given each ledger entry includes a globally unique operationId and tokenNonce, When the same entry is uploaded more than once (due to retry or race), Then the server processes it at most once and returns an idempotent reconciliation result for duplicates. Given the server indicates a duplicate by operationId or tokenNonce, When the client receives the response, Then the client marks the entry as Succeeded and removes it from the queue without applying duplicate local side effects (no extra credit decrement or attendance change).
Conflict Detection for Already-Redeemed Tokens
Given a queued entry references a tokenNonce already redeemed elsewhere, When sync occurs, Then the server returns a conflict code TOKEN_ALREADY_REDEEMED including the conflicting operationId and timestamp. Given the client receives a TOKEN_ALREADY_REDEEMED response, When updating local state, Then the client flags the entry as Conflict, surfaces an operator alert within 2 seconds on the check-in screen, and stops auto-retrying that entry. Given the operator opens the conflict item, When choosing "Discard local event and restore credit", Then the client rolls back the local decrement/attendance and marks the entry Resolved. Given the operator opens the conflict item, When choosing "Keep as exception (no further retries)", Then the client records an exception note, keeps local state as-is, and marks the entry Resolved.
Local State Reconciliation
Given the server returns reconciliation results for a batch (applied credit adjustments, attendance statuses, dedup markers), When the client processes the batch, Then local balances, attendance, and wallet usage reflect server truth, with processing time ≤1s per 100 items. Given operations have timestamps and dependencies, When applying results, Then the client applies in original operation order to preserve consistency and prevents display of stale counts to the operator. Given reconciliation completes successfully, When updating UI, Then the queue size decreases accordingly and the "Last sync" timestamp reflects the completion time.
Queue Persistence & Data Safety
Given there are N queued entries while offline, When the app is force-closed or the device restarts and the app relaunches, Then all N entries persist with their original operationId, tokenNonce, and timestamps. Given storage I/O is interrupted mid-write, When the app restarts, Then the client validates queue integrity via checksums and either recovers the last transaction atomically or moves the affected entry to a "Needs Attention" state without losing unrelated entries. Given the queue grows beyond 5,000 entries, When the app loads the queue, Then memory usage remains within platform limits and the UI remains responsive (frame drops under 5% during list interaction).
Sync Status UI: Last Sync Time and Queue Size
Given the instructor opens the class check-in screen, When viewing sync status, Then the UI displays Last sync timestamp (local timezone + relative) and current Queue size (e.g., "Queue: 12"). Given a sync is in progress, When the batch is uploading, Then the UI shows a syncing indicator and updates queue size in real time as items are reconciled. Given the last sync failed within the last 10 minutes, When the instructor views status, Then an error badge with a retry CTA is shown and tapping retry triggers an immediate sync attempt subject to backoff policy. Given queue size is 0 and last sync succeeded within 5 minutes, When viewing status, Then the status reads "Up to date" with a green indicator and no error badges.
Offline-First Check-in UX
"As an instructor, I want clear offline indicators and instant feedback on scans so that I can manage class entry confidently without network."
Description

Deliver a streamlined, one-tap check-in experience optimized for poor connectivity: clear online/offline indicator, scan success/failure feedback (color, haptic, sound), visible remaining credits after decrement, and minimal-latency camera scanning with low-light fallback. Provide queue count and last sync status, plus an instructor override (if permitted by policy) with audit logging. Expected outcome: confident, fast check-ins in basements, parks, and patchy venues.

Acceptance Criteria
Basement Class Offline Scan Check-in
Given the device has no internet connectivity and a class session is open When the instructor scans a valid Wallet pass for that class Then the app validates the single-use token locally and decrements one credit And shows a green success state with checkmark, success sound, and a single short haptic within 300 ms of scan And displays the attendee name and updated remaining credits within 1 second And adds one item to the offline sync queue And persists the updated remaining credits locally so they survive app restarts while still offline When connectivity is restored Then the local decrement is reconciled with the server and the displayed credits match the server response; if adjusted, an "Updated after sync" toast is shown
Online/Offline Indicator and Last Sync
Given the app is connected When connectivity is lost for more than 2 seconds Then an Offline indicator appears within 1 second with an accessible label and icon And the indicator meets WCAG AA contrast requirements And the header shows the last successful sync timestamp in local time When connectivity is restored Then the indicator changes to Online within 1 second And all queued check-ins begin syncing automatically And the last sync timestamp updates upon completion of the sync And the user can tap Sync now to force a retry if any items are pending
Duplicate Scan Prevention Offline
Given the device is offline and an attendee has already been checked in for this class instance When the same Wallet pass is scanned again Then the app rejects the scan locally using the single-use token record And shows a red failure state with an "Already checked in" message, distinct error tone, and double haptic within 300 ms And does not decrement credits again And logs the rejected attempt for audit with timestamp and device ID When connectivity is restored Then no duplicate check-in is created on the server for the same class instance
Low-Light Scanning Fallback
Given ambient light is low (e.g., camera reports low exposure) and the scan view is opened When the scan view initializes Then the camera preview loads within 500 ms And the app prompts to enable the torch and shows a high-contrast scanning guide And the torch can be toggled without leaving the scan view And valid pass scans succeed within 2 seconds in low-light test conditions with at least a 98% success rate And scan decoding maintains an average processing time under 150 ms per frame on reference devices
Queue Count and Sync Behavior
Given the app is offline When the instructor completes n successful check-ins Then the UI shows a queue count equal to n within 300 ms of each check-in And the queue persists across app restarts When connectivity is restored Then queued items sync automatically in FIFO order And the queue count decrements in real time until zero And each item shows a status of Pending, Synced, or Failed in an activity panel And failed items display an error with a Retry action that uses exponential backoff up to 5 attempts
Instructor Override with Audit Logging
Given override is permitted by policy and a pass cannot be scanned or validated When the instructor taps Override check-in Then the app requires selection of a reason and a confirmation step And records an audit entry with instructor ID, attendee ID, class instance ID, reason, timestamp, and device ID And marks the attendee as checked in with an Override tag in the roster And decrements credits if configured, never allowing a negative balance And enqueues the audit record for sync and flags it for review in the server upon reconnection
Accessibility and Feedback Compliance
Given success or failure feedback is presented Then visual feedback meets WCAG 2.1 AA contrast (≥4.5:1 for text, ≥3:1 for UI components) And sounds are distinct and respect system volume and mute settings And haptic patterns follow: success single 30 ms, failure double 30 ms with a 100 ms gap And all interactive targets in the scan view are at least 44x44 pt And screen readers announce connection status, last sync time, queue count, and scan results
Admin Audit & Security Controls
"As a studio owner, I want visibility into offline redemptions and potential misuse so that I can quickly audit activity and mitigate risks."
Description

Provide administrative visibility and controls for Offline Tap Sync: device inventory and status (enrolled, revoked), audit logs of offline redemptions with attribution, export/reporting, and anomaly alerts (e.g., repeated invalid scans, high undo rate, out-of-hours redemptions). Include remote wipe/revoke for compromised devices and policy settings (e.g., require online revalidation after a certain number of offline redemptions). Expected outcome: reduced fraud risk, faster issue resolution, and compliance-ready records.

Acceptance Criteria
Device Inventory & Status Overview
Given an Organization Admin is authenticated with the Admin role When they navigate to Admin > Devices Then they see a paginated list of devices with columns: Device Name, Device ID, Status (Enrolled/Revoked/Pending), Assigned Instructor, Assigned Location, App Version, OS Version, Last Online At, Last Sync At, Offline Redemptions Since Revalidation, and Pending Unsynced Events And the list supports filter by Status, Instructor, Location, App Version and date range (Last Online At) And the list supports sort by Name, Status, Last Online At, Last Sync At And the first page of 50 devices loads in under 2 seconds for up to 500 total devices When a device row is opened Then a detail drawer shows: device metadata, policy currently applied, offline key age, last 50 redemption events, and an activity timeline And only Admins can access this view; non-admins are blocked with 403 and no data leaked
Remote Revoke and Remote Wipe Enforcement
Given a device is currently Enrolled When an Admin selects Revoke on that device and confirms Then the device status changes to Revoked within the admin UI within 5 seconds And the backend invalidates the device’s offline keys immediately and emits an audit event DEVICE_REVOKED with actor, reason, timestamp, and device identifiers When the revoked device attempts an offline validation after revocation time Then the app blocks the validation with error DEV_REVOKED and logs a local event; no credit decrement occurs When the revoked device next comes online or receives push connectivity Then the app performs a remote wipe of offline keys and pending signing material and confirms wipe to the server And any unsynced redemptions with event timestamps prior to revocation are uploaded and marked as PRE-REVOKE; none after the revocation timestamp are accepted When an Admin re-enrolls the device Then a new device identity and offline keys are issued and the prior identity cannot be used
Offline Redemption Audit Logs & Search
Given offline redemptions occur on enrolled devices When events are generated (accept/decline/undo) Then each event is recorded server-side with fields: Event ID, Event Type, Outcome, Organization ID, Device ID, Device Name, Instructor ID, Class/Session ID, Pass ID, Token Hash (non-reversible), Local Timestamp, Server Ingested At, Geo/Location (if permitted), Reason Code, Policy Snapshot, and Actor (tap source) And records are immutable (append-only) and visible in Admin > Audit with role-based access control When an Admin searches by date range, device, instructor, class, outcome, reason code, or token hash prefix Then matching results return within 2 seconds for up to 10,000 records And clicking an event opens a detail view with full payload, signature metadata, and linkage to related undo/sync events And the total count displayed matches the number of returned records
Export & Scheduled Reporting
Given an Admin applies filters to the Audit view for a defined date range When they request an export to CSV or JSON Then the export contains all matching records with documented columns/fields and is available to download within 60 seconds for up to 100,000 records And the file name includes organization, filter hash, and timestamp; a checksum (SHA-256) is provided And an audit trail records who exported what and when When an Admin schedules a recurring report (daily/weekly) to email or S3 with the same filters Then deliveries occur within 15 minutes of the scheduled time window with signed URLs (email) or server-side put to the configured S3 bucket And failed deliveries retry up to 3 times and generate an ADMIN_ALERT on failure
Configurable Anomaly Alerts
Given default anomaly policies exist for invalid scans, undo rate, out-of-hours redemptions, and excessive offline streak When an Admin updates thresholds and channels (email, in-app) per organization or location Then changes are saved, versioned, and distributed to enforcement within 5 minutes When a device produces >5 declined validations with reason INVALID_TOKEN within 10 minutes Then an alert is generated within 2 minutes including device, instructor, counts, sample event IDs, and recent timeline When an instructor’s undo rate exceeds 10% over at least 20 redemptions in the last 24 hours Then an alert is generated and subsequent duplicate alerts for the same condition/device are suppressed for 30 minutes When redemptions occur outside configured business hours or the offline streak exceeds policy (e.g., >N offline redemptions or >H hours offline) Then alerts are created accordingly and visible in Admin > Alerts with acknowledge/resolve actions and audit updates
Offline Revalidation Policy Enforcement
Given an Admin sets Max Offline Redemptions (N) and Max Offline Duration (H hours) and optionally per-location overrides When policy is saved Then the policy is versioned and distributed to enrolled devices within 5 minutes; device detail shows the active policy version When a device reaches 80% of N or H*0.8 elapsed since last revalidation Then the app warns the instructor with a non-blocking banner and logs POLICY_WARNING When a device reaches N redemptions or H hours without revalidation Then the app blocks further offline validations, displays a message requiring online revalidation, and logs POLICY_BLOCKED; no credits are decremented while blocked When the device reconnects online and successfully revalidates Then counters reset, offline keys are rotated, and logging shows POLICY_RESET; offline validation is re-enabled

Gift Pass

Sell giftable passpacks delivered as a Wallet pass via shareable link, SMS, or scheduled email. Givers add a message; recipients claim in one tap and start using credits immediately. Expands reach during holidays and birthdays and brings new clients into the studio funnel.

Requirements

Gift Pass Product Configuration
"As a studio owner, I want to configure gift passpacks with pricing, validity, and branding so that I can sell gifts that match my offerings and brand guidelines."
Description

Enable studio owners to create and manage giftable passpacks with granular settings: credit quantity, validity/expiration window, applicable class types and locations, transferability rules, price, currency, tax handling, and purchase limits. Support branded assets (logo, colors) and terms displayed on the Wallet pass, plus SKU creation, promo eligibility, and inventory caps. Provide real-time preview of the Gift Pass and Wallet pass. Allow deliver-now or scheduled delivery defaults per SKU. Enforce regional compliance for gift expiry and disclosures. Changes should be versioned and auditable.

Acceptance Criteria
Credits and Validity Window Configuration
Given I am creating a Gift Pass SKU When I enter a credit quantity between 1 and 999 inclusive Then the value is accepted and the preview shows that number of credits Given I enter 0, a negative number, a non-integer, or leave credit quantity blank When I attempt to save Then the Save action is disabled and an inline error "Enter an integer between 1 and 999" is displayed Given I select a validity type of "Relative" and enter 1–730 days When I save the SKU Then the validity is stored as relative_days and the preview shows "Expires N days after claim" Given I select a validity type of "Fixed date" and pick a date in the future When I save the SKU Then the validity is stored as fixed_date with the selected timezone and the preview shows the formatted date Given I select an expiry date in the past or a relative duration outside 1–730 When I attempt to save Then saving is blocked with a specific inline error Given I save the SKU When I view the SKU details Then a unique SKU code is generated and a version snapshot is recorded with timestamp, actor, and diff
Applicable Classes and Locations Restriction
Given I select one or more class types and locations for applicability When I save the SKU Then only the selected class types and locations are stored as allowed and shown in the preview summary Given a recipient attempts to redeem credits on a non-allowed class type or location When they attempt to book Then the booking is blocked with message "Gift credits not applicable to this class or location" and the event is logged Given I select none for class types and locations When I save Then the system defaults to "All class types" and "All locations" and displays this in the preview
Transferability Rules Enforcement
Given I set transferability to "Non-transferable" When a recipient attempts to transfer remaining credits Then transfer action is unavailable and an explanation tooltip is shown Given I set transferability to "One-time transfer" When the first transfer is completed to a verified recipient email or phone Then ownership updates, an audit entry is recorded, and subsequent transfer attempts are blocked Given I set transferability to "Unlimited transfers" When a transfer is initiated Then the recipient must be unique and verified, and the audit trail records each transfer with from, to, time, and remaining credits Given a transfer occurs When notifications are sent Then both giver and recipient receive confirmation via the selected channel (SMS/email) and delivery status is recorded
Pricing, Currency, and Tax Handling Configuration
Given I choose a supported currency and enter a price >= 0.50 and <= 9999.99 When I save Then the price is stored with currency ISO code and the storefront shows the correctly formatted currency symbol and decimal precision Given I toggle Tax handling to "Tax-inclusive" When viewing the storefront price and checkout Then the price label indicates "incl. tax" and the internal tax breakdown is calculated and stored in order records Given I toggle Tax handling to "Tax-exclusive" with a selected tax profile When viewing checkout in a taxable region Then applicable tax is calculated, displayed as a separate line, and stored; in non-taxable regions, the tax line is 0 Given "Promo eligible" is enabled for the SKU When a valid promotion is applied at checkout Then the discount applies to this SKU; if disabled, the promotion is rejected with a clear message Given the currency is changed on an existing SKU When I save Then a new version is created and the change is audited; existing orders continue using the prior currency
Purchase Controls: Limits, Inventory, and Delivery Defaults
Given I set a per-customer purchase limit (e.g., 1 per 30 days) When a buyer attempts to exceed the limit Then the purchase is blocked with "Purchase limit reached" and the attempt is logged Given I set a global inventory cap of N When N purchases have completed Then the SKU status becomes Sold Out, is hidden from the public list (if configured), and further attempts show "Sold out" Given I enable "Deliver now" as the default When the SKU is presented at checkout Then "Deliver now" is preselected; if "Scheduled delivery" is the default, date/time and recipient timezone fields are required and validated Given an order completes successfully When inventory is decremented Then the operation is atomic to prevent oversell in concurrent purchases
Branded Assets, Terms, and Real-time Wallet Pass Preview
Given I upload a logo (PNG/SVG, max 1MB) and set brand colors (#RRGGBB) When I save or change inputs Then the Wallet pass preview updates within 500ms to reflect logo and colors; invalid files or color codes produce specific errors Given I enter terms text and a terms URL When the preview renders Then the Wallet pass shows a visible "Terms" link and the Gift Pass page displays the terms snippet and link Given the chosen brand colors fail WCAG AA contrast against the pass background When I attempt to save Then a warning is shown with suggested adjustments and saving requires acknowledgment Given I toggle on "Display studio logo on pass" When previewing on mobile viewport widths 320–414px Then the layout remains responsive with no overlap or truncation of credits, expiry, giver message placeholder, or terms link
Regional Compliance for Gift Expiry and Disclosures
Given the studio region is set to a jurisdiction that prohibits gift expirations When configuring validity Then expiry options are disabled and a disclosure explains "Gift passes do not expire in your region" Given the region requires minimum 5-year validity When I attempt to set a shorter duration Then saving is blocked with an error referencing the regional rule and the minimum allowed value is auto-suggested Given the region mandates specific disclosures When viewing the checkout and Wallet pass preview Then the required legal text is displayed verbatim with the correct jurisdiction label Given a non-compliant configuration was previously saved When regional settings change to a stricter jurisdiction Then the SKU is flagged for review, a new version is created, and sales are paused until compliance is confirmed
Secure Gift Checkout
"As a gift giver, I want a fast, secure checkout with my preferred payment and scheduled delivery so that I can send a gift without friction or risk."
Description

Build a mobile-first checkout optimized for gifting: collect giver and recipient details (name, email, optional phone), personal message, delivery channel (link, SMS, email), and delivery schedule. Support Apple Pay, Google Pay, and cards with 3DS where required, tax calculation, currency display, and receipts. Allow multiple gifts per order and optional "gift to self." Include fraud checks (velocity limits, BIN risk, device fingerprinting) and address verification. Provide clear confirmation with order summary and delivery status tracking.

Acceptance Criteria
Mobile-First Gift Checkout Form Fields
Given I am on a mobile device When I open the Gift Pass checkout Then the form displays giver name, giver email (required), giver phone (optional), recipient name, recipient email (required), recipient phone (optional), personal message (max 300 chars), delivery channel, and delivery schedule Given required fields are empty or invalid When I tap Pay Then inline validation messages appear and the payment is not submitted Given I choose delivery channel "Link" When I proceed Then recipient email and phone are optional Given I choose delivery channel "SMS" When I proceed Then recipient phone in E.164 format is required and validated Given I choose delivery channel "Email" When I proceed Then recipient email is required and RFC 5322 validated Given I set delivery schedule When I select "Send now" Then delivery is scheduled immediately in the studio timezone Given I set delivery schedule When I select a future date/time Then the date/time cannot be in the past and respects the selected timezone
Payments with Apple Pay, Google Pay, Cards and 3DS
Given the device supports Apple Pay or Google Pay and gateway tokenization is configured When checkout loads Then the corresponding express pay button is visible and enabled Given Apple Pay or Google Pay is used When the pay sheet is confirmed Then the payment succeeds, taxes and totals match the order summary, and a receipt is generated Given a card payment in a 3DS-required region or high-risk assessment When I submit the card Then a 3DS challenge is presented inline and, upon successful authentication, the payment is captured without losing form state Given a payment error occurs When the gateway returns a decline or failure Then a clear error message is shown and I can retry without re-entering non-sensitive fields
Tax Calculation and Currency Display
Given I enter a billing country and postal code When tax rules apply Then tax is calculated per configuration (inclusive or exclusive) and shown as a separate line item with the applied rate Given the studio supports multiple currencies When my buyer currency is supported Then prices and totals render in that currency with correct symbol, decimal, and thousand separators Given I change my billing address When the tax jurisdiction changes Then tax and totals update within 200 ms Given a valid tax-exempt code is entered When the code is verified Then tax is removed and the exemption is noted on the receipt
Address and Identity Verification
Given I enter a billing address for card payments When AVS is enabled Then the address is sent for AVS check and mismatches trigger decline or step-up per policy Given I enter CVV When the authorization is attempted Then CVV must pass verification or the transaction is declined Given suspicious mismatches or anomalies are detected When risk rules require review Then the order is routed to manual review and delivery is deferred until approved
Multiple Gifts per Order and Gift to Self
Given I have added two or more gift passes to my cart When I review the order summary Then each gift shows recipient details, credits, unit price, delivery channel, schedule, and message, and I can edit or remove each before paying Given I select "Gift to self" When I proceed Then recipient defaults to my giver details and delivery channel defaults to Email, both editable Given payment succeeds When deliveries are generated Then each gift has an independent delivery job and a failure for one gift does not block the others
Fraud Controls and Velocity Limits
Given device fingerprinting and IP reputation are active When multiple high-risk signals are present Then the order is flagged, 3DS is required, or the transaction is blocked according to policy and an audit log is recorded Given velocity limits of 3 gift orders per device per 24 hours and a $500 per order cap When a limit is exceeded Then checkout is prevented with a clear message and no charge is attempted Given BIN risk lists and geo restrictions When the card BIN is on a blocklist or the issuing country mismatches the billing country Then the transaction is declined or stepped up by 3DS per configuration
Confirmation, Receipts, and Delivery Status Tracking
Given payment is successful When I land on the confirmation screen Then I see order number, itemized gifts, delivery channels and schedules per gift, taxes, total paid, last4 or wallet method, and a receipt link Given deliveries are link, SMS, or email When the confirmation renders Then I can copy the shareable link and see scheduled send time or Sent now status for each gift Given scheduled deliveries When I view the buyer dashboard Then each gift shows status (Scheduled, Sent, Delivered, Claimed, Failed) with timestamps and automatic refresh every 15 seconds with backoff Given a delivery fails (bounce or undeliverable) When failure is detected Then the buyer is notified and can correct recipient contact and resend without additional charge
Wallet Pass Generation & Multichannel Delivery
"As a recipient, I want to receive a Wallet pass through the channel chosen by the giver so that I can add it easily and start using credits immediately."
Description

Upon successful payment, generate a unique Wallet-compatible pass (Apple PKPass and Google Wallet) with a scannable code, dynamic balance, expiration, issuer info, and deep links. Deliver via short shareable URL, SMS, or scheduled email with branded templates. Implement delivery status tracking, retries for failed sends, and bounce handling. Support pass revocation and regeneration if a link is compromised. All deliveries are logged and associated with the originating order for support and auditing.

Acceptance Criteria
Generate Unique Wallet Passes After Successful Payment
Given an order for a Gift Pass is paid successfully When post-payment fulfillment is processed Then a unique Apple PKPass file and a Google Wallet pass object are generated for the pass And each pass contains a unique non-guessable identifier and scannable code tied to the originating order And the pass includes issuer info, pass name, dynamic balance field, expiration date, and deep links for claim and booking And the Apple PKPass validates against PassKit schema and is signed with a valid certificate And the Google Wallet object validates against Google Wallet API schema And pass generation completes within 2 seconds for the 95th percentile And any generation error marks fulfillment as failed and is logged with an error code and correlation ID
Device-Smart Claim via Short Shareable URL
Given a unique pass has been generated When the system creates a shareable link Then it produces an HTTPS short URL with a non-guessable token providing at least 128 bits of entropy And the link resolves within 500 ms at P95 and redirects to a device-smart claim page And iOS devices deep link to Add to Apple Wallet; Android devices deep link to Save to Google Wallet; desktop renders QR and instructions And the link embeds the originating order reference for auditing without exposing PII And the link remains valid until pass revocation or expiration And all link creations and click events are logged with timestamp and referrer
SMS Delivery with Status Tracking and Retries
Given SMS delivery is selected and a valid E.164 recipient number is provided When payment succeeds and SMS delivery is triggered Then an SMS is sent using the branded template including the short URL and giver message And delivery status transitions are tracked as queued, sent, delivered, or failed with provider message IDs And transient failures are retried up to 3 times with exponential backoff (e.g., 1m, 5m, 15m) And messages to opted-out or invalid numbers are not sent and are marked failed with reason And final status is reflected in the order timeline and recipient contact record And all events are logged with timestamp within the originating order
Scheduled Email Delivery with Bounce Handling
Given a recipient email and a scheduled send time are configured When the scheduled time is reached Then a branded email is sent containing the short URL and clear CTA to claim the pass And the email is sent from a domain with valid SPF and DKIM alignment And delivery status transitions are tracked as queued, sent, delivered, soft-bounced, or hard-bounced with provider IDs And soft bounces are retried up to 3 times over 24 hours; hard bounces are not retried And final status and bounce reasons are logged and visible on the order timeline And personalization includes the giver message and pass summary without exposing sensitive data
Dynamic Balance and Expiration Updates on Pass
Given a pass exists with an initial credit balance and expiration date When credits are consumed or added through bookings or adjustments Then the pass balance updates and is reflected on Apple Wallet and Google Wallet within 60 seconds at P95 And when balance reaches zero, the pass displays 0 credits and disables redemption And after the expiration timestamp in the pass timezone, the pass status shows Expired and redemption attempts are rejected And updates are performed via PassKit webServiceURL and Google Wallet API without changing the pass ID or barcode And all balance changes and expiration events are audit-logged with actor and reason
Pass Revocation and Secure Regeneration on Compromise
Given a merchant flags a pass link as compromised or requests regeneration When revocation is confirmed Then the original short URL is immediately invalidated and returns HTTP 410 Gone And associated pass objects are marked inactive so barcodes cannot be redeemed And a new pass and short URL are generated preserving remaining balance and expiration And the recipient can be re-notified via original delivery channel(s) if configured And all actions (revoke, regenerate, notify) are logged with actor, timestamp, reason, and old/new identifiers And attempted use of the revoked pass is denied and logged with reason code
Comprehensive Delivery Logging Linked to Order
Given any delivery action occurs (URL creation, SMS send, email send, retries, bounces) When the system records delivery events Then each event captures order ID, pass ID, channel, template ID, recipient identifier, provider message ID, status, timestamp, and actor And logs are immutable and queryable by order, pass, recipient, status, and date range And the order detail page shows a chronological delivery timeline with current status per channel And authorized users can export delivery logs as CSV; unauthorized users cannot view PII fields And all log records are retained for at least 24 months for support and auditing
One-Tap Claim & Account Linking
"As a recipient, I want to claim my gift in one tap without creating friction so that I can start booking right away."
Description

Provide a one-tap claim flow that works from link, email, SMS, or Wallet pass. Pre-fill recipient details; allow quick account creation or sign-in with magic link/SMS code. On claim, allocate credits to the recipient’s ClassNest account, enforce single-claim uniqueness, and record an audit trail (who purchased, who claimed, when). Support claiming with a different email/phone than the giver provided, with ownership verification. Show balance and expiration instantly post-claim, without requiring a payment method.

Acceptance Criteria
One-Tap Claim from Link, Email, SMS, or Wallet Pass
Given a valid, unclaimed Gift Pass URL is opened from an email CTA, SMS link, web link, or Wallet pass action on a mobile device When the recipient lands on the claim screen Then the screen loads within 2 seconds and displays giver name, personal message, pass name, available credits, and expiration date And the primary action button labeled Claim Now is visible and enabled And if a signed-in session exists for the recipient, tapping Claim Now completes the claim and routes to success within 2 seconds
Pre-Filled Recipient Details with Edit Option
Given the gift configuration contains recipient email and/or phone When the claim screen renders Then the email and phone fields are pre-filled with the provided values And the fields are editable by the recipient And edited values persist into the authentication and claim steps
Authentication via Magic Link or SMS Code
Given the recipient is not signed in When they tap Claim Now Then the system offers authentication via email magic link or 6-digit SMS code And the selected message is dispatched within 5 seconds And completing the magic link or entering a correct code signs in or creates an account without a password And after 3 invalid code attempts, verification is rate-limited for 60 seconds with an error message
Claim with Different Email/Phone and Ownership Verification
Given the recipient edits the contact to a different email or phone than originally provided by the giver When the recipient verifies ownership via the magic link or SMS code sent to the edited contact Then the claim succeeds and the pass is linked to the verified contact/account And the audit trail records both the originally provided contact and the final verified contact
Single-Claim Uniqueness and Idempotency
Given a Gift Pass claim token has not yet been redeemed When a claim is completed using that token Then subsequent visits to the claim URL or token attempts display an already claimed state that includes claimant name and timestamp And no additional credits are allocated to any account And repeated claim submissions with the same idempotency key result in only one successful allocation
Credit Allocation and Instant Balance/Expiration Display (No Payment Method Required)
Given the claim is successful When the recipient lands on the success screen Then the recipient’s account balance increases by the exact pass credit amount and the pass expiration is stored And the updated balance and expiration are shown on the success screen and in the account wallet immediately And the recipient can initiate a booking that consumes credits without being prompted to add a payment method
Comprehensive Audit Trail on Claim
Given any claim attempt (success or failure) When the system processes the claim request Then it records purchaser ID and name, pass SKU, claim token, claimant account ID (if available), verified email and/or phone, source (email/SMS/link/wallet), IP address, device user agent, timestamp, and outcome And successful claims additionally record credited amount, expiration date, and idempotency key used And all audit fields are queryable by admins within 5 minutes of the event
Redemption & Booking Integration
"As a client, I want my gift credits to work seamlessly at checkout and on the waitlist so that I can book classes without extra steps."
Description

Integrate gift credits across booking flows: show "Use Gift Credits" at checkout, deduct balances on confirmation, and enforce product rules (eligible classes, blackout dates, cancellation windows, penalties). Support auto-application of credits on smart waitlist auto-offers, and reflect balance changes back to the Wallet pass in real time. Provide clear messaging for ineligible uses or expired credits and allow split payments if credits are insufficient. Ensure compatibility with existing passpacks and promotions.

Acceptance Criteria
Checkout: Apply Gift Credits to Eligible Class Booking
Given a logged-in recipient with an active gift pass and sufficient credits views checkout for an eligible class When the checkout loads Then a visible and enabled "Use Gift Credits" control is shown and the available credit balance is displayed Given the user toggles "Use Gift Credits" When totals are recalculated Then credits to be used are shown, and if credits cover the full cost the total due equals $0.00; otherwise the remaining balance is displayed Given the user confirms the booking with gift credits applied When the booking is processed Then credits are deducted atomically, the booking status is Confirmed, and the Wallet pass balance reflects the deduction within 5 seconds Given the user double-clicks confirm or a retry occurs When idempotency is enforced Then credits are not double-deducted and only one booking is created
Rule Enforcement: Ineligible Class or Blackout Date is Prevented
Given a gift pass with defined eligible class categories and blackout dates When a user opens checkout for an ineligible class/date Then the "Use Gift Credits" control is disabled or hidden and an info tooltip/badge indicates ineligibility per policy Given a user attempts to redeem credits via deep link or API for an ineligible product/date When server-side validation runs at apply and at confirm Then the request is rejected with a 4xx validation error, no credits are deducted, and no booking is created Given cancellation window and penalties exist When a redemption would conflict with these rules (e.g., late-cancel-locked session) Then the system blocks redemption and surfaces the specific policy message without deducting credits
Waitlist Auto-Offer: Auto-Apply Credits and Confirm
Given a recipient is on the smart waitlist for an eligible class and has sufficient credits and auto-accept is enabled When an auto-offer is triggered Then gift credits are auto-applied and the seat is confirmed without user action Given the auto-offer converts to a booking When credits are deducted Then the Wallet pass balance updates within 5 seconds and a confirmation notification (SMS/email) is sent Given insufficient credits at auto-offer time When auto-apply is attempted Then the offer is not auto-accepted, the user is notified to add payment, and no credits are deducted
Cancellation: Credit Refunds and Penalties According to Policy
Given a booking paid with gift credits is canceled within the refundable window When the user cancels Then the original credits are returned to the same gift pass and the Wallet balance updates within 5 seconds Given a booking is canceled inside the penalty window or marked no-show When the policy is applied Then the configured penalty is enforced (e.g., credits forfeited or partial return) and the Wallet balance reflects the outcome within 5 seconds Given any credit refund or forfeiture occurs When ledger records are created Then the booking ID, pass ID, credit delta, timestamp, and actor are recorded for audit
Messaging: Expired or Ineligible Credits
Given a gift pass is expired or all credits are expired When the recipient visits checkout Then "Use Gift Credits" is not applicable and an inline message states: "Gift credits expired on {date}. Pay with card or buy a pass to continue." Given some credits are valid but the selected class is ineligible When the user attempts to apply credits Then an inline message states: "This class isn't eligible for your gift credits. See eligible classes." with a link to eligibility details Given any error or info message is displayed When read by assistive technology Then the message uses role=alert (or equivalent), is focus-visible, and meets WCAG AA contrast
Split Payment: Partial Credits with Card Top-Up
Given the recipient has fewer gift credits than required for the class When they toggle "Use Gift Credits" Then all available credits are applied and the remaining balance due in currency is displayed Given a remaining cash balance exists When the user selects a saved card or adds a new payment method and confirms Then one booking is created, credits are deducted, and the card is charged for the remainder in a single transaction Given the card charge fails When the transaction is rolled back Then no booking is created and previously deducted credits are restored to the Wallet within 5 seconds
Compatibility: Existing Passpacks and Promotions
Given a user has gift credits, passpacks, and an eligible promo code When they reach checkout Then available payment sources are displayed with allowed combinations and the system applies them per configured priority without double-discounting Given the user changes the selected payment source (e.g., prefers passpack over gift credits) When they confirm Then the chosen source is used, other balances are untouched, and totals reflect the selection Given a promo code and credits are both applicable When totals are calculated Then stacking rules are enforced and the final amount and credits deducted are accurate
Sender/Recipient Notifications & Reminders
"As a gift giver, I want confirmation and optional updates when my gift is claimed and used so that I know it was received and appreciated."
Description

Offer branded email/SMS templates for delivery, claim confirmation, first-use notification to the sender, balance updates, and pre-expiry reminders. Support localized content, time zone–aware scheduling, unsubscribe and compliance headers, and per-message personalization (giver message, recipient name, studio name). Provide notification preferences and opt-out controls. Track delivery, open, and click metrics to feed analytics.

Acceptance Criteria
Branded Email/SMS Template Rendering
Given a studio has branding configured (logo, primary color, from-name, reply-to) When any Gift Pass notification (delivery, claim confirmation, first-use, balance update, pre-expiry) is generated Then the rendered email uses the studio logo and primary color in header/CTA and sets the from-name and reply-to accordingly And Then email HTML passes automated accessibility checks (WCAG 2.1 AA color contrast) and includes a text-only fallback And Then SMS begins with the studio display name and fits within 1–3 segments; if content exceeds 3 segments it is truncated with an ellipsis while preserving the primary action link And Then dark mode–compatible colors are applied for supported clients
Time Zone–Aware Scheduling
Given a sender schedules delivery for a specific date and local time and a recipient time zone/locale is set or inferred When the scheduled time occurs Then the notification is sent within ±2 minutes of the recipient’s local time And Then DST transitions are handled so the intended local time is honored And When no time zone is provided Then the system infers time zone from phone country code or email locale with documented fallback to the studio time zone And Then scheduled and actual send timestamps are stored in UTC with the inferred/explicit time zone
Personalization Tokens Resolve
Given the template contains {{giver_message}}, {{recipient_first_name}}, {{studio_name}}, {{claim_link}} and context values exist When the notification is sent Then all tokens render with the correct values And When a value is missing Then a configured fallback is used (e.g., “there” for missing name) and the message remains grammatically correct And Then user-supplied content is sanitized and HTML-escaped to prevent script injection; newline and emoji characters render correctly across email and SMS And Then links use tracking parameters tied to gift_pass_id and message_id
Preferences and Opt-Out Compliance
Given sender and recipient have channel preferences per notification type When preferences are updated Then changes persist immediately and are applied to subsequent sends And When a contact has opted out globally or replies STOP/UNSUBSCRIBE Then no further SMS/email are sent on that channel within 60 seconds and the suppression is logged with timestamp and source And Then all emails include a visible unsubscribe link and physical mailing address; all SMS include opt-out instructions (e.g., “Reply STOP to opt out”) And Then per-studio suppression lists are enforced before send and are exportable
Sender Notifications: Claim and First Use
Given a gift pass is claimed by a recipient When claim is completed Then the sender receives a confirmation via their selected channels within 5 minutes containing recipient first name, pass name, and claim date, without exposing recipient contact details And Given the recipient uses credits for the first time When the first successful redemption is recorded Then the sender receives a first-use notification within 5 minutes including remaining balance summary And When the sender has disabled either notification type Then the corresponding message is not sent
Balance Updates and Pre-Expiry Reminders
Given a gift pass with remaining credits and an expiry date When credits are redeemed or adjusted Then the recipient receives a balance update within 10 minutes indicating remaining credits and a link to book, subject to their channel preferences And When days-until-expiry equals 14, 3, and 1 (default cadence configurable per studio) Then pre-expiry reminders are scheduled and sent respecting time zone and preferences And When balance reaches 0 or the pass is expired or already renewed Then pending pre-expiry reminders are automatically canceled And Then duplicate reminders across email and SMS are deduplicated within a 30-minute window
Delivery/Open/Click/Bounce Tracking to Analytics
Given a notification is sent When delivery, open, click, bounce, or unsubscribe events occur Then events are captured with message_id, recipient_id, gift_pass_id, notification_type, channel, and UTC timestamp And Then events are deduplicated and available in analytics within 15 minutes with at least 99% processing success over a 24-hour period And Then Apple Mail Privacy Protection opens are flagged and excluded from open-rate denominators by default configuration And When a link is clicked Then the target session receives UTM parameters including utm_source=classnest_notifications and utm_campaign set to notification_type
Admin Management & Analytics
"As a studio owner, I want visibility and control over gift passes so that I can support clients and measure ROI from gifting campaigns."
Description

Add an admin dashboard to search and manage gift passes by status (purchased, delivered, claimed, partially used, expired, refunded), resend or change delivery before send, process refunds/cancellations per policy, transfer ownership on request, and export CSVs. Provide analytics on gift revenue, claim rates, time-to-first-booking, and conversion to paying client, with filters by campaign, date, and channel. Ensure role-based permissions and GDPR/CCPA-compliant data handling.

Acceptance Criteria
Gift Pass Search and Filter by Status and Attributes
Given I am an Owner or Admin on the Admin Management dashboard and there are at least 100 gift passes in the system When I load the Gift Passes view with no filters Then the first page returns within 2 seconds and shows results sorted by purchase date descending Given I am on the Gift Passes view When I filter by status for purchased, delivered, claimed, partially used, expired, and refunded (any combination) Then only passes with the selected statuses are shown and the count reflects the filtered total Given I enter a term into the search field When the term matches purchaser name/email, recipient name/email/phone, passpack name, order ID, or gift code Then matching records are returned and non-matching records are excluded Given I apply filters for campaign, channel (email/SMS/link), and purchase date range When I apply all selected filters together Then the results reflect the intersection of filters and load within 2 seconds Given no records match my filters When the query executes Then a zero-results message appears with a Clear Filters action that restores the default view within 2 seconds
Pre-Delivery Modification and Resend
Given a gift pass is purchased and the initial delivery has not yet been sent When I edit delivery method (email/SMS/link), scheduled send date/time, recipient contact, or gift message Then the changes are saved and reflected in a preview before confirmation Given I confirmed the updated delivery settings When the job is queued Then the previous schedule is superseded and the new schedule is visible in the activity log with timestamp and actor Given a gift pass is undelivered or delivered but not yet claimed When I click Resend and select a channel Then a new message is sent via that channel within 2 minutes, the delivery status updates, and the activity log records the resend (timestamp, channel, actor) Given a gift pass is already claimed When I attempt to modify delivery or resend Then the action is blocked with an explanatory message and no message is sent
Post-Purchase Actions: Refunds, Cancellations, and Ownership Transfer
Given a studio refund policy is configured When I initiate a refund for an unclaimed gift pass Then a full refund is issued to the original payment method, the pass status changes to refunded, unused credits are voided, and an audit log entry is created Given a gift pass is claimed and partially used When I initiate a refund per policy Then only the unused monetary value or credits are refunded, the pass status changes to refunded, and future bookings created with remaining credits are handled per policy (cancel or retain) with notifications sent accordingly Given a gift pass has a scheduled delivery When I cancel the scheduled delivery before it is sent Then no messages are sent and the pass remains in purchased status with a log entry for the cancellation Given a gift pass is claimed or unclaimed When I transfer ownership to a new verified email or phone Then remaining credits and future bookings are reassigned to the new recipient, the previous recipient loses access, both parties are notified, and the activity log captures the transfer (old/new recipient, actor, timestamp) Given any of the above actions complete When the system updates records Then the UI reflects the new state within 3 seconds or shows a specific error with no partial state changes
CSV Export for Passes and Analytics with Privacy Safeguards
Given I am an Owner or Admin and have applied filters on either the Gift Passes view or Analytics view When I click Export CSV and select columns Then a UTF-8 CSV with a header row is generated containing only the filtered records and selected columns Given the export contains up to 50,000 rows When the job runs Then the file is available to download within 60 seconds and a time-limited signed link (expires in 24 hours) is emailed to me Given role-based masking rules When a Staff user exports Then recipient PII (email/phone) is masked; Owner/Admin receive unmasked data Given timestamps are included When the CSV is generated Then all timestamps are in the studio’s configured timezone using ISO 8601 format Given records have been anonymized or deleted per privacy policy When exporting Then anonymized/deleted PII is excluded from the CSV
Analytics Metrics and Filters
Given there are gift pass purchases, deliveries, claims, bookings, and client payments When I open the Gift Analytics view Then I see metrics: Gross Gift Revenue, Net Gift Revenue (after refunds), Claim Rate (% purchased claimed), Median Time-to-First-Booking (days from claim to first booking), and Conversion to Paying Client (recipients who make a non-gift purchase within the selected window) Given filters for campaign, purchase date range, and delivery channel When I adjust any filter Then all metrics and charts recalculate within 2 seconds to reflect only filtered data Given a conversion window control (default 60 days) When I change the window Then the Conversion to Paying Client recalculates using the new window Given I click a metric or chart segment When drill-through is supported Then I am taken to the Gift Passes list pre-filtered to the underlying records Given new data is ingested When I refresh the Analytics view Then the data freshness timestamp indicates last update within 15 minutes
Role-Based Permissions and Access Control
Given roles Owner, Admin, and Staff When a Staff user opens the Gift Passes view Then they can view and search but cannot refund, transfer, modify delivery, or export; restricted controls are disabled and API calls return 403 Given an Admin opens the Analytics view When interacting with metrics Then they can view, filter, drill through, and export; Staff can view and filter only; Owner has all Admin capabilities plus role management Given an unauthorized user attempts a restricted action via direct URL or API When the request is processed Then access is denied with 403 and the attempt is logged with user ID, timestamp, and IP Given a user’s role is changed When the next request is made Then updated permissions apply without requiring logout/login
GDPR/CCPA Compliance and Data Subject Rights
Given the studio operates under GDPR/CCPA When scheduling or sending gift delivery via email/SMS Then messages include required sender identification and an opt-out mechanism, and are not sent to contacts on the suppression list Given a data subject submits a right-of-access request When an admin generates the export Then a machine-readable file of the subject’s gift pass data is produced within 7 days and excludes other subjects’ data and confidential system fields Given a data subject requests deletion When the request is approved Then personal identifiers on related gift passes (name, email, phone, message) are anonymized within 30 days while transactional records needed for tax/audit are retained without PII; analytics continue using aggregated, non-identifiable data Given a data retention period is configured (e.g., 24 months after expiry or refund) When the retention job runs Then PII beyond the retention window is anonymized and the action is written to the privacy audit log Given any privacy-related processing occurs When the action completes Then an audit log entry is available to Owners including actor, action, timestamp, and scope

Pack Insights

Track pass performance at a glance: sales, active vs. expired credits, breakage, top-up conversion, and predicted renewals. Get actionable suggestions (send a nudge, add a bonus, extend expiry) to maximize revenue and keep rosters full without manual number-crunching.

Requirements

Pack Performance Dashboard
"As a studio owner, I want a single dashboard that shows how my packs are selling and being used so that I can quickly spot issues and opportunities without running manual reports."
Description

A mobile-first dashboard that surfaces key pass metrics at a glance—including total sales and revenue, active vs. expired packs, remaining credits, breakage rate, top-up conversion, and predicted renewals for the next 30/60 days. Provides time range and segmentation filters (pack type, instructor, location), trend sparklines, definitional tooltips, and CSV export. Refreshes near real-time from existing bookings, payments, and attendance data, honors role-based access, and aligns metric definitions with ClassNest analytics to ensure consistency and trustworthy insights.

Acceptance Criteria
Mobile Metric Cards Overview
Given a logged-in Owner on a mobile device (viewport width 320–480px) When they open the Pack Performance Dashboard with default time range Last 30 Days Then the dashboard displays metric cards for: Total Pack Sales (count), Total Revenue (currency), Active Packs (count), Expired Packs (count), Remaining Credits (count), Breakage Rate (percentage), Top-up Conversion (percentage), Predicted Renewals (Next 30 days, Next 60 days) And each card shows a label, primary value, and secondary helper text where applicable (e.g., currency symbol, % sign) And values are formatted as: currency in tenant currency with no more than 2 decimals; percentages with 0–1 decimal; counts as integers And initial content paint for metric cards occurs within 3 seconds on a 4G connection; subsequent in-page updates complete within 800 ms And empty or zero values render with 0 or 0% and do not error
Time Range and Segmentation Filters
Given the dashboard is loaded When the user changes the time range (Today, Last 7 Days, Last 30 Days, Last 90 Days, Custom Date Range) Then all metric cards and sparklines recompute for the selected range within 2 seconds And the selected time range is visually indicated and persists during navigation back to the dashboard within the session Given the dashboard is loaded When the user applies segmentation filters (Pack Type, Instructor, Location) singly or in combination Then all metrics reflect the intersection of selected filters within 2 seconds And filter chips show the active selections and can be cleared individually or all at once And clearing all filters returns metrics to the default state
Trend Sparklines and Period Comparison
Given trend sparklines are enabled for metrics that support trends (sales, revenue, top-up conversion, breakage) When a time range is selected Then each sparkline shows correctly ordered data points matching the granularity of the range (daily for <=30 days; weekly for >30 days) And tapping a sparkline point displays the value and date in a tooltip And an optional Compare to previous period toggle displays delta arrows and % change, computed as (current - previous) / previous, formatted to 1 decimal place And metrics without sufficient historical data show a neutral placeholder state
Metric Definitions Consistency and Tooltips
Given ClassNest Analytics has established definitions for pack metrics When the dashboard computes Breakage Rate, Top-up Conversion, Remaining Credits, Active vs. Expired Packs, and Predicted Renewals Then each metric matches the Analytics service values for the same tenant, filters, and time range within a tolerance of <=0.1% And any discrepancy above tolerance is logged with metric name, inputs, and timestamps Given a user taps the info icon on any metric card When the tooltip opens Then it displays a concise definition and formula (including inclusions/exclusions and time basis) and a link to Learn more And the tooltip can be dismissed by tapping outside or the close control and is accessible via keyboard and screen readers
CSV Export of Pack Metrics
Given filters and time range are set When the user taps Export CSV Then a CSV downloads within 10 seconds containing a header row, a metadata section (tenant, time range, filters, generated at), and rows for each displayed metric with name, value, unit, and, where applicable, comparison delta And exported values exactly match on-screen metrics for the same filters and time range And the file name follows convention pack-performance-YYYYMMDD-HHMM-tenant.csv And only data the user is authorized to view is exported
Near Real-Time Data Refresh and Freshness Indicator
Given the dashboard is open When a new booking, payment, or attendance event that affects pack balances is recorded Then the affected metrics refresh within 2 minutes without a full page reload And a Last updated timestamp is visible and updates on refresh, reflecting the server time of the data snapshot And a manual Refresh action is available and preserves current filters And transient refresh does not cause layout shift greater than 0.1 CLS on mobile
Role-Based Access to Pack Performance Dashboard
Given role-based access control is configured When an Owner or Admin signs in Then they can access the Pack Performance Dashboard and view all locations, instructors, and pack types When a Manager signs in Then they can access the dashboard but only for their assigned locations; attempts to select unassigned locations are disabled When an Instructor signs in Then they can access the dashboard scoped to their own packs and clients only; cross-instructor data is hidden When an unauthorized or unauthenticated user attempts access Then the system returns 403 (authorized but forbidden) or redirects to sign-in without leaking data And deep-linked URLs enforce the same scope and do not bypass restrictions
Credit Lifecycle Tracking
"As an instructor, I want to see which clients are close to credit expiry so that I can encourage them to book before they lose value."
Description

End-to-end tracking of credits from issuance to redemption and expiry to power accurate active vs. expired counts and upcoming expirations. Handles per-pack expiry rules, grace periods, and time zones; supports retroactive adjustments and audit logs. Exposes client-level and pack-level views, with alerts for credits nearing expiry and APIs for syncing with booking and reminder workflows, ensuring reliable data for insights and actions.

Acceptance Criteria
Issue Credits Per Pack Rules in Studio Time Zone
Given a pack configured with N credits and an expiry rule (rolling duration or fixed date) and a studio time zone When a client completes purchase and the payment is confirmed Then N credits are issued with status "active" and an expiry timestamp set to 23:59:59 in the studio time zone per the pack's rule And client- and pack-level counts reflect total_issued=N, active=N, redeemed=0, expired=0 And all timestamps are stored and exposed in ISO 8601 including time zone offset
Consume and Restore Credits via Booking Lifecycle
Given a client with active credits and a class booking event arrives with an idempotency key When the booking is confirmed Then exactly one credit is marked "redeemed" and linked to the bookingId, and active count decreases by 1 And a duplicate booking event with the same idempotency key does not change balances When the booking is canceled before class start within policy Then the redeemed credit is restored to "active" with its original expiry preserved, and an audit entry is created When the booking is canceled after class end (no-show or late cancel) Then no credit is restored unless a manual adjustment is performed
Expire Credits with Configurable Grace Period and Accurate Counts
Given a credit with an expiry timestamp and a pack-level grace period G days (which may be zero) When the system time reaches the expiry timestamp Then the credit transitions to status "in_grace" if G>0, otherwise "expired" And during the grace period the credit remains redeemable, is counted in "active", and is labeled "in_grace" When the system time reaches expiry timestamp + G days at 23:59:59 in the studio time zone Then the credit transitions to status "expired", can no longer be redeemed, and counts update (active decreases, expired increases) And upcoming expirations lists include credits whose expiry (pre-grace) falls within configured thresholds (e.g., 3/7/14 days)
Retroactive Adjustments Recalculate Balances and Log Changes
Given an administrator performs a retroactive adjustment (e.g., change expiry, add/remove credit, revert redemption) with a reason When the adjustment is saved Then all affected client- and pack-level counts are recomputed immediately to reflect current correct totals And an immutable audit log entry is recorded with actor, timestamp, operation type, impacted credit IDs/booking IDs, before/after values, and reason And the audit log is queryable by clientId, packId, date range, and operation type
Accurate Client- and Pack-Level Credit Views
Given a client with one or more packs When viewing the client-level credits view Then the UI returns totals for total_issued, redeemed, active (including in_grace), expired, and upcoming_expiring_count for 7/14/30 days that match the underlying records When viewing a specific pack's detail Then the same totals are presented for that pack and match the client-level breakdown And both views return within 500 ms for datasets up to 10,000 credits and present consistent counts within a single request
Alerts for Credits Nearing Expiry
Given alert thresholds are configured (e.g., 7 days before expiry) When a client has one or more credits in a pack expiring within the threshold Then an "expiry_imminent" alert is generated containing clientId, packId, credit_count, threshold_days, and the earliest/latest expiry timestamps And no more than one alert per clientId+packId is generated in a 24-hour window And alerts are exposed via API and include a delivery_status that updates to "delivered" when fetched via webhook or pull API
Lifecycle APIs and Webhooks for Syncing Workflows
Given lifecycle events occur (issued, redeemed, restored, expired, adjusted) When events are emitted Then webhooks are enqueued within 5 seconds carrying event type, ids, and timestamps, and retried at least 3 times with exponential backoff on failure And each webhook includes an idempotency key; duplicate deliveries with the same key must be safely ignored by consumers When retrieving lifecycle data via API Then list endpoints support filtering by clientId, packId, status, and date range; use cursor pagination with a maximum page size of 200; and return ISO 8601 timestamps with time zone offsets
Breakage and Liability Analytics
"As a business owner, I want visibility into breakage and outstanding liability so that I can make informed pricing and expiry policy decisions."
Description

Calculates realized and predicted breakage based on historical utilization patterns and pack rules, and estimates deferred revenue liability with aging buckets. Provides configurable assumptions, transparent methodology notes, exports for accounting, and trend comparisons across periods and pack types. Enables owners to quantify unused value, forecast revenue recognition, and tune policies to balance client satisfaction with cash flow.

Acceptance Criteria
Configure Breakage & Liability Assumptions
Given an Admin user, When they open Settings > Pack Insights > Breakage & Liability, Then they can configure lookback window, expiry grace days, recognition rules, and aging bucket thresholds. Given changes to assumptions, When Save is clicked, Then the system versions the assumptions with timestamp and user and persists them for future calculations. Given saved assumptions, When any analytics view is loaded, Then the current assumptions version and effective date are displayed with a link to methodology notes. Given a multi-location account, When location-level overrides are set, Then calculations for that location use the overridden values. Given invalid inputs (e.g., negative days or non-numeric thresholds), When Save is attempted, Then validation prevents saving and shows field-level error messages.
Calculate Realized Breakage from Historical Utilization
Given a date range and selected pack types, When the user runs the report, Then realized breakage equals the sum of expired unused credit value per pack within the range and is displayed as totals and by pack type. Given the results, When viewed, Then totals include counts of credits issued, used, expired, and % breakage with two-decimal precision. Given no qualifying packs, When the report runs, Then the view displays zero values and an empty-state message without errors. Given data refresh, When the dataset updates, Then a "Data as of" timestamp is shown and is no older than 24 hours.
Forecast Predicted Breakage with Confidence & Notes
Given historical utilization exists, When the user selects a forecast horizon (30/60/90 days), Then the system displays predicted breakage amount with 80% and 95% confidence intervals. Given adjustment of assumptions (e.g., grace days or utilization decay), When sliders/inputs are changed, Then forecast figures update within 2 seconds on a stable connection. Given a need for transparency, When the user clicks Methodology, Then a modal lists formulas, model inputs, assumptions version, exclusions, and last refresh time. Given anomalous periods are excluded via date filter, When forecast is recalculated, Then excluded ranges are omitted from the training data and the change is noted in the methodology modal.
Compute Deferred Revenue Liability with Aging Buckets
Given active unredeemed credits, When the liability view is opened, Then total deferred revenue equals the sum of unredeemed credit value and is broken into aging buckets per configured thresholds. Given bucketed results, When displayed, Then each bucket shows amount, number of packs/clients, and average days outstanding; bucket sums equal the total within $0.01. Given the recognition schedule, When the user expands Forecast Recognition, Then projected revenue recognition by month for the next 6 months is shown based on current assumptions. Given a bucket or total is clicked, When drill-down is invoked, Then a tabular list of underlying packs with client, issue date, expiry date, credits remaining, and amount deferred is shown.
Export Analytics for Accounting (CSV/Excel)
Given the liability or breakage view and applied filters, When the user clicks Export > Accounting, Then CSV and XLSX files are generated containing at minimum: pack_id, client_id, pack_type, issue_date, expiry_date, credits_issued, credits_used, credits_remaining, deferred_amount, realized_breakage, recognition_schedule_month, aging_bucket, assumptions_version, and report_period. Given datasets up to 100,000 rows, When export is requested, Then the file downloads within 30 seconds; larger datasets run as a background job and an email with a secure link is sent within 10 minutes. Given an export completes, When totals are compared to on-screen figures for the same filters, Then they match within $0.01 rounding. Given an export link is emailed, When accessed after 7 days, Then the link is expired and prompts regeneration for security.
Compare Trends Across Periods and Pack Types
Given two time periods and selected pack types, When Compare Periods is applied, Then the UI shows period-over-period changes for realized breakage, predicted breakage, deferred liability, and top-up conversion rates with absolute and percentage deltas. Given granularity selection (daily/weekly/monthly), When the user switches granularity, Then charts and tables update within 2 seconds and totals remain consistent. Given a pack type filter change, When applied, Then comparisons recalculate to include only the selected pack types and the filter state is reflected in the header. Given the comparison view, When hovering data points, Then tooltips display values, deltas, and the assumptions version used for each period.
Top-up Conversion Tracking
"As a marketer, I want to know which top-up prompts drive purchases so that I can double down on the highest-converting messages and channels."
Description

Measures the performance of top-up offers and low-credit prompts across channels (in-app, email, SMS), attributing conversions, time-to-top-up, and revenue uplift. Provides funnel views from prompt shown to purchase, segmentation by pack type and audience, and tagging of experiments. Integrates with ClassNest messaging to auto-tag prompts and writes outcomes back to inform future targeting and optimization.

Acceptance Criteria
Multi-Channel Top-up Attribution (Last-Touch, 7-Day Window)
Given an authenticated user receives low-credit prompts via in-app, email, and SMS with auto-tagged links When the user clicks one of the prompts and completes a top-up purchase within 7 days Then the conversion is attributed to the last clicked prompt’s channel and tag within the 7-day window And the same purchase is de-duplicated so it appears only once across all channels and tags And time_to_top_up_click_min and time_to_top_up_view_min are recorded for the conversion And revenue_attributed equals the top-up order subtotal minus discounts, in the workspace currency And the attribution window is configurable at the workspace level between 1 and 30 days
Prompt-to-Purchase Funnel Dashboard
Given a date range and filters for channel, pack type, audience segment, and experiment tag are selected When the user opens the Top-up Funnel view Then the dashboard displays counts and conversion rates for Prompt Shown, Prompt Viewed, CTA Clicked, Checkout Started, and Purchase Completed And median and p90 time_to_top_up_click_min are displayed for the filtered set And filter changes recalculate metrics and render within 3 seconds And a data freshness timestamp is visible and never exceeds a 15-minute lag And the user can export a CSV that reflects the same filters and metrics
Segmentation by Pack Type and Audience
Given segment filters exist for pack type (name, credit count, price tier, category) and audience (new vs returning, last booking date buckets, custom tags) When one or more segments are applied to the Top-up Conversion report Then all metrics reflect only the selected segments And each segment shows sample size (n) for Prompt Shown and Purchase Completed And segments with n < 30 for Prompt Shown are flagged as low-sample and excluded from rate comparisons by default And applied segment filters persist in copied/shareable report URLs
Experiment Tagging and Variant Reporting
Given prompts are sent with experiment_tag and variant identifiers (e.g., exp_low_credit_nudge:A/B) When conversions occur for those prompts Then results can be grouped by experiment_tag and variant showing conversion rate, revenue_attributed, and time_to_top_up metrics per variant And a variant can be designated as Control to calculate conversion lift and revenue uplift vs control And prompts missing a variant are grouped under variant="untagged" And variant totals reconcile to experiment-level totals
Messaging Auto-Tagging and Event Capture
Given a campaign is created in ClassNest Messaging for in-app, email, or SMS When the campaign is published Then tracking identifiers (campaign_id, channel, prompt_type, experiment_tag, variant) are auto-injected into links and in-app events And Prompt Shown, Prompt Viewed, and CTA Clicked events are emitted with the same identifiers for attribution And messages sent without tags from external tools are recorded with channel="unknown" and tag="untagged" And duplicate delivery/click events are de-duplicated using idempotency keys
Conversion Outcomes Write-Back for Targeting
Given a top-up purchase is attributed to a specific prompt tag When the purchase is completed Then the user profile is updated within 5 minutes with last_top_up_date, last_top_up_channel, last_prompt_tag, last_top_up_revenue, last_time_to_top_up_min, and top_up_converted=true And audience memberships are updated for responded_to_low_credit_prompt (30d) and top_up_in_last_30d And a suppression flag prevent_low_credit_prompts_72h is set for 72 hours from conversion And these updates are available to messaging targeting and automation immediately after write-back And users with global marketing opt-out are not enrolled into any new outreach audiences as a result of the write-back
Renewal Prediction Model
"As an operations manager, I want predicted renewal likelihoods so that I can focus retention efforts on clients most at risk of lapsing."
Description

Generates per-client, per-pack renewal likelihood scores using a phased approach (rule-based heuristics first, then ML) with features like attendance cadence, time since last visit, tenure, seasonality, and prior response to offers. Outputs explainable drivers, confidence levels, and next review dates. Includes evaluation metrics (AUC, lift), threshold controls, and privacy-safe processing. Feeds scores into the dashboard and action engine to prioritize outreach and offers.

Acceptance Criteria
Client Pack Renewal Score Generation
Given a client has at least one active pack or a pack expired within the last 30 days When the nightly scoring job runs at 02:00 workspace local time Then a renewal likelihood score in [0.00, 1.00] with two-decimal precision is stored per client-pack And the score record includes: model_phase (heuristic|ml), score_confidence in [0.0,1.0], top_drivers (>=3 with name and signed contribution), next_review_date (<= 7 days from compute time), and model_version (semver) And the job completes within 20 minutes for 10,000 client-packs with a job success rate >= 99.9% And failed records are retried up to 3 times and logged with error codes
Phase 1 Heuristics Toggle and Audit
Given an admin opens Pack Insights settings When the admin sets Model Phase to Heuristics Then subsequent scoring runs persist model_phase=heuristic and use the configured heuristic ruleset version And the toggle event is audit-logged with user_id, timestamp, old_value, new_value, and environment And an on-demand 90-day backtest report generates within 5 minutes and includes precision@10%, precision@20%, recall@10%, recall@20%, and lift versus baseline renewal rate, exportable to CSV
Dashboard Surfacing of Scores and Drivers
Given a coach views Pack Insights > Packs list When a client-pack row is rendered Then columns display Renewal Score (0–100%), Confidence (Low/Med/High mapped from numeric: <0.40 Low, 0.40–0.70 Med, >0.70 High), and Next Review Date (UTC-normalized) And an info icon reveals the top 3 drivers with positive/negative indicators and plain-language labels And scores below alert threshold t are colored red, t to t+0.15 amber, and > t+0.15 green And the UI reflects new scores within 2 minutes after the scoring job completes
Action Engine Prioritizes Outreach by Score Thresholds
Given an owner sets outreach threshold t in [0.0,1.0] When scores are ingested by the action engine Then suggestions are created per qualifying client-pack: score <= t => "send a nudge"; t < score <= t+0.10 => "add a bonus"; expired within 7 days and t < score <= t+0.10 => "extend expiry" And the queue is sorted by ascending score then descending confidence And each client receives no more than 1 suggestion per 48 hours across packs (de-dup applied) And an execution log records action_id, client_id, pack_id, score, threshold, action_type, and timestamp for each suggestion
Model Evaluation: AUC and Lift Thresholds for ML Promotion
Given an ML candidate is trained on the last 6 months When evaluated on a chronologically held-out most-recent month Then ROC-AUC >= 0.72 and top-decile lift >= 2.0 versus overall renewal rate And Brier score <= 0.18 and Expected Calibration Error (10 bins) <= 0.05 And a model card is generated including features used, excluded features, training window, leakage checks (Pass), and segment AUCs with < 5% absolute disparity across defined segments (new vs returning, mobile vs web) And the model is not eligible for promotion unless all thresholds are met
Privacy-Safe Processing and Data Governance
Given the scoring pipeline executes When feature computation and scoring occur Then only pseudonymous IDs are present in feature and model artifacts; names, emails, and phone numbers are excluded from logs and stores And data in transit uses TLS 1.2+ and at rest uses AES-256 encryption And processing remains within the configured data region and access is restricted via least privilege roles And subject deletion requests remove features, cached scores, and model artifacts for that subject within 7 days And a privacy impact assessment and data inventory are linked in settings
Feature Inclusion and Fallbacks
Given features are computed for a client-pack When data is available Then the feature set includes attendance cadence (avg sessions/week over last 8 weeks), time since last visit (days), tenure (days since first booking), seasonality (month-of-year/holiday flag), prior response to offers (conversion rate, days since last response), and pack usage rate And if any feature is missing or sparse (< 3 observations), imputation/fallback heuristics are applied and a feature_quality flag is set And feature importances (ML) or rule contributions (Heuristics) are recorded and stored with each score for explainability
Action Recommendation Engine
"As a studio owner, I want clear recommendations with one-click actions so that I can increase renewals and utilization without spending time analyzing data."
Description

Creates contextual, revenue-focused suggestions—such as send a nudge, add a bonus credit, or extend expiry—based on current metrics, predicted risk, and business rules (budget caps, frequency limits, and client preferences). Provides one-click actions that prefill messages, scheduling with quiet hours, impact simulations before sending, and post-action outcome tracking to learn what works. Integrates with existing ClassNest reminders and maintains an audit log for compliance and rollback.

Acceptance Criteria
Contextual Suggestions from Pack Insights Metrics
Given Pack Insights has current metrics (sales, active/expired credits, breakage, top-up conversion, predicted renewals) for an account with ≥100 active clients When the engine runs on demand or via hourly schedule Then it generates ≥3 ranked suggestions per eligible pack or segment with actionType in {send_nudge, add_bonus_credit, extend_expiry}, a rationale, and an expected revenue uplift range Given an eligible suggestion When displayed to the user Then the suggestion includes fields: actionId, targetCount, predictedUpliftPct, confidence, estimatedCost, ROI, and nextBestAction Given an account with ≤5,000 active clients When suggestions are requested Then the list renders in ≤3 seconds and includes a generated timestamp Given a suggestion with predicted ROI < 1.2x and no override configured When ranking is computed Then the suggestion is suppressed from the actionable list and marked with reason=BELOW_ROI_THRESHOLD
Business Rule Enforcement (Budget, Frequency, Preferences)
Given org-level budget caps (daily/weekly/monthly) are configured When the engine proposes actions Then the sum of estimatedCost per period does not exceed the cap; excess suggestions are suppressed and logged with reason=BUDGET_CAP Given per-client frequency limits exist by actionType When targeting is built Then any client exceeding limits is excluded; excludedCount is displayed with reason=FREQUENCY_LIMIT Given client channel preferences, quiet hours, and opt-out/consent are stored When building suggestions Then no suggestion violates DND, opt-out, or channel preference; violations are logged with reason=PREFERENCE_VIOLATION Given a rule change is published When the engine next runs Then the new rule version is applied and ruleVersionId is recorded with each suggestion
One-Click Action with Prefilled Content and Quiet Hours
Given a displayed suggestion When the user clicks One-Click Apply Then a confirmation modal opens with a prefilled message template using merge fields {firstName, packName, creditsRemaining, expiryDate, instructorName} and a preview for a sampled recipient Given account- and recipient-level quiet hours When scheduling is determined Then the send time auto-selects the next allowable window in the recipient's timezone and shows scheduledAt per channel Given the user confirms send When the request is submitted Then targets are enqueued within ≤2 seconds and the action status becomes QUEUED with counts {queued, excluded} Given the user selects multiple channels When confirming Then each channel is scheduled respecting its quiet hours and rate limits; failures are retried with exponential backoff and surfaced as errors with errorCode
Impact Simulation Prior to Sending
Given a displayed suggestion When the user opens Simulate Impact Then the system shows estimated reach, cost, revenue uplift, breakage reduction, and ROI with 95% CI based on current metrics snapshot (snapshotTimestamp shown) Given the user adjusts parameters (audience filter, incentive amount, expiry extension days) When changed Then the simulation recomputes and updates all figures within ≤300 ms per change Given simulated ROI < configured threshold When the user attempts to send Then the Send button is disabled until an override reason is provided; the override reason is recorded with the action
Post-Action Outcome Tracking and Learning
Given an action has been sent When the attribution window closes (default 7 days, configurable 1–14) Then the system displays outcomes: sends, deliveries, responses, conversions, revenueAttributed, cost, ROI, and retention deltas vs. baseline Given outcome data is available When the engine next ranks suggestions Then model weights are updated to favor actions with higher realized ROI for similar segments; modelVersion increases and is logged with the ranking Given an action When results are recorded Then a unique actionId and experimentId link outcomes to the originating suggestion and are exportable via CSV
Integration with Reminders and De-duplication
Given existing ClassNest reminders are scheduled for the same audience and intent When the engine evaluates a suggestion Then potential duplicates within ±24 hours are de-duplicated; the suggestion is suppressed or merged, and a dedupeReason is displayed Given a suggestion uses an existing reminder template When sent Then the system reuses the template, channels, and central notification settings; no new template entities are created Given system-wide rate limits are configured When dispatching Then the unified delivery queue adheres to limits and exposes throttling metrics (queuedPerMinute, dropped, retries) to the user
Audit Log and Rollback for Compliance
Given a suggestion or action is created, modified, or executed When the event occurs Then an immutable audit log entry records who, when, what, before/after values, evaluated rules, message content version, and audience size Given an action that altered entitlements (bonus credit, expiry extension) When the user requests rollback within 24 hours and the action is not yet fully consumed Then the system restores the prior state, cancels pending deliveries where possible, logs the rollback with reason, and notifies affected users if configured Given a compliance review When an export is requested for a date range Then the system produces a CSV within ≤60 seconds containing all audit log entries with filters for actionType, userId, and outcome
Insights Alerts and Weekly Digest
"As a small studio team member, I want proactive alerts and a weekly summary so that I can act on important changes without monitoring the dashboard all day."
Description

Configurable alerts for threshold crossings (e.g., drop in top-up conversion, surge in upcoming expiries) and a weekly email/mobile digest summarizing key pack KPIs and recommended actions. Supports time windows, quiet hours, user-specific subscriptions, and deep links to the relevant views or one-click actions. Ensures stakeholders stay informed and act promptly without constantly checking the dashboard.

Acceptance Criteria
Configure alert for top-up conversion drop
Given a subscriber selects metric "Top-up conversion rate", baseline "previous 7 days", threshold "decrease by ≥10%", time window "rolling 7 days", channels "email" and "mobile push", and enables the alert When the monitored metric decreases by 10% or more versus the baseline within the selected window during non-quiet hours Then a single alert event is created within 5 minutes of detection and sent via the selected channels to the subscriber And the alert payload includes metric name, current value, baseline value, percent change, time window, workspace name, and a timestamp in the subscriber’s timezone And the alert contains a deep link to Pack Insights filtered to the metric and time window And if the condition is detected during configured quiet hours, the alert is queued and delivered within 10 minutes after quiet hours end
Alert on surge in upcoming expiries with deep link and actions
Given a subscriber configures an alert for "Credits expiring in next N days" with N=14, threshold ≥50 credits, channels email and mobile push, and quiet hours 21:00–07:00 When the count of credits expiring in the next 14 days crosses 50 for the first time in a 60-minute window Then an alert is generated and sent within 5 minutes (or queued until quiet hours end) including total expiring credits, affected pack types, and top 5 impacted packs And the alert includes one-click actions "Send nudge" and "Extend expiry by 7 days" with confirmation prompts And executing a one-click action requires "Manage packs" permission, is idempotent (retries do not duplicate effects), and is recorded in the audit log with actor, timestamp, and change summary And the deep link opens the expiring credits view pre-filtered to 14 days and the affected packs
Weekly digest generation and delivery
Given the workspace timezone is set (e.g., America/Los_Angeles) and a user is subscribed to the weekly digest scheduled for Mondays at 08:00 local time via email and in-app When it becomes Monday 08:00 in the workspace timezone Then the digest is generated within 60 seconds and delivered via push/in-app within 1 minute and email within 2 minutes And the digest includes KPIs: sales, active credits, expired credits, breakage rate, top-up conversion rate, predicted renewals, each with week-over-week delta and period labels And the digest includes up to 3 recommended actions with one-click buttons and deep links to the relevant Pack Insights views And unsubscribed users receive no digest, and bounced recipients are flagged and excluded from future sends for that channel
Manage alert and digest subscriptions per user
Given a user with role Owner or Manager opens Notification Settings When they create, edit, or delete subscriptions for alerts and the weekly digest (metrics, thresholds, time windows, channels, quiet hours) Then changes are validated and saved, and take effect within 5 minutes And a user with role Instructor can create and manage only their own subscriptions; Viewer can view but not edit And unsubscribing immediately cancels pending queued notifications for that user and prevents future sends And all subscription changes are recorded with actor, timestamp, and previous/new values
Quiet hours and timezone correctness
Given a user’s profile timezone is Europe/Berlin and quiet hours are 21:00–07:00 When an alert condition occurs at 22:15 Europe/Berlin Then the alert is not sent immediately and is delivered by 07:10 Europe/Berlin the next day And if quiet hours span midnight or a DST transition, delivery time respects local timezone rules And all timestamps in alerts/digests display in the recipient’s profile timezone with UTC offset, while system logs store UTC
Alert deduplication and rate limiting
Given a rate limit of 1 alert per metric per workspace per recipient per 60 minutes When multiple threshold crossings for the same metric occur within 60 minutes Then only the first alert is sent; subsequent events are aggregated into a single update delivered at the end of the 60-minute window (or quiet hours end), summarizing latest values and count of occurrences And retries do not cause duplicate deliveries; de-dupe keys include metric, workspace, recipient, and window start And users can mute a specific alert for a chosen duration, after which it resumes automatically
Delivery tracking and failure handling
Given an alert or weekly digest is generated for delivery Then a delivery record is stored per recipient and channel with id, created_at, sent_at, delivered_at, status (queued, sent, delivered, bounced, failed), and link click count And on email bounce or invalid push token, the recipient is auto-marked undeliverable for that channel, shown an in-app notice, and future sends to that channel are suppressed until updated And outbound delivery uses retry with exponential backoff (up to 3 attempts); after max attempts, status is Failed and visible in activity logs And all deep links use signed tokens expiring in 24 hours; expired links redirect to authentication and then intended destination without exposing PII in query parameters

Lightning Queue

High-speed, continuous scan mode for busy doors. Keeps the camera live, gives color/haptic confirms, blocks duplicates, and flags wrong-class scans so you can check in lines fast. Instantly starts grace timers and updates the roster to auto-offer freed spots to the waitlist—clearing queues in seconds with fewer errors.

Requirements

Continuous Camera Scan Mode
"As a front-desk staffer, I want the camera to scan continuously so that I can check in a long line without extra taps or delays."
Description

Keeps the device camera active in a continuous scan session to read QR codes back-to-back with near-zero latency, preventing screen sleep and eliminating tap-to-scan cycles. Provides torch toggle, autofocus control, and low-light optimization. Supports iOS and Android mobile browsers and in-app webviews, without persisting camera frames. Includes a pause/resume control and session timer. Targets recognition latency ≤200 ms per scan and throughput ≥3 successful scans per second under typical conditions.

Acceptance Criteria
High-Throughput, Low-Latency Continuous Scanning
Given a continuous scan session is active on iPhone 12+ (iOS 15+) Safari and Pixel 5+ (Android 12+) Chrome under 300–500 lux indoor lighting, when 30 valid QR codes are presented sequentially at 3–4 codes/sec at 20–40 cm, then the 95th percentile recognition latency per scan is ≤200 ms and sustained throughput is ≥3 successful scans/sec over any 10-second window. Given the same conditions, when 30 valid codes are presented, then ≥29 of 30 scans succeed on first pass (≤1 miss).
Persistent Camera and Wake Lock During Session
Given a session has started, when the device’s default auto-lock period elapses, then the screen remains awake and the camera stream remains active without requiring taps between scans. Given the session ends, then the wake lock is released and the camera track is stopped within 2 seconds.
Torch, Autofocus, and Low-Light Controls
Given the device hardware supports torch, when the user taps the torch control, then the LED toggles within 200 ms and the UI reflects the current state. Given autofocus is unlocked, when the user taps Lock Focus, then focus locks and remains stable until Unlock is tapped; when unlocked, continuous autofocus resumes. Given ambient light is <50 lux, when low-light optimization is enabled (automatically or via control), then ≥95% of 20 valid scans at 20–40 cm succeed with median latency ≤300 ms and throughput ≥2 scans/sec; when torch is on, median latency returns to ≤200 ms and throughput ≥3 scans/sec.
Pause/Resume Controls and Session Timer
Given an active session with a visible mm:ss session timer, when Pause is tapped, then the video preview freezes, decoding stops, and the timer stops within 200 ms. Given the session is paused, when Resume is tapped, then the camera preview and decoding resume within 500 ms, the timer continues from the paused value, and no scan events are recorded during the paused interval. Given the session is ended, then the timer resets to 00:00 and is hidden within 500 ms.
Privacy: No Camera Frame Persistence
Given an active session, then no camera frames are written to local/session storage, cache, or IndexedDB, and no image/video payloads are transmitted over the network; only decoded token metadata events are emitted. Given the session ends, then all MediaStreamTracks are stopped and the OS camera indicator turns off within 1 second; starting a new session does not display any previous frame content.
Cross-Platform Browser and WebView Support
Given iOS Safari 15+ and iOS WKWebView, and Android Chrome 100+ and Android WebView 100+, when starting a continuous scan session, then the camera permission prompt is displayed as required and, once granted, the session starts without errors in each context. Given a session is running in each platform context on reference devices (iPhone 12/13/14, Pixel 5/6), when 20 valid scans are performed, then performance targets are met (≤200 ms latency p95, ≥3 scans/sec throughput). Given device hardware lacks torch support, then the torch control is hidden or disabled with an explanatory tooltip; otherwise it is visible and functional.
Robustness and Error Recovery
Given a continuous session is active, when the camera becomes temporarily unavailable (another app takes control), then the session displays a non-blocking “Camera unavailable” state and auto-recovers within 2 seconds of availability without page/app reload. Given camera permission is revoked mid-session, when the user re-grants permission, then the session restarts the camera and resumes scanning within 2 seconds. Given 30 minutes of continuous scanning, then the app remains responsive and memory usage does not increase by more than 10 MB from baseline in the same tab/webview.
Real-time Pass Validation & Roster Check
"As an instructor, I want each scan validated against the live roster so that only eligible attendees are admitted."
Description

Validates each scan against the live roster and booking rules in real time, confirming class ID, start time window, location, payment status, and attendee identity. Uses a secure signed token embedded in the QR code and checks for revocation, transfer, or refund status. Falls back to a locally cached roster snapshot if connectivity degrades and reconciles when network returns. Returns structured outcomes (valid, duplicate, wrong class, expired, unpaid) within 300 ms round-trip when online.

Acceptance Criteria
Online Real-Time Validation Within 300 ms
Given a device with internet connectivity and a healthy backend service And a signed QR token representing a booked attendee for the target class instance When the token is scanned during the allowed start-time window at the correct location Then the backend verifies token signature, booking status, classId, locationId, attendeeId match, and payment status And returns outcome=valid with validationSource=live and latencyMs <= 300 And the response includes bookingId, attendeeId, classId, tokenId, requestId, serverTimestamp And the class roster reflects the attendee as checked_in by the time the response is received
Duplicate Scan Detection and Blocking
Given the attendee is already checked_in for this class instance When the same QR token is scanned again on any device Then the service returns outcome=duplicate with validationSource=live and latencyMs <= 300 And the roster remains unchanged And the response includes priorCheckInTimestamp and originalDeviceId
Wrong Class/Location/Time Detection
Given a valid signed QR token for a different class or location, or the scan occurs outside the allowed time window When the token is scanned Then the service returns outcome in {wrong_class, expired} with validationSource=live and latencyMs <= 300 And includes a reasonCode in {WRONG_CLASS_ID, WRONG_LOCATION, TOO_EARLY, TOO_LATE} And the roster remains unchanged
Unpaid or Refunded Booking Enforcement
Given a booking whose paymentStatus is in {UNPAID, REFUNDED, CHARGEBACK} When the corresponding QR token is scanned Then the service returns outcome=unpaid with validationSource=live and latencyMs <= 300 And includes reasonCode in {UNPAID, REFUNDED, CHARGEBACK} And the roster remains unchanged
Secure Token Verification and Revocation/Transfer Checks
Given a QR token with an invalid signature, a revoked tokenId, or a transfer mismatch between token and attendee identity When the token is scanned Then the service returns outcome=expired with validationSource=live and latencyMs <= 300 And includes reasonCode in {TOKEN_SIGNATURE_INVALID, TOKEN_REVOKED, TOKEN_TRANSFER_MISMATCH} And the roster remains unchanged
Offline Fallback with Cached Roster and Reconciliation
Given the device cannot reach the backend or a live validation attempt exceeds 300 ms And a locally cached roster snapshot exists When a QR token is scanned Then the app validates against the cached snapshot and returns a result within 200 ms local processing with validationSource=cached And the response outcome is limited to {valid, duplicate, wrong_class, expired, unpaid} based on snapshot data And the app queues the scan for server reconciliation with the original scan timestamp And when connectivity is restored, queued scans are reconciled within 5 seconds And any discrepancies trigger a correction event that updates the roster and emits an updated outcome with a reasonCode describing the change
Structured Outcome Schema and Audit Logging
Given any scan (live or cached) When the system returns a result Then the payload includes outcome in {valid, duplicate, wrong_class, expired, unpaid}, validationSource in {live, cached}, and timing metric (latencyMs for live or processingMs for cached) And includes requestId, tokenId, bookingId, classId, attendeeId, deviceId, serverTimestamp And includes reasonCode when outcome != valid And all results are durably written to an audit log correlated by requestId
Instant Multimodal Feedback
"As a door greeter, I want clear color and haptic confirmation so that I can move the line quickly without reading small text."
Description

Provides immediate visual, haptic, and optional audio feedback per scan: green success with short haptic, amber warning with medium haptic, red error with long haptic. Displays concise on-screen guidance (e.g., 'Already checked in', 'Wrong class') with accessible color contrast and large text suitable for outdoor use. Offers a mute toggle, vibration fallback where unsupported, and localization for messages. Ensures feedback is rendered within 100 ms of classification to maintain flow.

Acceptance Criteria
Green Success Feedback on Valid Scan
Given Lightning Queue is in continuous scan mode and a valid attendee code for this class is presented When the scan is classified as a successful check-in Then a green visual confirmation banner and checkmark render within 100 ms of classification And a short haptic pulse (≤150 ms) is emitted And, if audio is not muted, a brief success tone (≤300 ms) plays And the on-screen message is a concise success label (≤15 characters) And the feedback does not block processing of the next scan
Amber Warning on Duplicate Scan
Given the attendee’s code has already been checked in for this class When the code is scanned again and classified as a duplicate Then an amber warning banner renders within 100 ms of classification And a medium haptic pulse (300–500 ms) is emitted And, if audio is not muted, a single warning tone (≤300 ms) plays And the on-screen message reads “Already checked in” or localized equivalent (≤25 characters) And no additional check-in is recorded
Red Error on Wrong-Class Scan
Given a code for a valid attendee of a different class/time is presented When the scan is classified as wrong class Then a red error banner renders within 100 ms of classification And a long haptic pulse (≥700 ms) is emitted And, if audio is not muted, an error tone (≤500 ms) plays And the on-screen message reads “Wrong class” or localized equivalent (≤20 characters) And the current class roster remains unchanged
Audio Mute Toggle Behavior
Given the Lightning Queue screen is visible When the user toggles Mute on Then no audio feedback plays for subsequent scans while Mute is on And visual and haptic feedback still occur per classification When the user toggles Mute off Then audio feedback resumes for subsequent scans And the Mute control visibly indicates its current state
Haptic/Vibration Fallback on Unsupported Devices
Given the device does not support the haptic API When feedback is triggered for any scan classification Then system vibration is used as a fallback if available And if neither haptics nor vibration are available or permitted, only visual (and audio if unmuted) feedback is provided And feedback (visual and any available modality) still initiates within 100 ms of classification And no runtime errors or permissions prompts interrupt the scan flow
Accessible Visual Guidance for Outdoor Use
Given any feedback banner is displayed Then the text-to-background contrast ratio is ≥7:1 (WCAG 2.1 AA for normal text) And primary message text size is ≥24 px or respects OS large text settings up to 200% without truncation And information is not conveyed by color alone (icon/label present) And messages display without clipping or overflow on a 320 px wide viewport with up to 30% text expansion And color palette passes color-blind safe checks for success/warning/error states
Localized Feedback Messages
Given the device/app locale is set to a supported language (e.g., es-ES, fr-FR) When success, duplicate, or wrong-class feedback is shown Then the message strings are rendered from the active locale resources (no fallback to English) And right-to-left locales (e.g., ar) render messages RTL and mirror layout appropriately And if an unsupported locale is selected, messages fall back to English (en) without placeholders or missing keys And localized messages fit the same UI without truncation at 320 px width
Cross-Device Duplicate Locking & Re-entry Controls
"As a studio owner, I want duplicate scans blocked across devices so that we avoid errors and misuse during busy check-ins."
Description

Prevents duplicate check-ins across multiple staff devices by performing atomic server-side ticket locking and idempotent check-in operations. Enforces configurable re-entry rules (blocked, allow within X minutes, or staff override), with a secure PIN/role-gated override flow and reason capture. Logs all attempts with device ID, timestamp, and outcome for auditing and dispute resolution.

Acceptance Criteria
Simultaneous Scan on Two Devices for Same Ticket
Given a valid, unscanned ticket T for class C and two authenticated devices A and B And A and B submit check-in requests for T within 100 ms of each other When the server processes the requests Then exactly one request returns 200 OK with body.status='checked_in' And the other returns 409 Conflict with body.code='DuplicateCheckIn' And only one attendance record exists for T in class C
Idempotent Retry with Idempotency-Key
Given device A submits a check-in for ticket T with header Idempotency-Key=K And the same request is retried up to 3 times within 60 seconds due to network issues When the server receives duplicate requests with Idempotency-Key=K Then each response returns 200 OK with the same body and checkInId And only one attendance record is created for T
Re-Entry Policy: Blocked
Given class C has reEntry.mode='blocked' And ticket T has already been successfully checked in for class C When any device scans T again at any time Then the server returns 403 Forbidden with body.code='ReentryBlocked' And no new attendance or re-entry event is created And an audit log entry is recorded with outcome='denied' and deviceId captured
Re-Entry Policy: Allow Within X Minutes
Given class C has reEntry.mode='allow_within' and reEntry.minutes=15 And ticket T was successfully checked in for class C at t0 When any device scans T again at time t1 Then if t1 - t0 <= 15 minutes the server returns 200 OK with body.reEntry=true and records a re-entry event without increasing attendance count And if t1 - t0 > 15 minutes the server returns 403 Forbidden with body.code='ReentryWindowExpired' and records a denied re-entry attempt
Staff Override for Re-Entry Denial
Given re-entry for ticket T would be denied by policy for class C And staff member S initiates an override on a device When S enters a valid PIN and has a role with permission 'reentry.override' And S provides a reason of at least 10 characters Then the server authorizes the override and returns 200 OK with body.override.applied=true And a re-entry event is recorded with override=true, approverId=S.id, reason text, and deviceId And if PIN is invalid or S lacks permission, the server returns 401 or 403 respectively and no re-entry occurs
Audit Logging of All Check-In Attempts
Given audit logging is enabled When any check-in attempt occurs for class C and ticket T (success, duplicate, denied, or override) Then a log record is written with ticketId, classId, deviceId, staffId (if any), timestamp (UTC), action, outcome, and reason (if any) And logs are retrievable via the audit API filtered by classId and time range And the logged outcome matches the API response of the attempt
Per-Class Re-Entry Configuration Enforcement
Given the organization default reEntry.mode='blocked' and class C overrides to reEntry.mode='allow_within' with reEntry.minutes=10 When ticket T is scanned for class C and then scanned again 9 minutes later Then the class override is applied and the server returns 200 OK with body.reEntry=true And scanning ticket T again 11 minutes later returns 403 Forbidden with body.code='ReentryWindowExpired' And updating class C reEntry.minutes to 5 is reflected in subsequent attempts and in the class settings read API within 60 seconds
Wrong-Class Resolution & Upsell Path
"As a receptionist, I want guided options when someone scans for the wrong class so that I can resolve it on the spot without holding the line."
Description

Flags scans that do not match the current class or time window and offers staff actionable resolutions in-line: quick transfer to the correct session if policy allows, instant sale of a drop-in or credit redemption, or add to waitlist with estimated ETA. Surfaces pricing, taxes, and remaining credits; processes payments via stored cards or tap-to-pay; and updates the roster immediately upon resolution.

Acceptance Criteria
Wrong-Class Scan Flag & Options Display
Given the device is in Lightning Queue mode and a QR belongs to a booking that does not match the current class or admit window When the code is scanned Then the scan is flagged as Wrong Class within 500 ms and an in-line resolution sheet is shown within 1 s And the sheet presents only applicable options: Transfer, Sell Drop-In, Redeem Credit, Add to Waitlist And each option shows real-time eligibility, pricing, taxes, and remaining credits And the camera remains live, with distinct color and haptic feedback for the wrong-class state And the sheet is dismissible in 1 tap and the queue remains active
Quick Transfer to Correct Session
Given a wrong-class scan where transfer is allowed by policy (within transfer window, capacity available) When staff selects Transfer and confirms the target session Then the booking is moved to the target session and the attendee is marked checked-in And the grace timer for the target session starts immediately And the original session spot is freed and waitlist auto-offers trigger within 5 s And both session rosters reflect the changes within 2 s And an audit log entry records actor, timestamp, from/to sessions, and policy used And the flow completes in ≤3 taps and ≤3 s at p95 on the reference device
Instant Sale via Stored Card or Tap-to-Pay
Given a wrong-class scan with no eligible transfer and a drop-in is permitted by policy When staff selects Sell Drop-In Then the system displays itemized price and taxes and allows payment via stored card or tap-to-pay And upon authorization, payment is captured, a receipt is sent via the default channel, the attendee is checked-in, and the grace timer starts And the roster updates within 2 s of payment capture And on payment decline, a clear reason is shown and staff can retry with another method or cancel without leaving scan mode And rescans of the same QR within 2 min do not create duplicate charges and show the last resolution status
Credit Redemption Flow
Given the attendee has at least one eligible credit for the target session When staff selects Redeem Credit and confirms Then remaining credits before/after are shown prior to confirmation And on confirmation, exactly one credit is deducted, the attendee is checked-in, the grace timer starts, and a receipt/confirmation is sent And the roster reflects the check-in within 2 s And if credits are insufficient or not eligible, the action is blocked with a reason and a one-tap path to Sell Drop-In is offered And rescans within 2 min do not double-deduct credits
Add to Waitlist with ETA
Given the class is full or outside the admit window and waitlist is enabled When staff selects Add to Waitlist Then the system shows estimated time-to-offer and current position based on policy, and the default notification channel And on confirmation, the attendee is added at the correct priority and a confirmation appears within 1 s And if a spot opens within 5 s due to other resolutions, an auto-offer is sent to the next eligible waitlisted attendee And the waitlist and roster reflect changes within 2 s And if policy allows, staff may Add as Standby; otherwise the option is blocked with a reason
Policy Enforcement and Override
Given a wrong-class scan and a selected action conflicts with policy (membership restriction, level mismatch, transfer cutoff, payment required) When staff attempts the action Then the system prevents the action and shows a specific policy reason And if overrides are permitted, a manager authentication prompt is presented; on success the action proceeds and an audit record captures actor, reason, and override details And blocked attempts and overrides appear in the session activity feed within 2 s And users without override permission cannot access override controls
Auto Grace Timer & Smart Waitlist Release
"As an instructor, I want no-show spots to auto-release to the waitlist so that the class fills without manual intervention."
Description

At class start, initiates configurable grace timers for unscanned bookings and continuously recalculates available capacity as valid check-ins, cancellations, and denials occur. On grace expiry or manual release, opens the spot and triggers the smart waitlist’s auto-offer workflow with hold timers and SMS/email notifications, updating the roster in real time and preventing over-booking.

Acceptance Criteria
Start-of-Class Grace Timers for Unscanned Bookings
Given a class with scheduled start time T and a configured grace period G minutes And N bookings exist with 0 ≤ k ≤ N already checked in before T When the system time reaches T Then a grace timer of G minutes starts for each unscanned booking (N − k timers) And each timer is visible to staff with countdown and owner booking reference And no waitlist auto-offers are triggered solely by timers starting And if a booking checks in before its timer expires, that booking’s timer is canceled immediately
Continuous Capacity Recalculation on Roster Events
Given class capacity C and a roster containing bookings with statuses {checked-in, confirmed-unscanned, canceled, denied, waitlisted} And the system tracks ActiveGraceHolds and ActiveOfferHolds When a valid check-in occurs or a cancellation occurs or a denial occurs Then AvailableCapacity is recalculated as C − CheckedIn − ActiveGraceHolds − ActiveOfferHolds within 1 second And the instructor UI and roster API reflect the new AvailableCapacity within 1 second And recalculation is idempotent and processes events in timestamp order to avoid oscillation
Auto-Offer Trigger on Grace Expiry
Given a booking with an active grace timer G minutes And there is at least one waitlisted candidate When the grace timer expires without a check-in Then the booking is marked Grace Expired and its seat is released And the smart waitlist auto-offer is sent to the next eligible candidate And a hold timer H is started for the offer and the candidate receives SMS and email within 5 seconds containing accept/decline links and expiry time And the roster shows the seat as On Hold for that candidate until acceptance or hold expiry
Manual Release Initiates Auto-Offer
Given a staff user views a booking with an active grace timer When the staff user taps Release Spot and confirms Then the grace timer for that booking is canceled and the seat is immediately released And the smart waitlist auto-offer workflow is triggered exactly as on grace expiry And an audit log entry records user, timestamp, and booking id
Real-Time Roster Update on Waitlist Acceptance
Given an auto-offer with hold timer H minutes is active for candidate W When W accepts the offer before H expires via the provided link Then W is converted from waitlisted to confirmed booking within 1 second And ActiveOfferHolds for that seat are cleared and AvailableCapacity is decremented accordingly And any other pending offers for the same seat are automatically canceled and notified as Offer No Longer Available
Over-Booking Prevention Under Concurrent Actions
Given class capacity has exactly 1 available seat And two candidates attempt to accept offers nearly simultaneously When both accept requests reach the server Then only the first successful, atomic reservation claim confirms a booking And the second request receives an immediate Offer Unavailable response without creating a booking And AvailableCapacity never drops below 0 and no duplicate bookings are created
Hold Timer Management for Auto-Offers
Given hold timer duration H is configured for auto-offers When an auto-offer is sent to a candidate Then the hold timer starts immediately and is visible to staff with countdown And if the candidate neither accepts nor declines before H expires, the offer is marked Expired, the hold is released within 1 second, and the next eligible waitlist candidate is auto-offered And the process repeats until the roster is full or the waitlist is exhausted
Offline Scanning with Conflict-Free Sync
"As a staff member at a venue with spotty Wi‑Fi, I want scanning to keep working offline so that the queue keeps moving and syncs correctly later."
Description

Supports uninterrupted scanning when offline by using a signed QR token and a locally cached roster to make provisional decisions and queue events. On reconnection, revalidates all offline events, resolves conflicts using versioned check-in records, and reconciles duplicates or policy violations with clear operator prompts. Presents an offline status indicator and limits actions that require payments or transfers until online.

Acceptance Criteria
Offline Valid Scan — Provisional Check-In
Given the device is offline and has a cached roster for the active class and a scanned QR has a valid signature for that class/session and attendee When the QR is scanned in Lightning Queue mode Then the app displays a green success frame and haptic confirm within 300 ms, marks the attendee as Provisional Checked-In locally, starts the attendee's grace timer locally, and enqueues a check-in event with local timestamp, attendee ID, class ID, token hash, and record version And the provisional check-in appears in the roster list with an offline badge And the camera remains live for continuous scanning
Offline Duplicate Scan Prevention
Given the device is offline and an attendee already has a provisional check-in queued for this class When the same attendee’s QR is scanned again Then the app displays an amber duplicate indicator with a distinct haptic pattern, does not enqueue a new check-in event, and retains a single provisional record for that attendee And the duplicate attempt is logged locally with a count increment for telemetry
Offline Wrong-Class/Invalid QR Handling
Given the device is offline When a QR is scanned whose signature is invalid or expired OR whose class/session is not present in the cached roster Then the app displays a red error frame and haptic error, does not enqueue a check-in event, and records a local error event with reason (invalid signature, expired, wrong class)
Offline Event Queue Persistence and Capacity
Given the device is offline When up to 500 scan events are enqueued Then all events persist across app restarts and device sleep, preserving FIFO order When the queue reaches its capacity threshold Then scanning is paused, a blocking prompt informs the operator to reconnect or clear space, and no queued events are dropped without explicit operator confirmation
Reconnection Revalidation and Conflict Resolution Prompts
Given there are offline provisional check-ins queued and the device regains connectivity When the app initiates sync Then each queued event is revalidated against the server using versioned check-in records and classified as Confirmed, Rejected-Duplicate, or Rejected-Policy with a reason code And only Confirmed events update the server roster; Rejected events are rolled back from the local roster And a summary banner shows counts per outcome with access to a details screen; no rejections are applied silently
Grace Timer Expiry and Waitlist Auto-Offer After Revalidation
Given an attendee’s grace timer was started while offline and expires while still offline When the device reconnects and revalidates state with the server Then the no-show release is applied using server time, the attendee is marked No-Show per policy, the spot is freed if eligible, and waitlist auto-offers are triggered accordingly And if the attendee was confirmed by another device before expiry, then the local no-show release is rejected and surfaced in the conflict summary
Offline Status Indicator and Restricted Actions
Given Lightning Queue is active When the device loses connectivity Then an offline status indicator appears within 1 second and remains visible until connectivity is restored And actions requiring payments or transfers (e.g., purchases, refunds, class transfers) are disabled with an explanatory tooltip/toast, while scanning and provisional check-ins remain enabled

Low‑Light Assist

Adaptive scanning that thrives in dim studios and on glare-heavy or cracked screens. Auto-tunes exposure, shows on-screen brightness/glare prompts for guests, and provides a tap-to-enter short code fallback. Result: far fewer failed scans and manual lookups, even in tough lighting.

Requirements

Adaptive Exposure & Focus Control
"As a studio host checking in guests in a dim room, I want the app to automatically tune the camera for low light so that QR codes scan quickly without me adjusting settings."
Description

Implement a camera scanning module that dynamically adjusts exposure, ISO, shutter speed, white balance, and continuous autofocus to optimize code detection in dim studios and under glare. Leverage platform APIs (AVFoundation on iOS, Camera2/CameraX on Android) with anti‑banding (50/60 Hz) and frame-rate smoothing to sustain 24–30 fps while maximizing contrast on code edges. Include optional torch toggle, with guardrails to avoid washout, and ensure low CPU/GPU overhead to preserve battery during peak check-in windows. Integrate with the existing ClassNest check‑in scanner and permissions flow, degrade gracefully on devices with limited camera controls, and verify success via automated tests across a representative device matrix.

Acceptance Criteria
Low-Light Scan Performance (80–200 lux)
Given ambient illumination between 80–200 lux and a standard QR code at 25–60 cm When the scanner is opened Then exposure, ISO, shutter, and white balance auto-tune to maintain an average frame rate >= 24 fps with no dips below 20 fps for more than 1 second during a 30-second session And a valid code in focus is decoded within 1500 ms in at least 90% of 20 attempts And CPU usage attributable to the scanner remains <= 45% average (peak <= 60%), GPU <= 40% average over 5 minutes, and battery drain <= 3% over 10 minutes on mid-tier reference devices
Glare Handling & Anti-Banding (50/60 Hz)
Given strong specular glare causing more than 15% saturated pixels in the ROI and low detection confidence for 1 second When the scanner is active Then the UI displays glare/angle/brightness prompts within 300 ms And when indoor lighting flicker at 50 or 60 Hz is detected or inferred from locale Then anti-banding 50/60 is engaged within 500 ms and flicker amplitude in the ROI is reduced by at least 70% And decode success rate under these conditions is at least 85% across 20 trials
Torch Toggle with Washout Guardrails
Given ambient illumination below 40 lux or two seconds of failed decode attempts When the user taps Torch Then torch enables and exposure/ISO/white balance auto-adjust to prevent washout with highlight clipping <= 2% of ROI and code edge contrast increasing by at least 25% And with torch enabled, a valid code at 25–60 cm decodes within 1200 ms in at least 90% of 20 attempts And when ambient illumination rises above threshold or after successful decode Then torch auto-toggles off within 1 second
Continuous Autofocus Stability
Given a code moving from 15 cm to 60 cm at approximately 10 cm/s When the scanner is active Then continuous autofocus achieves focus lock within 800 ms after motion stops in at least 95% of trials And during continuous movement, focus oscillation does not exceed ±1 focal step for more than 500 ms and no focus hunting persists longer than 1 second And decode attempts only proceed when the focus sharpness metric exceeds the configured threshold
Graceful Degradation on Limited Camera Controls
Given a device lacking manual exposure/ISO/white balance or anti-banding APIs When the scanner is opened Then the module detects capability limitations and enables a fallback pipeline (auto-exposure compensation, software denoise, edge sharpening) within 300 ms and shows a Basic Mode badge And under 100–200 lux, a standard QR code decodes within 2 seconds in at least 80% of 20 attempts And no crashes or ANRs occur and memory footprint increase is <= 10% versus fully featured devices
Permissions & Short-Code Fallback Integration
Given camera permission is denied or camera initialization fails When the user opens the check-in scanner Then the short-code entry prompt appears within 500 ms with an in-flow keypad And given three consecutive failed decode attempts or 6 seconds without a successful decode When the user accepts the prompt Then the keypad opens immediately and entering a valid 6–8 digit short code completes check-in in 3 taps or fewer and within 5 seconds total And invalid short codes show inline error within 300 ms and allow retry without leaving the flow
Automated Device Matrix Verification
Given a representative device matrix (>= 10 devices spanning iOS A12–A16 and Android Camera2 LEGACY to LEVEL_3 across major vendors) When the automated CI test suite executes the scenarios above Then at least 95% of scenarios pass per OS family and at least 90% pass across the entire matrix And median low-light decode time (80–200 lux) across the matrix is <= 1.5 s and the 95th percentile is <= 3.0 s And no test records frame rate < 20 fps for more than 1 second or sustained CPU > 60% for 10 seconds
Glare Detection & On‑Screen Guidance
"As front-desk staff, I want clear on-screen prompts that coach guests to adjust their phone so that glared or cracked screens still scan on the first try."
Description

Add real‑time luminance and specular highlight detection to identify glare and insufficient brightness. When detected, overlay contextual prompts (e.g., “Tilt phone to reduce glare,” “Increase screen brightness,” “Move closer”) with simple iconography and localized copy. Throttle hints to avoid spam, support high-contrast accessibility modes, and ensure guidance does not block the code area. Do not store images; process frames in memory only to comply with privacy expectations. Measure effectiveness via time‑to‑successful‑scan and hint engagement analytics.

Acceptance Criteria
Real-Time Glare Detection Shows Tilt Prompt
Given the camera preview is active and a candidate code region is being tracked And specular highlight intensity in that region exceeds 0.85 normalized brightness for >= 3 consecutive frames When glare is detected Then display the localized prompt "Tilt phone to reduce glare" with a glare icon within <= 250 ms of detection And localize copy to the device locale with fallback to English And auto-hide the prompt within <= 500 ms after highlight intensity drops below threshold for 5 consecutive frames And do not display the glare prompt if a scan has already succeeded within the last 2 seconds
Low Luminance Triggers Increase Brightness Guidance
Given the camera preview is active and a candidate code region is being tracked And average luminance in that region is < 12% for >= 200 ms And auto-exposure/ISO has reached the configured maximum When low luminance is detected Then display the localized prompt "Increase screen brightness" with a brightness icon within <= 250 ms And localize copy to the device locale with fallback to English And auto-hide the prompt when average luminance rises to >= 18% for 5 consecutive frames or after 5 seconds, whichever comes first
Code Too Small or Blurry Triggers Move Closer Prompt
Given the camera preview is active and a candidate code region is being tracked And the detected code bounding box diagonal is < 120 px for >= 3 consecutive frames OR the estimated focus blur metric exceeds the blur threshold When insufficient target size or blur is detected Then display the localized prompt "Move closer" with an approach icon within <= 250 ms And auto-hide when the bounding box diagonal is >= 160 px for 5 consecutive frames and blur is below threshold
Hint Throttling Prevents Prompt Spam
Given any guidance prompt has been displayed during the current scan session When additional guidance conditions are detected Then do not display more than 1 prompt within any 5-second window And do not repeat the same prompt type within 15 seconds And cap total prompts to 3 per scan session And if multiple conditions are present simultaneously, display only the highest-severity prompt (priority: glare > low luminance > move closer)
Guidance Overlay Never Obscures Code Area
Given the predicted code bounding box and camera preview layout are known When rendering any guidance overlay Then position the overlay fully outside the code bounding box with a minimum 16 px margin on all sides And reposition within <= 100 ms if the code bounding box moves And ensure the overlay does not intercept touch events within the code area And ensure all overlays respect device safe areas and do not cover system bars
Accessibility: High-Contrast and Screen Reader Support
Given the device has Increased/High Contrast enabled OR Reduce Motion enabled OR a screen reader is active When any guidance prompt is shown Then render using a high-contrast theme meeting WCAG 2.1 contrast ratio >= 7:1 for text and icons And ensure prompt text is >= 16 pt and touch targets are >= 44x44 px And announce the prompt via platform screen reader within <= 1 second using an alert role And suppress non-essential animations and avoid flashes > 3 Hz when Reduce Motion is enabled
Privacy-Safe Processing and Effectiveness Metrics
Given the camera is active for scanning When processing frames for glare/luminance/size detection Then process all frames in volatile memory only and never write or transmit image/frame data to disk or network And ensure no image data is logged; logs contain only non-PII diagnostics And emit analytics events without images: hint_impression(type), hint_dismiss(type,reason), scan_success, with timestamps And compute time_to_successful_scan_ms from first frame to scan_success with ±20 ms accuracy And do not send analytics if the user has opted out per app settings
Tap‑to‑Enter Short Code Fallback
"As an instructor when a scan fails after a couple of attempts, I want to enter a short code quickly so that I can keep the line moving without searching names manually."
Description

Provide a prominent fallback on the check‑in screen to enter a short 6–8 character alphanumeric code printed in confirmations and passes. Support fast, numeric-first keypad input, auto‑grouping, immediate validation, and offline tolerance using a locally cached roster for today’s classes with secure token verification. Log fallback usage and reason codes, and update audit trails and attendance just like a successful scan. Ensure codes appear in automated SMS/email reminders and wallet passes with clear placement.

Acceptance Criteria
Prominent Fallback Entry on Check‑in Screen
Given the attendee check‑in screen is active with camera scanning enabled Then a visible "Enter Code" fallback control is displayed above the fold with label text "Can't scan? Enter code" and has a minimum touch target of 44x44 dp and contrast ratio ≥ 4.5:1 And activating the control opens the code entry UI within 300 ms without stopping the camera until input is focused And the fallback control is present in both portrait and landscape orientations And the fallback control is available on iOS, Android, and mobile web builds of the check‑in app
Numeric‑First Keypad, Auto‑Grouping, and Input UX
Given a user focuses the short code field on a mobile device Then the numeric‑first keypad is presented (digits row prioritized) with access to alphabetic characters via toggle when required And input accepts 6–8 alphanumeric characters [A–Z, 0–9], case‑insensitive, auto‑uppercased on display And pasted input containing spaces or hyphens is normalized by stripping non‑alphanumerics before validation And auto‑grouping is applied visually as 6→3-3, 7→3-4, 8→4-4 without affecting the underlying raw value And input latency per character remains < 50 ms on a reference device (mid‑tier Android of last 3 years) When the 6th, 7th, or 8th character is entered Then the field auto‑submits for validation without requiring an explicit submit action
Immediate Validation and Clear Errors
Given online connectivity When a valid short code for today’s class (based on class location timezone) is submitted Then validation completes within 500 ms P95 and the attendee detail is displayed for confirmation And the check‑in button is enabled and focused When an invalid, expired, or non‑roster short code is submitted Then an inline error is shown without page reload using one of: "Code not found", "Code expired", "Not for this class", or "Already checked in", and the field retains the entered code for correction And the error message includes a retry hint and is announced to screen readers And repeated invalid attempts do not lock out scanning or other attendees
Offline Tolerance with Local Cache and Secure Verification
Given the device is offline and the roster for today’s classes was cached within the last 24 hours And locally stored signed tokens for attendees are available When a short code is entered that matches a cached attendee with a verifiable signature Then the code is validated locally without network access within 150 ms P95 and the attendee is marked as checked in (offline) And a visible "Pending sync" badge is shown on the attendee row When connectivity is restored Then pending check‑ins are synchronized within 10 seconds and the "Pending sync" badge is cleared When a code cannot be verified offline and no network is available Then the user is offered "Record pending" which stores the attempt for later verification and does not grant access until confirmed
Audit Trail, Reason Codes, and Usage Logging
Given a check‑in is completed via short code Then an audit record is created with fields: event_type=check_in, method=short_code, timestamp (ISO 8601), operator/user id (if any), device id, online/offline flag, latency_ms, and a hashed representation of the code And the attendee id and class session id are linked And a mandatory reason code is captured from a selectable list [Low light, Glare, Cracked screen, Camera issue, Privacy preference, Other] When offline Then the audit and reason code records are queued locally and uploaded automatically on reconnect, preserving timestamps and device ids And all short code check‑in events appear in analytics with counts by reason code per class/session/day
Attendance Update Parity with Successful Scan
Given a short code is validated (online or offline) When the attendee is checked in Then the attendance state, capacity counters, and any downstream automations (e.g., waitlist auto‑offer triggers, notifications) are updated identically to a QR/barcode scan path And the check‑in operation is idempotent: repeat submissions for the same attendee/session do not create duplicates and return "Already checked in at HH:MM" with no side effects And the attendee’s audit trail shows a single check‑in event with method=short_code
Short Code Presence in SMS/Email Reminders and Wallet Passes
Given automated SMS and email reminders are generated for a booked attendee Then the short code appears within the first screenful on mobile (≤ 400 px from top) with the label "Check‑in Code" and monospaced styling for legibility And the code is included in the .pkpass/.wallet pass in a primary or secondary field with sufficient contrast (≥ 4.5:1) and no truncation And the code is copyable in email clients that support tap‑to‑copy and selectable in SMS And templates for all supported locales include the code placeholder and pass automated template validation before send
Robust Decoder Pipeline
"As a staffer, I want the scanner to read low‑contrast or partially damaged codes so that I avoid manual lookups and reduce check‑in delays."
Description

Enhance the decoding stack with multi‑frame sampling, motion compensation, adaptive thresholding, dewarping, inverse-color support (light-on-dark and dark-on-light), and stronger error correction for damaged or low‑contrast codes. Support QR and Code 128/39 barcodes used by legacy passes. Implement timeouts and progressive confidence scoring to provide immediate user feedback. Choose a proven library (e.g., ZXing/ML Kit) augmented with custom preprocessing, and expose a feature flag for controlled rollout.

Acceptance Criteria
Multi‑Frame Sampling + Motion Compensation Under Low Light
Given a handheld device in 5–20 lux and target motion up to 10 cm/s And a valid QR v2–v4 displayed at 250–400 nits When the Robust Decoder Pipeline is enabled Then the decoder samples ≥5 frames within 500 ms and applies motion compensation And achieves ≥90% decode success over a 100‑item low‑light video test set And median time‑to‑decode ≤800 ms; 95th percentile ≤2.0 s
Adaptive Thresholding + Inverse‑Color Support
Given a mixed test set of dark‑on‑light and light‑on‑dark QR, Code 128, and Code 39 including low‑contrast samples (contrast ratio ≥3:1) When the pipeline runs Then decode success rate is ≥98% on high‑contrast samples and ≥90% on low‑contrast samples And zero false decodes on 100 negative/no‑code images And color inversion is auto‑handled without user action
Dewarping and Glare/Crack Resilience
Given target codes at perspective skew up to 35° and with glare occluding ≤15% of the code area or cracks occluding ≤10% of modules/bars When the pipeline runs Then successful decode rate is ≥85% within 2 s median; 95th percentile ≤4 s And false positive rate ≤0.5% across the set
Enhanced Error Correction for Damaged/Low‑Contrast Codes
Given QR codes with up to 20% module damage (ECC M/H) and barcodes with up to 15% bar erosion/smearing When enhanced error correction is enabled Then damaged‑set decode success ≥85% and clean‑set ≥95% And checksum validation rejects all invalid inputs (0/200 false accepts)
Multi‑Symbology Coverage: QR + Code 128/39
Given a batch of 300 codes (QR v2–v10, Code 128, Code 39) in orientations 0–180° and sizes 1.2–5.0 cm When scanning with the pipeline Then symbology is auto‑detected with ≥99% correct classification And orientation‑agnostic decoding succeeds with ≥95% success across the batch And returned payloads conform to each symbology’s spec (Code 39 uppercase A–Z, 0–9, space/−./$/+/%; Code 128 full ASCII)
Timeouts and Progressive Confidence Feedback
Given the scanner is opened with the pipeline enabled When a code is in frame but not yet decoded Then a confidence indicator updates at least every 250 ms (range 0.0–1.0) And at t=1.5 s show “Still trying…”, at t=3.0 s present tappable short‑code fallback, and at t=5.0 s stop scanning and focus fallback And analytics log start, confidence_tick, decode_success/failed, timeout_reason with timestamps
Library Integration and Feature‑Flagged Rollout
Given ZXing or ML Kit is version‑locked and augmented with custom preprocessing When executed in CI and staging with the feature flag ON Then all decoding tests above pass And in production the flag defaults OFF, is toggleable per tenant/platform at runtime, and can rollback within 60 s without app update And dashboards report error rate, median time‑to‑decode, and false‑positive rate segmented by flag cohort
Device Capability Profiling & Graceful Degradation
"As a product owner, I want the feature to adapt to a wide range of devices so that users get reliable scans without crashes or excessive battery drain."
Description

On first run, profile device camera capabilities and performance to auto‑select the optimal scanning path (e.g., CameraX vs. legacy, GPU vs. CPU preprocessing). Cache the profile and use remote config to adjust thresholds without app releases. Provide fallbacks: disable advanced preprocessing if frame rate drops, suggest short‑code entry when conditions are untenable, and handle missing camera permissions. Target iOS 13+ and Android 8+ with a documented QA matrix.

Acceptance Criteria
First-Run Device Profiling Selects Optimal Scan Pipeline
Given a first-time app launch on a supported device When the scanning module initializes Then the app measures camera API support, preview resolution options, exposure latency, and median FPS over at least 2 seconds And selects the modern pipeline (Android: CameraX; iOS: AVCaptureSession with advanced configuration) when supported and stable, otherwise falls back to legacy And selects GPU preprocessing when median FPS ≥ 24 and required GPU ops are available, otherwise selects CPU preprocessing And persists the chosen pipeline and measured metrics to a local profile with timestamp, app version, OS version, and device model And completes profiling within 1500 ms on mid-tier devices and 2500 ms on low-tier devices And emits a debug/telemetry event "scan_profile.applied" including pipeline, fps, device, and platform
Cached Profile Reuse and Invalidation
Given a subsequent app launch after an initial profile exists When the scanning module initializes Then it loads the cached profile and skips re-profiling by default And re-profiles only if any invalidation condition is met: app version changed, OS version changed, remote config flag profile.invalidate=true, or profile age > 30 days And on cache hit, scanner initialization adds ≤ 50 ms latency compared to no-profile path And on invalidation, a single re-profile run occurs and overwrites the cache atomically And a telemetry event "scan_profile.cache_hit" is recorded with true/false and reason
Remote Config Adjustable Thresholds With Safe Fallback
Given remote config is available When the app fetches configuration at startup or scanner module warm start Then it reads and applies bounded values for: fps_low_threshold, fps_recover_threshold, glare_score_threshold, low_light_lux_threshold, reprofiling_interval_days And clamps out-of-range values to safe min/max and logs a warning event And applies new values immediately on scanner restart or next session without an app release And if fetch fails or times out, it uses last-known-good values; if none exist, it uses built-in defaults And after 3 consecutive fetch failures, it continues with last-known-good and suppresses further fetches for the current session
Real-Time Graceful Degradation on Performance Drop
Given the scanner is running with advanced preprocessing enabled When the rolling-average FPS over a 1.5 s window drops below 18 FPS for 2 consecutive windows Then advanced preprocessing is disabled within 300 ms, a one-time toast "Optimizing for speed" is shown, and state degraded=true is recorded And when rolling-average FPS recovers to ≥ 24 FPS for 3 consecutive windows, advanced preprocessing is re-enabled silently and degraded=false is recorded And toggling between states cannot occur more than once every 10 seconds (hysteresis) And transition events include reason, fps_before, fps_after, and duration
User Prompts and Short-Code Fallback in Untenable Conditions
Given the scanner is active When ambient lux < 5 or glare_score ≥ 0.8 is detected for 5 seconds with 0 successful decodes Then inline prompts are shown to increase screen brightness and adjust angle And if after an additional 5 seconds no successful decodes occur, a visible "Enter short code" action appears And tapping the action opens a short-code entry screen with numeric keypad, autofocus, and offline support And entering a valid short code completes check-in within 1 second; invalid entries show an inline error and allow retry without leaving the screen And starting short-code entry pauses the camera to reduce power usage
Missing Camera Permission Handling
Given camera permission is denied, restricted, or not yet requested When the user navigates to the scan screen Then a rationale sheet explains why the camera is needed and offers two actions: "Allow Camera" and "Enter short code instead" And selecting "Allow Camera" triggers the OS permission prompt or opens system settings if permanently denied; no camera APIs are called before permission is granted And selecting short code opens the entry screen as a full fallback flow And if the user returns with permission granted, the scanner auto-starts within 500 ms without requiring additional taps And the app does not crash or freeze under any permission state transitions
QA Matrix Coverage for iOS 13+ and Android 8+
Given the release branch is prepared When QA artifacts are generated Then a QA matrix exists in the repository documenting test devices across iOS 13–17 and Android 8–14 with at least 2 devices per major OS And for each device, recorded results include: selected pipeline, median FPS, low-light/glare prompt behavior, degradation toggles, short-code fallback, and permission handling And ≥ 95% of test cases pass; any failures are linked to tracked issues with severity and owner And the matrix link is included in release notes and exported in CI artifacts
Scan Feedback & Accessibility
"As an instructor in a noisy, low‑light studio, I want strong visual and haptic cues so that I know instantly whether a scan succeeded without straining to read the screen."
Description

Deliver immediate, unambiguous feedback via haptics, brief audio cues, and high‑contrast visual states for success/failure, respecting system mute and accessibility settings. Provide large, legible instructions, VoiceOver/TalkBack labels, and color‑blind‑safe indicators. Include a setting to reduce motion and disable sounds. Align components with the ClassNest design system and WCAG 2.1 AA standards.

Acceptance Criteria
Immediate Success Feedback in Dim Studio
Given a valid booking QR is detected in low light When the scan is confirmed Then a high-contrast success state with check icon and "Checked in" text is shown within 200 ms And a distinct success haptic is emitted only if device haptics are enabled And a brief success tone plays only if in-app Sounds is ON and the device is not in Silent/Vibrate And VoiceOver/TalkBack announces "Checked in: <guest name>" as a polite notification within 500 ms And the success state remains visible for at least 1.5 s or until the operator dismisses it
Failure Feedback with Clear Recovery
Given a code is invalid, expired, or unreadable due to glare/low light When the scan attempt fails Then a high-contrast error state with cross icon and explicit reason (e.g., "Code not found") appears within 300 ms And actionable options are visible: "Try again" and "Enter short code" And an error haptic is emitted only if device haptics are enabled And an error tone plays only if in-app Sounds is ON and the device is not in Silent/Vibrate And VoiceOver/TalkBack announces "Scan failed. Try again or enter short code." as an assertive notification And the error state auto-dismisses after 2 s back to camera unless the user taps an action And no information is conveyed by color alone; icon + text remain present
Screen Reader Labels and Focus Management
Given VoiceOver or TalkBack is enabled When the scan screen is opened Then the camera viewport is labeled "Scanner viewfinder" with control type set to image and marked not focusable during continuous scanning And primary actions (Flash/Light, Enter short code, Close) have accessible names, roles, and hints And the reading order matches the visual order from top to bottom When a scan succeeds or fails Then accessibility focus moves to the result banner and its status is announced once (no duplicate announcements) And the result banner is dismissible via accessibility action And all controls are reachable and operable with switch access
Large Text and High Contrast Compliance
Given the device text size is set up to Accessibility XXL/Dynamic Type largest When the scan screen renders Then all instructional text and buttons reflow without truncation, clipping, or overlap And tap targets are at least 44x44 pt (iOS) / 48x48 dp (Android) And text contrast ratio is >= 4.5:1 against background; non-text UI elements >= 3:1 (WCAG 2.1 AA SC 1.4.3, 1.4.11) And focus indicators are visible with >= 3:1 contrast in both light and dark modes And the success/error banners meet the same contrast thresholds
Reduce Motion and Disable Sounds Settings
Given the user opens Scan Settings When Reduce Motion is toggled ON or system Reduce Motion is ON Then all scan result animations are replaced by static or fade transitions <= 100 ms with no parallax or zoom When Disable Sounds is toggled ON Then no audio cues play regardless of system mute state When Disable Sounds is OFF Then audio cues play only when the device is not in Silent/Vibrate And setting states persist across app restarts for the signed-in user And toggles are accessible, labeled, and expose state to assistive tech
Color-Blind-Safe State Indicators
Given the app must distinguish success vs failure states When the result banner is shown Then success and failure are differentiated by iconography (check vs cross), text labels, and shape/border; not color alone (WCAG 2.1 AA SC 1.4.1) And simulated protanopia, deuteranopia, and tritanopia views still allow state identification with 100% accuracy in QA And monochrome/Grayscale mode preserves distinguishability of states
Scan Quality Analytics & Privacy Controls
"As a studio owner, I want visibility into scan performance and fallback rates so that I can improve lighting and staffing and verify the feature’s impact on check‑in speed."
Description

Instrument anonymized metrics for scan attempts, time‑to‑first‑success, fallback usage, and inferred lighting/glare conditions (derived from luminance only; no image frames stored). Provide a dashboard segment by location, device class, and class time to quantify improvements and identify problem spots. Include admin controls to opt out of analytics and a data retention policy aligned with ClassNest privacy standards.

Acceptance Criteria
Event Instrumentation & Anonymization
Given analytics are enabled for Low‑Light Assist scanning When a guest initiates and performs a scan attempt Then a scan_attempt event is emitted with: attempt_id (UUIDv4), tenant_scope_id (hashed), location_id, device_class, os, app_version, class_id, class_time_bucket, timestamp And the payload contains no PII (e.g., name, email, phone), no IP address, and no image frames And only luminance_scalar and glare_flag may be included from camera sensors And events are accepted by the server only if they meet the schema and privacy constraints, otherwise they are rejected and logged for validation errors
Time‑to‑First‑Success Measurement
Given a scan session starts at first camera activation for check‑in When a code is successfully scanned or a short code is successfully redeemed Then time_to_first_success is recorded in milliseconds from session_start to success_time And if the session ends without success, time_to_first_success is recorded as null with outcome = "abandoned" And the metric is linked to the same session/attempt identifier and available for dashboard aggregation
Fallback Usage Tracking
Given analytics are enabled and the "Enter short code" fallback is available When a guest selects the fallback and completes redemption Then a fallback_used event is recorded and associated to the current session And multiple fallback interactions within the same session are de‑duplicated And the dashboard computes fallback_usage_rate = fallback_sessions / total_sessions for any selected filter slice
Luminance‑Only Lighting/Glare Inference
Given the camera preview is active during a scan session When luminance is sampled from the camera/exposure pipeline Then lighting_level ∈ {Dim, Normal, Bright} and glare_detected ∈ {true,false} are inferred using luminance‑derived statistics only And only the classification labels and aggregate luminance stats (min/avg/max) are emitted And no image frames or thumbnails are stored or transmitted
Admin Analytics Opt‑Out Controls
Given an org admin toggles analytics off at the org or specific location in Admin > Privacy When the setting is saved Then clients in the affected scopes stop emitting analytics on next config fetch or app restart And the server rejects incoming analytics from opted‑out scopes and does not persist them And the dashboard hides analytics for opted‑out scopes and displays an opt‑out notice
Dashboard Segmentation & Improvement Insights
Given analytics exist for the selected date range When the admin opens the Scan Quality dashboard and applies filters Then metrics are displayed segmented by location, device_class, and class_time buckets And for each segment the dashboard shows: scan_attempts, success_rate, median and p90 time_to_first_success, fallback_usage_rate, and Dim/Bright/Glare distribution And a pre/post comparison toggle computes deltas versus a selected baseline period and flags segments with regression
Data Retention Policy Enforcement
Given the organization’s analytics_retention_days policy is configured When the scheduled retention job runs Then raw analytics events older than analytics_retention_days are permanently deleted And aggregate rollups beyond max_aggregate_retention_days (if configured) are deleted And no image frames are ever stored, so none are retained And an audit log entry records deletion counts, scopes, and time window

Kiosk Mirror

Turn any tablet or spare phone into a self-serve check-in kiosk. Guests scan their QR and get large, friendly visual confirmation; instructors see real-time updates on their device. Optional name prompt and waiver nudge reduce bottlenecks while freeing the instructor’s hands at class start.

Requirements

Check-in Token/QR Generation & Delivery
"As a booked guest, I want to receive a scannable QR code for my reservation so that I can quickly check in at the studio without manual lookup."
Description

Generate a unique, signed, time-bounded check-in token per booking and class instance, encoded as a QR code and included in confirmation and reminder messages (email/SMS) and wallet passes. Tokens must be single-use with replay detection, revocable on cancellation/refund/transfer, and resilient to clock skew. Provide a deep link fallback (short URL) for devices without camera access. Support regeneration on user request and surface the latest valid token across channels. Ensure tokens carry minimal PII, are scoped to the specific class occurrence, and expire after a configurable window. Integrate with existing booking and notification services without impacting send times, and log issuance and redemption for audits.

Acceptance Criteria
Unique, Signed, Scoped Token Creation with Minimal PII
Given a confirmed booking for a specific class occurrence When a check-in token is generated Then the token is unique per booking-occurrence, cryptographically signed with a server-held key, and verifiable at redemption And the token payload contains no PII (no name, email, phone, or account identifiers) And the token is scoped so it cannot redeem a different class occurrence And the token includes an expiry timestamp and will expire according to the configured window And attempts to tamper with any payload field invalidate the signature and cause redemption to fail with reason code SIG_INVALID
QR and Wallet Delivery Across Channels Without Latency Impact
Given a generated token When sending confirmation and reminder email/SMS Then the message contains a scannable QR code representing the token and a short deep link URL to the same token And the short URL length is <= 30 characters And adding the QR and short URL increases median message processing latency by < 5% and adds <= 250 ms at p95 relative to baseline When issuing a wallet pass Then the pass displays the QR and resolves the deep link to the latest valid token And QR images are <= 50 KB and render on common devices (iOS/Android)
Single-Use Redemption with Replay Detection and Deep Link Fallback
Given a valid, unredeemed token (via QR scan or short URL) When it is redeemed at the kiosk or via the deep link Then the booking is marked checked-in once and a success response is returned within 2 seconds And any subsequent redemption attempts return "Already checked in" without altering state and respond with HTTP 409 and reason code REPLAY And redemption attempts after expiry return HTTP 410 with reason code EXPIRED And if the device lacks camera access, opening the short URL completes the same single-use redemption flow
Revocation on Cancellation, Refund, or Transfer
Given a booking with an issued token When the booking is cancelled, refunded, or transferred Then the existing token and its short URL mapping are revoked within 5 seconds And redemption of a revoked token fails with HTTP 403 and reason code REVOKED And for transfers, a new token is generated for the transferee within 60 seconds and delivered via email/SMS and wallet pass, while the prior token remains invalid
Token Regeneration and Latest-Token Surfacing
Given an attendee initiates token regeneration from the booking detail screen When regeneration is confirmed Then a new token is created and becomes the only redeemable token within 5 seconds And all prior tokens are invalidated and fail redemption with HTTP 403 and reason code SUPERSEDED And the short deep link remains the same but resolves to the latest token And the booking page and wallet pass display the new QR within 10 seconds
Configurable Expiry Window and Clock Skew Tolerance
Given an organization-level configuration for check-in validity and clock skew When an admin sets the validity window (open: 1–240 minutes before class start; close: 0–240 minutes after start) and skew tolerance (0–10 minutes) Then redemption honors the configured window using server time and allows the configured skew And default settings are open 120 minutes before and close 60 minutes after with 5 minutes skew And attempts outside the window fail with explicit error messages and reason codes (TOO_EARLY, EXPIRED)
Audit Logging of Token Issuance and Redemption
Given a token is issued or redeemed When the event occurs Then an audit record is persisted within 1 second containing: token ID (hashed), booking ID, class occurrence ID, user ID (hashed), event type, server timestamp, channel (email/SMS/wallet/kiosk/deeplink), device info/IP when available, outcome, and reason code And 100% of events are retained for at least 180 days and are queryable by booking, class, and time range And exports up to 100k events complete within 30 seconds in CSV or JSON And audit records are immutable and include a trace ID to correlate issuance and redemption
Secure Kiosk Pairing & Session Lock
"As an instructor, I want to securely pair a tablet to my current class so that only attendees for this session can check in on that device."
Description

Enable instructors to pair a tablet/phone to a specific class session via authenticated selection or by scanning an instructor pairing code. Establish an ephemeral session token that locks the kiosk to that class, restricts accepted check-ins to the paired session, and auto-expires after class end plus a configurable buffer. Provide a PIN-protected exit, remote deauthorize, and automatic lock on inactivity. Prevent cross-class or cross-location check-ins and enforce role-based access. Maintain an auditable log of pairing, de-pairing, and session activity for compliance and troubleshooting.

Acceptance Criteria
Pair kiosk via authenticated class selection
Given a user with role Instructor or Studio Admin is logged into ClassNest on the kiosk and the device is unpaired And the user has permission for Location L When the user selects class session S at Location L and taps "Pair Kiosk" Then the kiosk requests and receives an ephemeral session token bound to session_id S, location_id L, device_id D, and actor U And the token TTL equals session_end_time(S) + configured_buffer_minutes And the kiosk UI switches to a session-locked check-in screen showing S title, start time, and Location L And a pairing event is written to the audit log with timestamp, actor U, device D, session S, location L, method "select", and result "success" And pairing attempts by users with unauthorized roles (e.g., Student) are denied with HTTP 403 and UI message "You don't have permission to pair kiosks", and the denial is logged
Pair kiosk via instructor pairing code
Given an authorized user U (Instructor or Studio Admin) generates a pairing QR code for class session S at Location L from their mobile app And the code is single-use and expires in 60 seconds When the unpaired kiosk scans the QR code Then the backend validates U's role and location permission and that S belongs to L And an ephemeral session token bound to S, L, D, and U is issued and stored on the kiosk And the kiosk moves to the session-locked check-in screen for S And the QR code is invalidated after first successful use And scanning the same code again or after expiry returns error "Pairing code invalid or expired" and the kiosk remains unpaired And a pairing event is logged with method "qr" and result "success" or "expired/invalid" as applicable
Restrict check-ins to paired session and location
Given kiosk K is paired and the session token is valid for session S at Location L When a guest scans their ClassNest QR code Then if the booking belongs to session S at Location L and is not already checked in, the system records attendance within 1 second and displays a large green confirmation with the attendee name And if the booking is for a different session or different location, the check-in is rejected with a red message "Not in this class/location" and no attendance is recorded And if the attendee is already checked in for S, the system displays "Already checked in" and does not duplicate the record And all successful and rejected check-in attempts are logged with reason codes (success, wrong_session, wrong_location, duplicate)
Auto-expire session after class end + buffer
Given class session S ends at 10:00 and the kiosk buffer is configured to 15 minutes When the device clock reaches 10:15 or the server signals session close Then the kiosk invalidates the session token and returns to the pairing screen within 2 seconds And further QR scans are blocked with "Kiosk session expired" until re-paired And a de-pairing event is logged with reason "auto-expire" And if the buffer setting is changed while paired, the new expiry time is applied immediately and logged
PIN-protected exit and inactivity auto-lock
Given kiosk K is paired and a 4–8 digit staff PIN is configured When a user taps Exit or Settings on the kiosk Then the kiosk prompts for the staff PIN and only unlocks the exit flow on correct PIN entry And after 5 consecutive incorrect PIN attempts, the kiosk enforces a 60-second lockout and logs the failed attempts And if there is no touch or scan activity for the configured inactivity timeout (e.g., 2 minutes), the kiosk auto-locks to a PIN screen, blocks check-ins, and logs event "auto-lock" And upon correct PIN entry, the kiosk resumes the active session without issuing a new token
Remote deauthorize paired kiosk
Given kiosk K is paired to session S and is online When an authorized user U taps Deauthorize on their mobile or web dashboard for device K Then the kiosk receives the revoke signal and ends the session within 5 seconds, disabling further check-ins and showing "Kiosk session ended" And the server revokes the session token immediately so that any subsequent API calls with that token are rejected with HTTP 401 And any in-progress scan flow is cancelled without recording attendance And a deauthorization event is logged with actor U, device K, session S, and method "remote"
Auditable session activity log
Given audit logging is enabled by default When pairing, de-pairing, check-in attempt (success/reject), PIN exit/unlock, auto-lock, remote deauthorize, or token expiry occurs Then an immutable audit record is stored with timestamp (UTC), event_type, actor_id (if available), device_id, session_id, location_id, method, result, and reason_code And audit records are tamper-evident, queryable by date range, device, session, or actor, and retained for at least 365 days And any attempt to modify or delete audit records via the API is denied with HTTP 403 and the attempt is logged
QR Scan Check-in with Visual Confirmation
"As a guest, I want to scan my QR at the kiosk and immediately see a clear confirmation so that I know I’m checked in without needing staff assistance."
Description

Implement a fast, forgiving scan flow using the device camera that decodes and validates tokens, checks in the attendee, and renders a large, high-contrast confirmation with name and status in under one second. Provide clear error states (invalid, already used, wrong class, past window) with actionable guidance. Include manual lookup fallback by name/email or short code, with configurable enablement. Handle capacity and waitlist edge cases and display appropriate messaging. Queue scans offline with secure local storage and background sync, ensuring idempotent server-side processing and conflict resolution. Support undo within a brief window for mis-scans and record all state transitions for audit.

Acceptance Criteria
Sub-second QR Scan and Confirmation
Given the kiosk camera preview is active and a valid QR token for the current class within the check-in window is presented When the code is detected Then the token is decoded, validated against the server, and the attendee is marked checked-in And a full-screen, high-contrast confirmation with attendee first and last name and status appears within 1.0 second from detection And the confirmation screen meets WCAG 2.1 AA contrast and uses ≥48px text for name and status And the instructor’s device receives the check-in update within 1.5 seconds from detection
Error States with Actionable Guidance
Rule: If the token is invalid or signature verification fails, show "Invalid QR" with "Try again" and "Manual lookup" actions; do not check in; record the attempt Rule: If the token is already used for this class, show "Already checked in at [timestamp]"; do not check in; provide "See instructor" guidance; record the attempt Rule: If the token is for a different class/time/location, show "Wrong class" with the correct class details; offer "Manual lookup"; do not check in; record the attempt Rule: If outside the configured check-in window (too early or closed), show appropriate message with time guidance; do not check in; record the attempt
Configurable Manual Lookup Fallback
Given manual lookup is enabled in kiosk settings When the user selects Manual lookup Then they can search by name, email, or 6-character short code and see ranked results within 2 seconds And selecting a result checks the attendee in and shows the same confirmation screen Given manual lookup is disabled in kiosk settings Then the Manual lookup entry point is not visible on the kiosk
Capacity and Waitlist Edge Cases
Rule: If the attendee is on the enrolled roster, check-in succeeds and status shows "Checked in", regardless of public capacity state Rule: If the attendee is not enrolled and the class is at capacity, show "Class full" with "See instructor" guidance; do not check in Rule: If the attendee is on the waitlist with a released/accepted spot for this class, check-in succeeds and status shows "Checked in (waitlist promoted)" Rule: If the attendee is on the waitlist without a released spot, show "Waitlist—no spot available" with guidance; do not check in
Offline Queueing and Background Sync
Given the kiosk is offline When a valid scan or manual lookup check-in occurs Then the operation is stored in encrypted local storage with timestamp and a unique client operation ID And the UI shows a confirmation with a visible "Pending sync" badge When connectivity is restored Then queued operations auto-sync in original order with server-side idempotency using the client operation ID And conflicts (e.g., attendee already checked in) are resolved by server authority and reflected on the kiosk within 3 seconds with clear status messaging
Undo Mis-Scan Window
Given a check-in was just completed on this device When the user taps Undo within 10 seconds Then the check-in is reversed locally and on the server, attendee status reverts, and the instructor’s device reflects the change within 2 seconds And the reversal is recorded in the audit log linked to the original operation ID When offline and Undo is tapped within 10 seconds Then the reversal is queued and applied on reconnect with server-side conflict resolution
Audit Trail of Check-in State Transitions
Rule: Every attempt and outcome (success, error, undo, sync) creates an immutable audit entry with attendee ID, class ID, device ID, operation type, UTC timestamp, source (scan/manual), result, and operation ID Rule: Audit entries are retrievable via admin UI/API filtered by class, attendee, and time range, and reflect server state within 5 seconds of sync completion Rule: Audit logs must not store raw token payloads; only non-reversible identifiers or hashes
Real-time Roster Sync to Instructor Devices
"As an instructor, I want kiosk check-ins to update my roster in real time so that I can see who has arrived and manage last-minute openings."
Description

Broadcast check-in, undo, and waitlist-seat consumption events from the kiosk to instructor views in real time using WebSockets or SSE. Ensure sub-300ms median latency, ordered delivery per class, and automatic resubscription on connectivity changes with delta resync. Update arrival counts, roster badges, and waitlist availability instantly, supporting multiple instructors and mirrored displays. Throttle and debounce updates to conserve battery and bandwidth on mobile devices. Provide monitoring and diagnostics for dropped connections and retry storms.

Acceptance Criteria
Real-Time Event Propagation ≤300ms Median
Given an active class session with kiosk and at least one instructor device subscribed When a check-in, undo check-in, or waitlist-seat-consumed event occurs at the kiosk Then the instructor device receives and applies the event with end-to-end median latency ≤300ms across 200 events and p90 ≤600ms, measured from kiosk emit to UI state apply
Ordered Delivery Per Class Session
Given events carry a monotonically increasing sequence per class When multiple events are published in quick succession Then the instructor device applies events strictly in ascending sequence with no gaps or reordering And if an out-of-order event is received Then the client requests missing deltas and applies events only once the sequence is contiguous
Auto-Resubscribe with Delta Resync on Connectivity Changes
Given the instructor device loses connectivity for up to 5 minutes When connectivity is restored Then the client auto-resubscribes within ≤2 seconds and requests deltas since the last acknowledged offset And missing events are applied exactly once without duplicating previously applied events Given connectivity flaps cause 5 reconnects within 60 seconds When backoff is engaged Then retry cadence escalates to a max interval of 30 seconds and does not exceed 10 attempts per minute
Instant UI Updates: Arrival Counts, Badges, Waitlist
Given the roster and waitlist are visible on the instructor device When a check-in event is applied Then arrival count increments, the attendee row shows a Checked In badge, and waitlist availability updates within ≤100ms of event apply When an undo check-in event is applied Then arrival count decrements and the attendee row reverts to Not Checked In within ≤100ms When a waitlist-seat-consumed event is applied Then roster count increments and waitlist count decrements atomically within ≤100ms
Multi-Device Consistency for Instructors and Mirrors
Given up to 10 instructor or mirrored displays are subscribed to the same class When any check-in, undo, or waitlist consumption event is broadcast Then all devices converge to the same roster state with maximum inter-device divergence ≤500ms and no conflicting badges or counts Given a device rejoins after being offline When it resubscribes Then it completes delta resync and matches the latest class state within ≤2 seconds
Throttle/Debounce to Conserve Battery and Bandwidth
Given a burst of 60 events over 30 seconds When UI update coalescing is active Then the instructor app limits UI renders to ≤4 per second and average network overhead is ≤1 KB per business event without dropping or delaying final state beyond 600ms Given the app is idle for 2 minutes When keep-alive is maintained Then heartbeat frequency is ≤1 ping every 25 seconds and battery drain attributable to sync remains ≤2% per hour with screen on Given the app is backgrounded by the OS When connectivity is throttled Then the client suspends UI work and defers reconnection to a single attempt until foregrounded
Monitoring & Diagnostics for Connection Health
Given connections are established, retried, or dropped When telemetry is emitted Then metrics include connect_success, connect_failure, retry_count, bytes_rx, bytes_tx, events_applied, and p50/p90 latency with class_id and device_id labels Given a retry storm defined as ≥5 reconnects in 60 seconds for ≥2 minutes affecting ≥5% of active devices for a class When detected Then an alert is raised and a circuit breaker backs off to ≥30 seconds between retries Given a support investigation for a device When diagnostics are retrieved Then logs include correlation_id, last_ack_offset, last_delta_range, last_error_code, and connection state transitions for the past 24 hours
Waiver Nudge and Missing Info Capture
"As a guest, I want the kiosk to prompt me for any required waivers or missing info so that I can complete everything on the spot and avoid delays."
Description

After a successful scan, detect missing required items (e.g., signed liability waiver, emergency contact, age/guardian consent, marketing opt-ins per policy) and route the attendee through a minimal, mobile-friendly completion flow on the kiosk. Store versioned, time-stamped signatures with device metadata and link them to the customer profile. Respect configuration rules (block vs. warn) and allow instructor override where permitted. Resume and finalize check-in automatically upon completion. Localize copy and ensure legal text rendering and retention meet compliance requirements.

Acceptance Criteria
Blocking Flow: Missing Liability Waiver After QR Scan
Given a kiosk in blocking mode for unsigned liability waivers And an attendee scans a valid QR code without a signed waiver on file When the kiosk routes to the waiver nudge flow Then the attendee is required to complete no more than 3 screens to submit the waiver And the legal text displayed matches the active waiver version configured for the class/location And the Submit button remains disabled until required fields are valid and the legal text is scrolled to the end When the attendee signs and taps Submit Then the system stores a versioned record linked to the customer profile including: waiver version ID, signed_at (UTC), kiosk device ID, app version, locale, and IP hash And check-in is finalized automatically within 3 seconds and a full-screen success confirmation is shown And the instructor’s device reflects the attendee as “Checked in” within 3 seconds
Warning Flow: Non‑Blocking Missing Marketing Opt‑In
Given a kiosk configured to warn (not block) for missing marketing opt-in And an attendee scans a valid QR code without a marketing opt-in on file When the nudge screen is shown Then the attendee may Skip and still complete check-in And the attendee may choose Subscribe or Decline with explicit consent logging (choice, timestamp, device ID, locale) When the attendee Skips or makes a choice Then check-in finalizes and the instructor view shows a “Missing Info” badge if still incomplete And an audit event is recorded with outcome: skipped, subscribed, or declined
Instructor Override of Blocking Requirements
Given a kiosk in blocking mode for missing waiver or emergency contact And an attendee declines to complete the flow When an instructor enters a valid override via PIN on the kiosk or from the instructor device Then the system logs the override with actor ID, timestamp, reason code, and affected requirement(s) And check-in finalizes and is flagged “Override” And the customer profile still reflects the unmet requirement as pending
Emergency Contact and Age/Guardian Branching
Given emergency contact is required per policy and age/guardian consent is required for attendees under 18 And an attendee scans with missing DOB and no emergency contact on file When the flow prompts for DOB Then if DOB indicates under 18, guardian name, relationship, and signature are required And if DOB is 18 or over, guardian fields are not shown And emergency contact name and phone are required and validated (phone E.164 format) When valid data is submitted Then records are stored and linked to the customer profile and check-in finalizes automatically
Legal Text Rendering, Versioning, and Retention
Given a published waiver version is active for the class/location When the kiosk displays the waiver Then the exact text and formatting of the active version are rendered without truncation at a minimum 16px equivalent font size And the language shown matches kiosk locale with fallback to default if translation missing When the attendee submits a signature Then the stored record captures a content hash of the rendered legal text, waiver version ID, and locale And the record is retrievable via admin audit with immutable timestamp and device metadata And retention policies are applied per configuration (min 7 years) and records are read-only
Localization and Accessibility of the Nudge Flow
Given the kiosk language is set to Spanish When an attendee with missing items enters the nudge flow Then all UI labels, buttons, error messages, and legal text appear in Spanish And date, time, and number formats follow the locale And the flow meets kiosk accessibility basics: focus order logical, buttons 44x44pt minimum, and labels have sufficient contrast (WCAG AA) And switching kiosk language updates the flow within one screen without data loss
Auto‑Resume and Real‑Time Status Update
Given an attendee completes all required steps in the nudge flow When the final submission succeeds Then the kiosk returns to the check-in confirmation screen automatically and resets to scan-ready after 5 seconds And the attendee’s check-in status updates in the instructor view within 3 seconds And if the flow is abandoned or times out after 60 seconds of inactivity, no check-in is finalized and the session clears to scan-ready And partial data entered before timeout is not saved except audit events and rate-limit counters
Accessible, Brandable Kiosk UI (PWA)
"As a guest, I want a simple, accessible kiosk interface that works on any tablet or phone so that I can check in quickly regardless of device or accessibility needs."
Description

Deliver a mobile-first, kiosk-optimized UI with large touch targets, high contrast, and WCAG 2.1 AA compliance including screen reader labels, focus order, and language selector. Provide a configurable attract/idle screen, automatic screen wake lock, and adjustable confirmation duration. Support theming (logo, colors, background) and privacy mode to obfuscate personal details after a short timeout. Package as a cross-platform PWA with robust camera permission handling on iOS/Android, network status detection, and graceful degradation for older devices and low light environments.

Acceptance Criteria
Accessible High-Contrast UI with Large Touch Targets
Given the kiosk runs on a mobile or tablet device (≤768px width), When the UI renders, Then every tappable control has a minimum 44x44 px touch target with at least 8 px spacing. Given any text is displayed, When contrast is measured, Then text-to-background contrast ratio is ≥4.5:1 for normal text and ≥3:1 for large text/icons. Given the user sets device font size to 200% (OS accessibility), When the kiosk loads, Then content reflows without clipping/overlap and all controls remain reachable without horizontal scrolling. Given orientation changes (portrait/landscape), When the kiosk UI adjusts, Then primary actions remain above the fold and tap targets remain ≥44x44 px. Given color perception is reduced or grayscale mode is enabled, When state is conveyed (e.g., success/error), Then it is also indicated via text and/or iconography, not color alone.
Screen Reader Labels and Logical Focus Order
Given a screen reader is active (VoiceOver/TalkBack), When navigating the kiosk, Then every actionable element announces a descriptive accessible name, role, and state that matches its visual purpose. Given keyboard or switch access is used, When tabbing through the UI, Then focus order follows visual order and cycles without traps; a "Skip to scanner" control is focusable as the first element. Given a modal dialog (e.g., confirmation) opens, When focus management occurs, Then focus moves into the modal, background content is aria-hidden, and focus returns to the trigger on close. Given the QR scanner view is active, When focus is set, Then the scanner region has an accessible label and brief instructions, and no continuous announcements interfere with scanning.
Language Selector Availability and Persistence
Given the kiosk detects browser locale, When first loaded, Then default language matches the browser locale if supported, else falls back to English. Given the user opens the language selector, When a different language is chosen, Then all visible UI text and ARIA labels update immediately without page reload. Given a language is selected, When the kiosk is reloaded on the same device, Then the selected language persists via local storage. Given a right-to-left language is selected, When the UI renders, Then layout direction switches to RTL and remains readable and navigable.
Configurable Attract/Idle Screen Behavior
Given the kiosk is idle for the configured threshold (e.g., 30–120 seconds), When no user interaction occurs, Then the attract screen displays with brand logo, background, and call-to-action. Given the attract screen is displayed, When the screen is tapped or a QR enters camera view, Then the kiosk returns to the scanner within 500 ms. Given an admin changes the idle timeout and assets in settings, When the kiosk reloads, Then the attract screen uses the new timeout and assets without code changes. Given a screen reader is active, When the attract screen appears, Then it makes a single concise announcement and does not loop repetitive speech.
Wake Lock Activation and Fallback Handling
Given the device/browser supports the Screen Wake Lock API, When the scanner view is active, Then a wake lock is requested and remains active until exit or 2 minutes of inactivity. Given wake lock is denied or unsupported, When the scanner view is active, Then the app displays a one-time non-blocking tip to adjust auto-lock settings and keeps the UI fully functional. Given the app is backgrounded or the tab loses visibility, When visibility changes, Then the wake lock is released and re-acquired on return to the scanner view. Given wake lock is active, When the session ends, Then it is released within 1 second to conserve battery.
Privacy Mode Obfuscation after Inactivity
Given a check-in confirmation shows personally identifiable information, When the configured privacy timeout elapses (e.g., 5–15 seconds) without interaction, Then names and contact details are obfuscated (e.g., J*** D***, last 2 digits only). Given privacy mode is active, When a new scan occurs, Then full details are shown only for the new attendee and previous attendee details remain obfuscated. Given staff need to verify details, When a valid staff PIN or "Reveal" action is used, Then details reveal for 10 seconds and automatically re-obfuscate. Given the kiosk returns to the attract screen, When it re-enters scanner mode, Then prior personal data is not visible in the UI or view history.
PWA Camera Permission, Low-Light, and Offline Resilience
Given first-time camera access, When the scanner initializes, Then the app shows an in-app explainer and triggers the OS-native camera permission prompt without freezing. Given camera permission is denied or unavailable, When scanning is attempted, Then the app offers manual code entry and scan-from-gallery fallback and hides the live camera preview. Given ambient light is below a defined threshold, When scanning begins, Then a torch toggle is shown (if supported) and exposure is adjusted; scanning remains successful at 10 lux with ≥95% success rate within 2 seconds. Given the network goes offline, When check-ins occur, Then they are queued locally (minimum capacity 200 events) with timestamps and automatically sync within 10 seconds of reconnection; an offline banner is visible while offline.
Kiosk Settings, Health Monitoring, and Analytics
"As a studio owner, I want to configure kiosk behavior and view check-in analytics so that I can optimize the flow and reduce bottlenecks."
Description

Provide owner/admin controls to enable/disable kiosk per class or location and configure behaviors (manual lookup allowed, waiver enforcement mode, confirmation screen length, offline allowances, pairing timeout). Expose real-time health status (paired, online/offline, battery, last check-in) and send alerts when a kiosk goes offline before class. Capture analytics including check-in counts, average scan time, error categories, waiver completion rate, and offline queue size; surface trends and export or sync to BI. Ensure data retention aligns with privacy policies and support per-location segmentation.

Acceptance Criteria
Per-Class and Per-Location Kiosk Enablement Controls
Given an admin with permission opens Settings > Kiosk Controls When they toggle Enable Kiosk ON for Class A at Location X Then the kiosk pairing API returns 200 and the class accepts pair requests within 5 seconds of save Given the toggle is OFF for Class B at Location X When a kiosk attempts to pair or submit a check-in for that class Then the request is rejected with 403 and the kiosk displays "Kiosk disabled for this class" Given the admin bulk-enables kiosk for 5 classes When saved Then all 5 records reflect enabled=true in the database within 10 seconds and an audit log records user, time, and scope change Given an instructor (non-admin) opens Settings When viewing kiosk controls Then enable/disable toggles are not visible or actionable (RBAC enforced)
Behavior Settings Enforcement
Given Manual Lookup = Disabled When a user taps Find by name/email on the kiosk Then the control is hidden or disabled and not reachable via hardware keyboard Given Waiver Enforcement = Required When a guest without a signed waiver checks in Then the kiosk blocks completion and launches the waiver flow within 2 seconds; upon completion, check-in resumes and is marked waiver_signed=true Given Confirmation Screen Length = 3 seconds When a guest successfully checks in Then the success screen auto-dismisses in 3±0.5 seconds Given Offline Allowance = Enabled When network connectivity is lost Then the kiosk allows check-ins to queue up to N (configurable) and shows an Offline mode badge; if Disabled, check-in controls disable within 5 seconds of loss Given Pairing Timeout = 60 seconds When pairing is initiated and not completed within 60 seconds Then the pairing token expires and the kiosk returns to the idle screen
Real-Time Kiosk Health Dashboard
Given at least one kiosk is paired When viewing Admin > Kiosk Health Then each kiosk tile shows Paired status, Online/Offline, Battery %, Last heartbeat timestamp, and Last check-in time Given a kiosk network status changes When the change occurs Then the dashboard reflects the new online/offline state within 15 seconds (p95) Given battery drops below 20% When telemetry is received Then the tile displays a Low battery indicator and the current percentage Given no heartbeat received for 2 consecutive intervals When monitoring runs Then status transitions to Offline and last_seen is updated Given continuous updates When data is pushed/pulled Then update frequency does not exceed 1 request/second per kiosk and UI remains responsive (<200ms main-thread blocks p95)
Proactive Offline Alerts Before Class
Given a class with kiosk enabled starts at T0 When the paired kiosk has been offline for >5 minutes during the window [T0−15min, T0) Then send an alert to owner/admin via email and push immediately Given multiple kiosks at the same location go offline within 10 minutes When alerts are generated Then deduplicate into a single alert with a count of affected kiosks Given the kiosk returns online before T0 When evaluating alerts Then suppress further alerts for that class instance Given an alert is sent When delivery completes Then the event is logged with kiosk_id, class_id, timestamp, and delivery status; delivery success rate ≥99% (p95)
Check-in Analytics Capture and Trends
Given check-ins occur When analytics are processed in real time Then capture per class and location: total check-ins, average QR scan time, error counts by category (invalid QR, unpaid, duplicate, waiver required), waiver completion rate, and offline queue max size Given a date range is selected When the dashboard is viewed Then trends are displayed by day/week with filters for location, instructor, and class type Given Export CSV is requested for a range ≤31 days When processing begins Then a UTF-8 CSV with headers (≥100k rows supported) is generated within 60 seconds and downloadable via a signed URL Given BI sync is configured When the nightly job runs at 02:00 local time Then data is delivered to the destination with schema versioning; failures retry with exponential backoff up to 3 times and emit an error event
Data Retention and Privacy Compliance
Given retention policy = 24 months When records exceed 24 months of age Then PII is anonymized or deleted per policy within 24 hours while preserving aggregate metrics Given a user submits a deletion request When processed Then kiosk analytics and check-in data linked to that user are deleted or anonymized within 30 days and excluded from future exports/BI sync Given per-location segmentation is enabled When exporting or syncing data Then only data for the selected location(s) is included; cross-location leakage = 0 Given consent/notice versions are updated When kiosk flows collect data (e.g., waiver) Then the current privacy/consent version IDs are logged with each event
Offline Queue Processing and Reconciliation
Given offline mode is active and Q check-ins are queued When connectivity is restored Then all queued check-ins are submitted in FIFO order within 60 seconds for Q≤200 and are idempotent by token to prevent duplicates Given class capacity is reached while offline When the queue is replayed Then overflow check-ins are marked waitlisted according to configured rules Given a queued check-in fails due to invalid or expired booking When replay occurs Then it is marked failed with an error code and appears in the Offline reconciliation report Given reconciliation completes When counts are computed Then the discrepancy between kiosk and server check-in counts is ≤0.5%

Doorway Drop‑In

No booking found or class just opened? Offer instant at-the-door options—redeem a pass, buy a drop-in, or join the waitlist—right from the instructor’s device with Apple/Google Pay. The guest is added to the roster on success, with receipts and reminders sent automatically.

Requirements

On-Device Express Pay
"As an instructor, I want to collect a drop-in payment on my device using Apple/Google Pay so that I can admit a guest immediately without using a separate POS or manual entry."
Description

Enable instructors to collect at-the-door payments on their own mobile devices using Apple Pay and Google Pay via a PCI-compliant gateway. Present context-aware price options (drop-in, pass purchase) with taxes/fees included, support SCA/3DS where required, and provide hosted-field fallback for manual card entry when wallets are unavailable. Handle payment states (initiated, pending, authorized, failed, succeeded) with clear UI feedback, retries, and cancellation. On success, immediately return a confirmation payload that downstream processes use to add the guest to the roster and trigger receipts. Ensure currency, locale, tax rules, and business configuration are honored, and log all transactions for reconciliation and refunds.

Acceptance Criteria
Wallet Detection and Eligibility
Given an instructor opens Doorway Drop‑In on a supported iOS/Android device, When a wallet is provisioned (Apple Pay on iOS or Google Pay on Android) and merchant domain is verified, Then an Express Pay button is shown with the correct platform brand and is enabled. Given no eligible wallet is available or device/OS is unsupported, When the payment sheet is prepared, Then Express Pay buttons are hidden/disabled and a “Pay with card” fallback is shown. Given multiple wallet cards exist, When the wallet sheet opens, Then the sheet displays the merchant name, total amount with currency, and line items, and allows card selection per platform conventions. Given the selected funding source is ineligible (e.g., MCC blocked, expired card), When the wallet returns an ineligible error, Then show an actionable error, keep the payment dialog open, and present the card-entry fallback option.
Context-Aware Pricing with Taxes and Fees
Given a class, attendee count, and business configuration, When presenting price options, Then show only applicable options (e.g., Drop‑in, Pass Purchase) based on inventory, eligibility, and configuration flags. Given regional tax rules, inclusive/exclusive tax configuration, and fees, When computing totals, Then the displayed option amounts include all taxes/fees and are rounded to the currency minor unit using the business’s rounding rules. Given device locale and business currency, When formatting amounts, Then prices and wallet sheet amounts use the business currency and locale‑appropriate formatting (e.g., symbol placement, decimal/thousand separators). Given a user selects a price option, When the wallet sheet opens, Then the total, tax, and fee breakdown (if supported by the wallet) match the option displayed in‑app and the computed backend total within ±0.01 of the currency minor unit. Given pass purchase is selected, When payment succeeds, Then the pass is created/credited with the correct products, validity, and remaining uses according to configuration.
SCA/3DS Challenge Handling
Given the payer’s region and scheme require SCA, When initiating payment via Apple Pay or Google Pay, Then native wallet authentication (biometrics/passcode) satisfies SCA and the gateway marks the transaction as SCA‑compliant. Given manual card entry is used and the gateway requires 3DS2, When the payment is submitted, Then a 3DS2 flow is initiated (frictionless or challenge), and the in‑app webview/SDK presents the challenge without leaving the app. Given a 3DS challenge times out, is canceled, or fails, When the gateway returns the outcome, Then the payment is marked Failed, an error message is displayed with a retry option, and no roster addition occurs. Given 3DS authentication succeeds and authorization is approved, When the gateway returns success, Then the payment state transitions to Authorized/Succeeded per capture mode and the flow proceeds to confirmation payload generation.
Hosted-Field Card Entry Fallback
Given Express Pay is unavailable or the user chooses to pay by card, When the card form is displayed, Then card number, expiry, and CVC inputs are rendered as hosted fields (iframes) from the PCI‑compliant gateway and no raw PAN/CVC is accessible to the app backend. Given the user enters card details, When validation runs, Then inline errors are shown for invalid fields, the Pay button remains disabled until all required fields pass Luhn/format checks, and postal code collection follows business rules. Given the card form is submitted, When tokenization succeeds, Then the app sends only the token and necessary metadata (amount, currency, tax/fee breakdown, class/pass identifiers) to the backend for authorization/capture. Given tokenization fails or the gateway is unreachable, When submission occurs, Then the user sees a clear error with a retry option and the form remains intact with previously entered data (except CVC which is cleared).
Payment State Management and UI Feedback
Given a payment attempt begins, When the user taps Pay, Then the UI shows a loading state and the payment is recorded as Initiated with an idempotency key tied to class, price option, and guest. Given the gateway response is delayed or asynchronous, When awaiting confirmation, Then the UI reflects Pending with a visible spinner and a timeout of 90 seconds before offering Retry or Cancel. Given the gateway returns an authorization approval with immediate capture, When the capture succeeds, Then the state becomes Succeeded and the confirmation step proceeds; if capture is deferred, state becomes Authorized and is treated as success for roster addition while capture is finalized. Given the gateway returns a hard decline or error, When processing completes, Then the state becomes Failed, a reason code is displayed, and the user can retry up to 3 times before the attempt is locked. Given the user cancels during Pending, When Cancel is confirmed, Then the attempt is marked Canceled, the wallet/card flows are aborted, and no further charges or roster changes occur. Given a duplicate tap or network retry, When the backend receives a request with the same idempotency key, Then no duplicate charge is created and the original outcome is returned.
Success Confirmation Payload and Downstream Triggers
Given a payment is marked Authorized or Succeeded, When the gateway response is received, Then a confirmation payload is generated within 2 seconds and returned to the client and event bus. Given the confirmation payload is produced, When inspected, Then it contains at minimum: payment_id, status, amount_total, currency, tax_amount, fee_amount, product_type (drop‑in|pass), class_id, instructor_id, payer_contact (email/phone if collected), payment_method (apple_pay|google_pay|card), created_at (ISO‑8601), locale, and receipt_url. Given the payload is received by downstream services, When processed, Then the attendee is added to the class roster immediately, inventory is decremented, and digital receipts and reminders are queued per business notification settings. Given the product_type is pass, When processing the payload, Then the pass entitlement is created/credited and linked to the guest before adding to the roster. Given any downstream step fails, When error handling runs, Then the payment remains succeeded, a compensating retry is scheduled, and operators see an alert with correlation_id for manual follow‑up.
Transaction Logging for Reconciliation and Refunds
Given any payment attempt occurs, When an event happens (initiated, pending, authorized, succeeded, failed, canceled), Then an immutable ledger entry is written with event_type, timestamp, actor (instructor_id), class_id, guest reference (if available), amount, currency, tax/fee breakdown, gateway ids, wallet/card brand and last4 (if allowed), 3DS outcome, and idempotency key. Given a successful payment, When settlement data is received from the gateway, Then the ledger entry is updated with capture_id, settlement date, and fee details without overwriting the original attempt record (append‑only model). Given an admin views reports, When exporting transactions for a date range, Then totals reconcile to gateway payouts within ±0.01 of the currency minor unit and each line links to a receipt and refund endpoint. Given privacy constraints, When storing payment method details, Then no full PAN/CVC is stored, and PII is minimized and encrypted at rest according to policy and PCI requirements for at least 24 months retention or per regional rules.
Pass Lookup & Redemption
"As an instructor, I want to look up and redeem a guest’s existing pass at the door so that they can join class without a prior booking or manual verification."
Description

Provide fast at-door identity lookup by phone, email, or name to find an existing customer profile and surface all active passes with remaining credits. Validate pass eligibility against the specific class (type, instructor restrictions, blackout dates), confirm redemption with the guest, decrement credits atomically, and record the redemption with notes. If no eligible pass exists, offer purchase options (drop-in or new pass) routed through Express Pay. Ensure deduplication if the guest is already booked, and create a customer profile on-the-fly for new guests with minimal required fields for future visits.

Acceptance Criteria
At-Door Identity Lookup by Phone, Email, or Name
Given the instructor is on the Doorway Drop‑In screen for a specific class When they enter a phone number, email, or name substring of at least 3 characters and submit Then matching customer profiles are returned within 2 seconds, ranked by match confidence And each result shows full name, avatar/initials, last visit date, and count of active passes with remaining credits And if no profiles match, a "Create New Guest" option is presented And selecting a result reveals full contact details only after selection for privacy
Eligibility Evaluation and Active Pass Surfacing
Given a customer profile is selected and a target class is in context When the system evaluates all passes on the profile Then passes eligible for the class based on class type, instructor restrictions, and blackout dates are listed under "Eligible" And ineligible passes are shown with explicit reasons (e.g., type mismatch, restricted instructor, blackout date) And expired or zero-credit passes are excluded from "Eligible" and labeled accordingly And the most cost-efficient eligible pass is preselected by default
Atomic Pass Redemption with Guest Confirmation
Given an eligible pass is selected and the guest verbally confirms use When the instructor taps Redeem Then exactly one credit is decremented atomically from that pass using an idempotency key to prevent double-charging on retries And the guest is added to the class roster immediately upon success And a redemption record is created with timestamp, class ID, pass ID, staff ID, and optional notes up to 250 characters And the operation completes within 3 seconds on a stable connection And if any step fails, no credits are deducted and the UI shows a clear, actionable error
Duplicate Booking Deduplication
Given the selected customer already has an active booking for the class When the instructor attempts to redeem a pass or add to roster Then the system prevents duplicate roster entries and shows "Already booked" with the existing booking details And no pass credits are decremented And the instructor can add an arrival note to the existing booking without creating a duplicate
New Guest Profile Creation On-the-Fly
Given no matching profile is found When the instructor selects Create New Guest and provides minimum required fields (first name plus either phone or email) Then a new customer profile is created within 2 seconds with phone normalized to E.164 if provided and email format-validated And if a profile with the same phone or email already exists, the system blocks duplicates and offers to select/merge instead And consent flags for SMS/email can be captured but are optional and do not block creation
Express Pay Purchase Flow for No Eligible Pass
Given no eligible pass exists or the guest declines to use an eligible pass When the instructor selects Drop‑In or New Pass and initiates Apple Pay or Google Pay Then the payment sheet displays the correct price, taxes/fees, and class details And upon successful payment, the guest is added to the roster, a new pass is issued if applicable, and remaining credits are updated And a receipt is sent automatically via SMS/email based on available contact And if payment fails or is canceled, no roster addition or pass issuance occurs and the UI indicates the reason And the end-to-end payment flow completes within 30 seconds
Redemption Recording, Receipts, and Reminders
Given a successful pass redemption or paid drop‑in completes When the transaction is finalized Then a detailed audit log entry is created (customer, class, pass/payment, staff, notes, device, and IP where available) And a digital receipt is sent automatically via SMS/email using the contact on file And class reminder notifications are scheduled according to account settings And staff can view the redemption record in the class roster and customer timeline within 5 seconds of completion
Instant Roster Add & Notifications
"As an instructor, I want successful at-door transactions to add the guest to the roster and trigger receipts/reminders so that attendance and communications stay accurate without extra work."
Description

Upon successful payment or pass redemption, automatically create a booking entry in the class roster, optionally mark the guest as checked-in, and allocate a seat/spot if the class uses assigned placement. Remove any duplicate hold or waitlist entries for the same guest. Trigger configured transactional communications: email/SMS receipt, calendar attachment, location/instructions, and class reminders per studio settings. Ensure idempotency (no duplicate roster entries), update attendance and revenue analytics in real time, and annotate the roster with the source (Doorway Drop‑In) and method (wallet, pass).

Acceptance Criteria
Wallet Payment Creates Roster Entry and Receipt
Given a class with available capacity and wallet payments enabled And a guest not already on the class roster When the instructor completes a Doorway Drop‑In Apple/Google Pay payment for the guest Then exactly one booking entry is created on the class roster for that guest And the booking status is Confirmed And the roster entry is annotated with source = "Doorway Drop‑In" and method = "wallet" And an email receipt is sent and an SMS receipt is sent if SMS is enabled and a valid phone exists, all within 60 seconds And the email includes a calendar attachment (.ics) for the class And the booking appears on the instructor’s roster view within 5 seconds
Pass Redemption Adds Roster Entry and Decrements Balance
Given a guest has an active eligible pass with at least 1 remaining credit And the guest is not already on the class roster When the instructor redeems the pass via Doorway Drop‑In for a class with available capacity Then exactly one booking entry is created on the class roster for that guest And the pass balance is decremented by 1 and the redemption is logged to the guest’s pass history And the roster entry is annotated with source = "Doorway Drop‑In" and method = "pass" And confirmation communications (email and/or SMS) are sent per studio settings within 60 seconds And booking-level revenue is recorded as $0 and attendance/usage analytics are updated accordingly within 5 seconds
Auto Check‑In and Assigned Placement Allocation
Given a class uses assigned placements and has at least one open spot And the instructor selects "Mark as checked-in" during Doorway Drop‑In When the booking is created Then the guest is marked as Checked-in on the roster And a specific seat/spot is allocated according to the studio’s assignment rules And the assigned placement is visible on the instructor’s roster within 2 seconds And no other guest is assigned to the same seat/spot
Duplicate Hold/Waitlist Cleanup on Roster Add
Given the guest has an existing hold and/or waitlist entry for the same class When the Doorway Drop‑In booking is successfully created Then all hold and waitlist entries for that guest for that class are removed And the guest appears only once on the class roster And the waitlist position counts update accordingly within 5 seconds And no further waitlist or hold notifications are sent to the guest for that class
Idempotent Re‑Submission Handling
Given the same payment intent or pass redemption is retried (e.g., duplicate tap or webhook replay) with the same guest and class When the operation is processed with the same idempotency key/payment intent ID/redemption ID Then no additional roster entries are created And no duplicate communications (receipts, confirmations, reminders) are sent And analytics (attendance, revenue, pass balance) are not incremented more than once
Transactional Communications Per Studio Settings
Given studio settings define enabled transactional communications (receipt channels, calendar attachment, location/instructions, reminders) When a booking is created via Doorway Drop‑In Then only the enabled communications are sent to the guest And calendar attachments reflect the correct class start/end time and timezone And messages include the class location and instructions as configured And reminder messages are scheduled at the configured offsets And channel opt-outs are honored and no message is sent to opted-out channels
Real‑Time Analytics Update
Given a booking is created via Doorway Drop‑In by wallet payment of $X or pass redemption When the roster entry is saved Then the class attendance count increments by 1 within 5 seconds And revenue analytics increase by $X within 5 seconds for wallet payments and by $0 for pass redemptions And the updates are visible in analytics dashboards and exports generated after the update
At-Door Waitlist Enrollment & Auto-Offer
"As a walk-in guest, I want to join the waitlist from the instructor’s device and get notified instantly if a spot opens so that I don’t miss an opening."
Description

If the class is full or the booking window is closed, enable the guest to join the smart waitlist from the instructor’s device by capturing preferred contact channel (SMS/email) and response window. Integrate with ClassNest’s live waitlist logic to prioritize and auto-offer openings, with configurable hold time and acceptance flows. For eligible guests, support auto-redeem (pass) or pay-on-accept (wallet link) and instantly convert accepted offers into roster entries. Provide immediate on-screen confirmation of queue position and send a confirmation message to the guest.

Acceptance Criteria
At-Door Waitlist Join with Contact Preference
Given the class is full or the booking window is closed When the instructor taps “Join Waitlist” on Doorway Drop‑In and enters guest name plus selects SMS or Email and a response window Then the submit action remains disabled until all required fields are valid and privacy consent is captured And upon submit the guest is added to the class waitlist within 2 seconds and a success state is displayed on the instructor device And if the guest already exists on the waitlist for this class, the system prevents a duplicate entry and surfaces the existing position
Response Window Capture and Contact Validation
Given the response window must be set When the instructor sets the response window Then only 5–60 minutes in 5‑minute increments are accepted with a default of 15 minutes And values outside the range or non-integer increments are rejected with inline error messaging And for SMS, phone number input must be valid E.164; otherwise submission is blocked with error And for Email, address must pass basic RFC 5322 pattern validation; otherwise submission is blocked with error
Auto-Offer Prioritization and Hold Timer
Given a guest is added to the waitlist When a seat opens Then the system issues an auto-offer to the highest-priority eligible guest per ClassNest waitlist rules within 10 seconds And the offer message displays the configured hold time for the class (default 10 minutes) And if the hold time elapses without response, the offer auto-expires and the next eligible guest is offered within 10 seconds And all offer state transitions (sent, accepted, declined, expired) are auditable with timestamps
Pass Auto‑Redeem on Offer Acceptance
Given a guest has an eligible active pass When the guest accepts the offer via the received link or reply Then the pass is auto‑redeemed atomically and the class credit balance decremented And no payment prompt is shown And a receipt is sent to the selected channel within 60 seconds And if redemption fails (e.g., insufficient balance or pass not valid for class), the flow falls back to Pay‑on‑Accept and the user is informed without creating a roster entry
Pay‑on‑Accept via Wallet Link
Given a guest without an eligible pass receives an offer When the guest accepts the offer Then a secure wallet link opens supporting Apple Pay and Google Pay And payment must complete before the hold time expires; a countdown is displayed And on successful payment authorization, a receipt and confirmation are sent within 60 seconds And on payment failure or timeout, the offer is voided and the next eligible guest is offered; the guest receives a failure/expired notification
Instant Roster Conversion and Capacity Enforcement
Given an offer is accepted and either pass redemption succeeded or payment succeeded When the system processes the acceptance Then the guest is added to the class roster within 3 seconds and removed from the waitlist And class capacity is updated without overbooking using optimistic concurrency to ensure only the first valid acceptance secures the seat And any subsequent acceptance attempts for the same seat receive a clear “seat no longer available” message and are not charged
On‑Screen Position and Confirmation Messaging
Given a guest successfully joins the waitlist at the door When the confirmation screen is shown Then the instructor’s device displays the guest’s queue position and an estimated offer window immediately And a confirmation message is sent to the chosen channel within 5 seconds including class name, date/time, response window, manage/opt‑out links, and support info And message delivery failures are retried according to policy and surfaced to the instructor with a non-blocking warning
Real-time Eligibility & Capacity Guardrails
"As a studio owner, I want the system to enforce capacity, product rules, and booking windows during at-door actions so that policies are upheld and overbooking is prevented."
Description

Enforce business rules during at-door actions: class capacity and seat holds, product eligibility, booking and late-entry windows, pass restrictions, waiver/consent completion, and duplicate booking prevention. Implement short-lived seat holds during payment/redemption to prevent race conditions; release holds on timeout or failure. Provide clear, actionable error states (e.g., class full—offer waitlist; pass ineligible—offer drop-in). All validations must be atomic and concurrency-safe to prevent overbooking and ensure consistent roster state.

Acceptance Criteria
Atomic Seat Hold During Payment
Given a class with available seats and a configured hold duration H seconds When an instructor initiates an at-door payment or pass redemption Then a single-seat hold is created immediately and prevents other bookings from consuming that seat until completion or expiry And if payment/redemption succeeds, the hold converts to one confirmed roster entry exactly once and the hold is released And if payment is declined, canceled, or no completion event is received within H seconds, the hold is released and the seat returns to inventory And concurrent attempts on the last seat result in exactly one success and the other receiving “Class full — join waitlist?” with no overbooking
Capacity Guardrail with Waitlist Fallback
Given a class at or over capacity When an instructor attempts an at-door add via drop-in or pass Then the roster add is blocked and the UI displays “Class full — join waitlist?” with a one-tap action And selecting Join Waitlist adds the guest to the waitlist with correct priority without altering the roster And if a seat becomes available before the guest leaves the flow, a new atomic seat hold must be required to proceed to payment; otherwise the guest remains on the waitlist
Booking Cutoff and Late-Entry Window Enforcement
Given a class with configured booking cutoff minutes before start and late-entry window minutes after start When the current time is earlier than the booking open time or past the booking cutoff Then at-door booking is disallowed and the UI shows a clear message with the next actionable option (e.g., join waitlist) When the current time is past class start and outside the allowed late-entry window Then at-door booking/check-in is blocked with an actionable message for alternatives And when within the allowed windows, at-door booking proceeds subject to all other validations
Product Eligibility and Pass Restriction Validation
Given a guest selects a pass to redeem at the door When the pass is expired, exhausted, blocked for this class type/location, or violates configured restrictions (e.g., blackout dates) Then redemption is blocked and the UI offers “Buy drop-in with Apple/Google Pay?” as an alternative When the pass is eligible Then the redemption proceeds and consumes exactly one use atomically with the roster add And for drop-in purchases, only class-eligible products are shown and non-eligible options are not displayed
Waiver and Consent Gate Before Roster Add
Given a class requiring an active waiver/consent and a guest without a current signed waiver When an instructor initiates an at-door add Then the flow blocks roster add and requires in-line e-signature capture And upon successful signature, the flow resumes exactly where it left off and proceeds with payment/redemption and roster add And if the guest declines or abandons the waiver, the transaction is canceled, no roster entry is created, and any active seat hold is released
Duplicate Booking Prevention and Idempotent Completion
Given a guest already on the class roster or holding an active seat hold for the same class When an at-door add is attempted for the same guest Then the system blocks the duplicate and shows “Already booked” (or “Seat already held”) with no additional charges or roster changes Given a guest is on the waitlist and a seat becomes available during the at-door flow When the guest proceeds Then the system atomically converts the waitlist spot to a single seat hold and completes payment/redemption once, resulting in one roster entry And repeated payment callbacks, retries, or submit taps result in at most one charge capture and one roster entry via idempotency keys
Role-Gated Actions & Audit Trail
"As a studio owner, I want only authorized staff to perform at-door payments and redemptions with a full audit trail so that I can maintain control and accountability."
Description

Restrict Doorway Drop‑In actions to authorized roles (e.g., Owner/Manager/Instructor with permissions) with lightweight re-auth at payment confirmation (PIN/biometric where available). Record an immutable audit log of who performed each lookup, redemption, charge, or waitlist action with timestamp, device metadata, and location. Surface audit entries in finance and attendance reports, and display the processing staff member on the roster booking. Provide configurable limits (e.g., max comps/overrides per class) and require elevated approval for price overrides or refunds at the door.

Acceptance Criteria
Role-Based Access to Doorway Drop‑In Actions
Given a logged-in staff user with role Owner or Manager or Instructor with the Doorway Drop‑In permission When they open the Doorway Drop‑In screen Then actions Lookup, Redeem Pass, Buy Drop‑In, and Join Waitlist are enabled for that user Given a logged-in staff user without the required permission When they attempt any Doorway Drop‑In action Then the action is blocked, an error "Insufficient permissions" is shown, and an audit entry is recorded with outcome "denied" Given API calls to Doorway Drop‑In endpoints without the required scopes When the request is made Then the API responds 403 with error code "forbidden.role" and no state change occurs Given a staff user’s permissions are changed by an admin When the user refreshes or re-opens the Doorway Drop‑In screen Then the updated permissions take effect within 30 seconds without requiring an app restart
Re‑Authentication at Payment Confirmation
Given a staff user initiates a Doorway Drop‑In payment via Apple Pay, Google Pay, or card When they tap Confirm Then the app prompts for biometric if available and enabled, otherwise app PIN re‑authentication is required Given biometric is available but fails 3 times or is not enrolled When re‑authentication is attempted Then the flow falls back to app PIN verification Given re‑authentication is requested When the user does not complete it within 60 seconds Then the payment is canceled and no charge is created Given successful re‑authentication When subsequent payments are initiated within 10 minutes Then no additional re‑authentication is required during that window Given the device has neither biometric nor an app PIN configured When the user attempts to confirm payment Then the app blocks payment and requires setting a PIN first Given a payment confirmation is completed When the audit entry is written Then the audit includes re‑auth method = biometric or pin without storing any biometric data
Immutable Audit Logging of Doorway Actions
Given any Doorway action (lookup, pass redemption, charge, waitlist add/remove, price override, refund) When the action is attempted Then a single audit entry is written containing auditId, action, actorUserId, actorRole, customerId, classId, timestampUTC (ISO 8601), deviceId, deviceOS, appVersion, ip (if web), geolocation (lat, lng, accuracyMeters, source or permission_denied), amount and currency (if monetary), paymentMethod (if monetary), outcome (success|fail|denied), reasonCode (optional), approvalAuditId (optional) Given an existing audit entry When an admin or API attempts to update or delete it Then the operation is rejected (405 for update, 403 for delete) and no mutation occurs Given an action completes When the audit stream is queried Then the entry is available within 5 seconds of completion Given an audit export is generated When entries are included Then each entry contains a tamper‑evident signature (HMAC) and can be verified using the tenant’s export key
Audit Visibility in Finance & Attendance and Roster Attribution
Given a Doorway Drop‑In charge or refund occurs When the Finance report is opened for the relevant date range Then the transaction appears with columns auditId, action, timestamp (venue timezone), actorName, device, location (city or Unknown), student, class/session, amount, taxes, fees, overrideAmount, approvalActor (if any) Given a class roster contains at‑door bookings When the roster or attendance report is viewed Then each affected booking shows "Processed by <Staff Name>" and the linked auditId, and waitlist conversions show "Auto‑offer" if system‑generated Given a new at‑door transaction is completed When reports are refreshed Then the entry appears within 1 minute of completion Given a report export (CSV) is generated When the file is downloaded Then it includes auditId and actor fields for all Doorway entries
Configurable Limits on Comps and Overrides per Class
Given the per‑class comp limit is set to N When staff attempts to comp the (N+1)th attendee for that class Then the action is blocked with message "Class comp limit reached" and an audit entry is recorded with outcome "denied" and reasonCode "limit_exceeded" Given a maximum door discount/override of D% is configured When staff attempts a price override exceeding D% Then elevated approval is required before proceeding Given two staff attempt comp actions concurrently when one slot remains When both submit within 2 seconds Then only the first commit succeeds and the second receives a limit exceeded error; the class total comps do not exceed N Given only Owner/Manager roles may edit limits When an Instructor attempts to change limits Then the change is blocked with 403 and an audit entry is recorded; when Owner/Manager updates limits, the change takes effect immediately for new actions and is logged
Elevated Approval for Price Overrides and Refunds at Door
Given a staff user without approval permission initiates a price override exceeding policy or a refund at the door When they submit the action Then the UI requires selection of an approver with "Approve at Door" permission who must re‑authenticate via biometric or PIN Given an approver is present When they provide re‑authentication, select a reason code, and optionally add a note Then the approval is granted and the action proceeds; the resulting audit includes approvalAuditId, approverUserId, reasonCode, and note Given an approval request is initiated When no approval is completed within 120 seconds or connectivity is offline Then the action is canceled with no state change and an audit entry is recorded with outcome "denied" and reasonCode "approval_timeout" or "offline" Given a refund is approved When it is processed Then it is executed to the original payment method (partial only if allowed by policy) and the receipt and finance report show "Approved by <Approver Name>"

Multi‑Host Sync

Let multiple staff scan the same class simultaneously without stepping on each other. Real-time sync prevents double check-ins, shows who scanned whom, and enforces role permissions—perfect for dual entrances and high-volume starts.

Requirements

Real-time Scan Sync Engine
"As a host at a high-volume class, I want my scans to sync instantly across all staff devices so that no attendee is checked in twice and lines keep moving."
Description

Implement a low-latency, bidirectional sync layer that propagates check-in events across all staff devices in under 300ms for the same class session. Use WebSockets with SSE/long-poll fallback, per-class channels, and idempotent event IDs to prevent race conditions. Ensure exactly-once effective processing via optimistic concurrency on the check-in service and event de-duplication on clients. Include heartbeats, exponential backoff, and presence signals so devices can detect stale connections. Integrate with existing booking/attendance records and QR/barcode scan flows. Provide operational metrics (latency, delivery success rate, active connections) and alerting. The outcome is instantaneous visibility of check-ins across multiple hosts without conflict.

Acceptance Criteria
Real-Time Propagation & Channel Isolation SLA
Given multiple staff devices are subscribed to the same class session channel via WebSocket When one device confirms a valid attendee check-in at time T Then all other connected devices receive and render the check-in with staff identifier and timestamp within 300ms p95 and 600ms p99 over a rolling 5-minute window And Given two concurrent class sessions are in progress When check-ins occur in one session Then no check-in events are delivered to devices subscribed only to the other session And Given observability is enabled When events are propagated Then per-class-channel metrics are recorded for end-to-end propagation latency, delivery success rate, and active connections, and alerts fire if p95 latency > 300ms for 3 consecutive minutes or delivery success rate < 99.5% over 5 minutes
Exactly-Once Processing with Idempotent IDs and Optimistic Concurrency
Given a booking QR/barcode is scanned multiple times within 5 seconds with the same idempotency key When the client submits duplicate check-in events Then only one attendance record is created/updated and subsequent duplicates return a 200 idempotent response without side effects and no duplicate UI entries And Given two hosts scan the same booking within 200ms of each other When both requests reach the check-in service Then optimistic concurrency ensures only one write succeeds, the other returns a 200 with "already checked in" state, and all devices show a single check-in And Given clients receive duplicated network deliveries When processing events Then clients de-duplicate strictly by event ID and do not re-render or double-count
Resilient Connectivity: WebSocket with SSE/Long-Poll Fallback and Replay
Given a device is connected via WebSocket When the connection drops Then the client attempts reconnection with exponential backoff (initial 1s, max 30s, jitter) and downgrades to SSE then long-poll if WebSocket cannot be re-established within 10s And Given the device reconnects after downtime When it resumes with the last acknowledged event cursor Then it receives and applies any missed events in order before processing new real-time events And Given the client misses 3 consecutive heartbeats When connection health is evaluated Then the client marks the connection unstable and initiates the fallback sequence
Presence, Heartbeats, and Stale Connection Detection
Given active hosts are connected to the class channel When heartbeats are received every 10s Then the presence list shows each host online with last-seen updated within 2s of receipt And Given a device stops sending heartbeats for 30s (3 missed intervals) When presence is evaluated Then the device is marked offline within 5s and appears offline to other hosts And Given a device is offline When a host attempts to scan Then the UI displays a connection-lost warning and prevents submission until connectivity is restored And Given the device reconnects When heartbeats resume Then its status switches to online on all other devices within 5s
Role-Based Permission Enforcement at Check-In
Given a user lacks the check-in.manage permission When they attempt to perform a scan Then the server rejects the action, no check-in event is broadcast, and the UI shows a permission denied message And Given a user has only check-in.view permission When connected to the class channel Then they receive real-time updates but cannot trigger check-ins And Given a user has check-in.manage permission When they scan a valid booking Then the check-in is accepted and broadcast to all authorized devices
Attribution: Show Who Scanned Whom
Given a valid check-in is processed When the event is broadcast Then the payload includes staff_id, staff_display_name, device_id, and an ISO-8601 timestamp And Given receiving devices render the event When the check-in list updates Then each entry displays "Scanned by <staff_display_name>" within 300ms of receipt And Given audit logs are queried for a check-in When details are retrieved Then the same attribution fields are stored and linked to the attendance record
Scan Flow Integration with Booking/Attendance Records
Given a booking with status Booked and a valid QR/barcode token When scanned Then the attendance record transitions to Checked-in and remains linked to the booking; subsequent scans return "already checked in" without additional state changes or broadcasts And Given a canceled or invalid booking token When scanned Then the system rejects the check-in, no events are broadcast, and an actionable error is returned and displayed And Given a waitlisted attendee is auto-promoted before class start When their booking is scanned Then the check-in succeeds and propagates to all connected hosts as a normal check-in
Double-Check-in Prevention
"As a check-in staff member, I want the system to block duplicate scans with a clear message showing who already checked a guest in so that we avoid errors and keep the line fast."
Description

Make the check-in action atomic and idempotent. On scan, perform a server-side compare-and-set on the attendee’s ticket status with a short deduplication window (e.g., 3–5 seconds) to neutralize near-simultaneous scans from different devices. Only the first valid event updates state; subsequent events receive a clear UI response indicating who already checked the attendee in and when. Disable the local scan button briefly after a successful scan to avoid accidental repeats. Log all attempts for audit. Integrate with bookings, passes, and payment status to ensure only eligible tickets can be checked in. The outcome is the elimination of double check-ins and the resulting billing or capacity inaccuracies.

Acceptance Criteria
Dual-Entrance Concurrent Scans
Given an attendee has an eligible booking with status "Booked" And two authorized hosts on different devices scan the same QR within 4 seconds When the first scan request reaches the server Then the server atomically updates status from "Booked" to "Checked-In", sets check_in_by to the first host, and sets check_in_at to the server timestamp And the server returns success to the first device And the second scan within the 4-second deduplication window returns "Already checked in by {host_name} at {HH:MM:SS}" without mutating state And attendance/capacity counters are incremented exactly once And both devices reflect the checked-in state and who scanned within 1 second of the server response
Same-Device Rapid Rescan Suppression
Given a host successfully checks in an attendee When the success state is shown Then the local scan button on that device is disabled for 2 seconds And any taps or scans during the disabled period do not trigger network requests And if a rescan is attempted within 4 seconds of the original scan, the server returns "Already checked in by {host_name} at {HH:MM:SS}" and no counters change
Ineligible Ticket Attempt Handling
Given an attendee's ticket is ineligible due to unpaid balance, expired/depleted pass, canceled booking, or blocked status When the QR is scanned by any host Then the server returns an ineligible response with a reason code and human-readable message And the attendee status remains unchanged And attendance/capacity counters are not updated And the attempt is logged with reason
Network Retries and Idempotency
Given the client assigns a unique idempotency key to each scan attempt And network instability triggers one or more automatic retries of the same scan When the server receives duplicate requests with the same key Then only the first request mutates state And subsequent duplicates return the same final outcome and check-in details as the first And attendance/capacity counters are not incremented more than once And all attempts are logged with a duplicate flag
Audit Logging and Traceability
Given scan attempts occur for a class session When viewing the audit log for that session Then each attempt (success, duplicate, ineligible, denied) includes attendee_id, ticket_id, class_id, host_id, device_id, server_timestamp, outcome, and reason where applicable And entries are ordered by server_timestamp And logs are retrievable in the admin audit view
Role Permission Enforcement at Scan
Given a user lacks the Check-In permission When they attempt to scan a ticket Then the server responds with HTTP 403 and an "Insufficient permissions" message And no state changes occur And the attempt is logged with outcome "denied" and the user's role
High-Volume Start Integrity
Given a class with 100 eligible attendees starting now And three hosts scan on separate devices across two entrances When 100 valid scans occur within 2 minutes with up to 50 near-simultaneous overlaps Then the system records exactly 100 checked-in attendees with zero double check-ins And average server response time per scan is <= 800 ms and the 95th percentile is <= 1500 ms And no capacity or billing counters exceed expected values
Scanner Attribution & Presence Indicator
"As a lead instructor, I want to see which staff member checked in each attendee and who is currently active so that I can coordinate coverage and resolve issues quickly."
Description

Display attribution for each check-in (staff name/initials, device label, timestamp) on the attendee list in real time, with color-coded indicators for recent activity. Show active hosts for the class and their connection status to support coordination across entrances. Provide a compact, mobile-first UI that doesn’t slow scanning and includes quick filters for “checked by me” and “needs attention.” Maintain an immutable audit trail accessible to managers. Respect privacy by only revealing attribution to staff assigned to the class. The outcome is transparent accountability and faster team coordination.

Acceptance Criteria
Real-time Attribution on Attendee Row
Given I am an assigned host on the class attendee list and scan a valid ticket, when the check-in is accepted, then the attendee row displays my initials (or display name per profile), my device label, and the local timestamp to the second within 300 ms end-to-end (P95 <= 500 ms). Given two assigned hosts scan the same attendee within 1 second, when the system resolves the conflict, then exactly one check-in is recorded, attributed to the earliest accepted scan, and an "also scanned by <initials>" badge with the second host appears for 10 seconds. Given an attendee is already checked in, when any host attempts to scan them again, then no duplicate check-in is created and the UI shows a non-blocking "Already checked in by <initials> at <time>" message. Given attribution is rendered, when I tap the attribution chip, then I see the full staff name and device label in a compact tooltip accessible via VoiceOver/TalkBack.
Color-Coded Recent Activity Indicators
Rule: Attendee row check-in recency indicator colors map as Green (<= 15 s since check-in), Amber (16–60 s), Grey (> 60 s). Rule: Host presence ring colors map as Green (action within last 15 s), Amber (heartbeat but no action 16–60 s), Grey (> 60 s or offline), Red (reconnecting). Given new activity occurs, when the indicator should change color, then it updates within 1 second and provides accessible color names and a non-color icon cue.
Active Hosts Presence & Connection Status
Given a class has two or more assigned hosts, when they open the class in the app, then a presence bar shows each host's initials avatar, device label on tap, and connection status as Online, Reconnecting, or Offline. Rule: Presence status is Online if last heartbeat < 10 s, Reconnecting if 10–60 s, Offline if > 60 s; updates every 5 s. Given a host goes offline while scanning, when they reconnect, then their status transitions from Reconnecting to Online and their missed heartbeats do not remove them from the bar for at least 2 minutes.
Quick Filters — "Checked by Me" and "Needs Attention"
Given I am an assigned host, when I tap "Checked by Me", then the list filters to attendees whose latest check-in attribution equals me for this class session and shows a count badge. Rule: "Needs Attention" includes attendees with any of: unpaid payment status, invalid pass, waiver missing, waitlist auto-offer pending, conflict-resolved duplicate scan, or manual ID required. Given I toggle "Needs Attention", when no attendees match, then I see a "0" badge and an empty state within 300 ms without blocking scanning.
Privacy and Role-Based Attribution Visibility
Rule: Only assigned hosts and managers see staff attribution (name/initials, device, timestamp) on attendee rows and in audit trail; unassigned staff see anonymized "Checked-in" with no staff identifiers. Given a notification about a check-in is sent, when the recipient is not assigned to the class and not a manager, then the message omits scanner identity. Given an API request from a non-privileged token, when fetching attendee list or check-in events, then attribution fields are omitted or masked.
Immutable Audit Trail for Managers
Given I am a manager, when I open the audit trail for a class, then I can view an append-only list of events including event type, attendee, staff ID, staff display name, device label, timestamp (UTC and local), and previous/new state. Rule: Audit entries cannot be edited or deleted by any role; corrections create new entries linked via previous_event_id. Given I export the audit trail, when the file generates, then I receive a CSV within 10 seconds for up to 5,000 events including a SHA-256 checksum of the export.
Mobile-First Performance and UI Compactness
Rule: On a 360×640 viewport, attendee row height <= 64 px and presence bar height <= 48 px; scanning FAB remains visible and tappable while filters are applied. Rule: Scan-to-ready time (from successful scan to UI ready for next scan) is <= 150 ms at P95 and <= 300 ms at P99 on reference devices; no camera reinitialization occurs due to attribution rendering. Given the device is offline, when I scan attendees, then check-ins queue locally with preserved timestamps, presence shows Offline, and on reconnection all queued check-ins sync with correct attribution within 3 seconds per 100 events.
Role-Based Permissions & Overrides
"As a studio owner, I want role permissions enforced during scanning with auditable overrides so that only authorized staff can check in or modify attendance records."
Description

Enforce server-side authorization for all scan and uncheck actions based on roles (Owner, Manager, Host, Assistant) and class assignments. Hosts can check in only for assigned classes; Managers can override status or undo check-ins with reason codes. Block access when a device or user is not permitted, and surface clear error states in the scanner UI. Minimize PII in broadcast events (no phone/email), sending only attendee first name/initial and booking ID. Record overrides and denials in an audit log with actor, timestamp, and reason. Integrate with existing team management and SSO/session tokens. The outcome is secure, compliant operation where only authorized actions are allowed.

Acceptance Criteria
Host Check-In Allowed Only for Assigned Class
Given a Host is authenticated with a valid session token for Org X And the Host is assigned to Class A occurring now And booking B belongs to Class A and is in booked state When the Host scans the attendee QR for booking B Then the server authorizes and sets booking B status to checked_in And the response is 200 with fields: bookingId=B, attendeeFirstNameOrInitial, actorId, actorRole="Host" And no attendee phone or email is present in the response And all connected scanners for Class A receive a sync event within 1 second of server write And the audit log records action=check_in with actorId, bookingId=B, classId=A, timestamp (UTC), reason=null
Host Check-In Blocked for Unassigned Class
Given a Host is authenticated for Org X but is not assigned to Class B And booking C belongs to Class B When the Host scans the attendee QR for booking C Then the server denies the action with 403 and errorCode=PERMISSION_DENIED_UNASSIGNED_CLASS And no state change occurs for booking C And no sync event is broadcast And the scanner UI displays "Not authorized for this class" and shows errorCode=PERMISSION_DENIED_UNASSIGNED_CLASS And the audit log records action=denied with actorId, bookingId=C, classId=B, reason=unassigned_class, timestamp (UTC)
Manager Undo Check-In Requires Reason Code
Given booking D for Class A is in checked_in state And a Manager is authenticated with a valid session token for Org X When the Manager selects Undo Check-In for booking D and provides reasonCode from the configured set Then the server sets booking D status to booked and returns 200 And all connected scanners for Class A receive an update within 1 second And the audit log records action=undo_check_in with actorId, bookingId=D, classId=A, reasonCode, timestamp (UTC) And if no reasonCode is provided, the server returns 422 with errorCode=MISSING_REASON_CODE and no state change occurs
Session or Device Not Permitted Handling
Given a scanner initiates a scan with an expired or invalid session token When any check-in or undo action is attempted Then the server returns 401 with errorCode=INVALID_OR_EXPIRED_TOKEN and no state change occurs And the scanner UI shows a re-authentication prompt and disables scanning until login Given a user is revoked from the team or the device is not permitted When any scan action is attempted Then the server returns 403 with errorCode=ACCESS_FORBIDDEN and no state change occurs And the audit log records action=denied with userId (if present), deviceId (if present), reason=access_forbidden, timestamp (UTC)
PII-Minimized Sync Event Payload
Given any check-in, undo, or denial event is emitted to connected scanners When consuming the event payload Then the attendee object contains only bookingId and attendeeFirstNameOrInitial And the payload excludes attendee phone, email, address, payment details, and DOB fields And automated schema validation rejects events containing any excluded fields with errorCode=PII_VIOLATION And actor information may include actorId, actorDisplayName, and actorRole but must exclude actor phone and email
Audit Trail for Overrides and Denials
Given an override (e.g., undo check-in) or a denied action occurs When the server processes the request Then an immutable audit record is written with orgId, classId, bookingId, actorId, actorRole, action, reasonCode (if any), timestamp (UTC), clientDeviceId And the record is available via the audit API within 5 seconds of the action And audit records are read-only; any modification attempt by non-admins returns 403 with errorCode=AUDIT_IMMUTABLE
Role Resolution via Team Management and SSO Tokens
Given a user authenticates via SSO and receives a session token scoped to Org X When the user initiates a scan Then the server resolves the user role and class assignments from the team management store And authorization decisions reflect the resolved role and assignments And cross-org access attempts return 403 with errorCode=ORG_MISMATCH and no state change And role/assignment changes take effect for new requests within 60 seconds And token revocation invalidates active sessions within 60 seconds
Multi-Entrance Device Session Management
"As a floor coordinator, I want to register and label multiple scanners to the same class so that teams can cover different entrances without conflicts."
Description

Allow multiple devices to join a shared class session via a join QR code or deep link, with optional entrance labels (e.g., Door A, Door B) and device nicknames. Show the number of connected devices and their labels to all hosts. Provide a quick onboarding flow that verifies role, class assignment, and network status. Support session revocation (kick a device) and rate limiting to prevent abuse. Persist session membership for the class’s duration with automatic cleanup at end time. Integrate with the sync engine presence channel and staff directory. The outcome is simple setup for dual entrances and high-volume starts without stepping on each other.

Acceptance Criteria
QR Join With Entrance Label and Nickname
Given a published class session is active and a host displays the join QR code When a second device scans the QR code and provides Entrance Label "Door A" and Nickname "Sam iPhone" Then the device joins the session within 2 seconds And all connected hosts immediately see the new device listed with Entrance Label "Door A" and Nickname "Sam iPhone" And the total connected device count increments by 1 on all host UIs within 1 second And if Entrance Label is omitted, the UI indicates it as "Unlabeled" And if Nickname is omitted, a generated nickname is displayed And if the device disconnects and reconnects during the class, its Entrance Label and Nickname persist for that session
Deep Link Join With Role, Class, and Network Verification
Given a staff member taps a class session deep link When the staff member exists in the staff directory and has Host permission for the target class And the device passes the network connectivity check Then the onboarding flow verifies role and class assignment and joins the session in under 3 seconds And successful joins publish the device to the sync engine presence channel with staff ID, role, and optional entrance label And if the staff member lacks permission or is not assigned to the class, the join is blocked with an error "Permission denied" and no presence is published And if the device fails the network check, the join is blocked with an error "Network unavailable" and no presence is published
Live Device List and Count Visible to All Hosts
Given multiple devices are connected to the same session When any device joins, updates its Entrance Label or Nickname, or disconnects Then all host UIs update the device list and total count within 1 second via the presence channel And each list entry shows Device Nickname, Entrance Label (if set), and Staff Name from the directory And the list order is deterministic by join time ascending And no device appears duplicated; concurrently arriving devices are de-duplicated by device/session ID
Session Revocation (Kick) Propagates and Persists
Given a host selects a connected device and chooses "Kick" When the kick is confirmed Then the target device is removed from the session across all hosts within 1 second And the target device receives a "Removed from session" message and is disconnected And the target device cannot auto-rejoin for 60 seconds and any join attempt during that window is rejected with "Access revoked" And the device may rejoin after the cooldown only by completing the onboarding flow again
Join Rate Limiting Prevents Abuse
Given a device repeatedly attempts to join the session When more than 5 join attempts occur from the same device fingerprint or IP within 60 seconds Then subsequent attempts within the next 60 seconds are rejected with HTTP 429 and a UI error "Too many attempts — try again in 1 minute" And a security log entry is recorded with timestamp, device fingerprint, IP hash, and class ID And rate limiting resets after the cooldown and allows a new attempt
Session Membership Persists for Class Duration With Auto-Cleanup
Given a class session has a scheduled end time When devices join during the class window Then their session membership persists through transient network interruptions up to 2 minutes without manual action And at the scheduled end time, all devices are automatically removed, the presence channel is closed, and device lists clear on all hosts And any new join attempts after cleanup are rejected with "Session ended"
Presence Channel and Staff Directory Integration
Given a device joins a session Then a presence record is created or updated with device/session ID, staff ID, role, nickname, entrance label, and timestamp And the staff name and avatar are resolved via the staff directory and displayed to hosts And when the device disconnects, the presence record is removed within 2 seconds And if the staff directory entry is disabled during the session, the device is removed from the session and presence within 2 seconds
Offline Queueing & Reconciliation
"As a host in a low-signal venue, I want to continue scanning offline and have the system auto-reconcile on reconnect so that check-in stays smooth and accurate."
Description

Enable scanning when a device loses connectivity by queuing signed check-in intents locally with timestamp and event nonce. On reconnection, reconcile against the server using idempotent keys and resolve conflicts by honoring the earliest valid scan, returning context if the attendee was already checked in elsewhere. Provide clear offline indicators, capacity warnings, and a manual fallback (search by name) with the same reconciliation safeguards. Encrypt cached data at rest and auto-expire stale queued items after the class ends. The outcome is resilient check-in flows in low-signal venues without introducing duplicates.

Acceptance Criteria
Offline Scan Queueing (No Connectivity)
Given the device loses connectivity during an active class When a host scans a valid attendee code Then the app displays an offline indicator within 1 second And the queued check-in count increases by 1 And a locally stored check-in intent is created with attendeeId, classId, deviceId, roleId, ISO-8601 timestamp, eventNonce (UUIDv4), idempotencyKey, and a device-signed signature And the attendee is marked Pending (offline) on the device And repeat scans of the same attendee within 60 seconds show Already queued and do not add another intent
Reconnection Reconciliation and Idempotency
Given there are queued offline check-in intents and network connectivity is restored When the app detects connectivity Then synchronization starts automatically within 3 seconds And intents are submitted in ascending timestamp order And the server deduplicates by idempotencyKey and attendeeId And the earliest valid scan across devices wins and is recorded as the attendee’s official check-in And later duplicates are rejected with reason ALREADY_CHECKED_IN including winning deviceId, hostName, and serverTimestamp And the device updates local statuses to Checked in or Already checked in elsewhere accordingly And no attendee ends with more than one check-in record And 200 queued intents complete reconciliation within 10 seconds on a stable connection
Conflict Resolution Context Display
Given an offline-queued intent is rejected due to a prior check-in on another device When reconciliation completes Then the device shows a contextual message naming the host/device, entrance label, and server timestamp of the winning scan And the local item status is set to Already checked in elsewhere And a link to the attendee’s audit trail is available from the check-in row
Manual Fallback Search by Name (Offline)
Given the device is offline and the class roster is cached When the host taps Search and enters at least 2 characters of a name Then matching attendees return from the local cache within 300 ms for up to 5,000 attendees And selecting a result creates a check-in intent with the same fields and Pending (offline) – Manual status And if no roster cache exists the app shows Offline search unavailable and does not allow manual check-in And reconciliation on reconnect applies the same idempotency and earliest-valid rules
Capacity Warnings and Permissioned Overrides (Offline)
Given the device has cached class capacity and remaining spots When offline check-ins are queued Then the local remaining count decrements per queued intent And when remaining reaches 0 the app shows Capacity reached and blocks further check-ins for users without Overbook permission And users with Overbook permission may queue additional check-ins which are labeled Over capacity (pending) And on reconnection the server rejects later duplicates beyond capacity and devices display resolution summaries
Auto-Expiry of Stale Queue Items Post-Class
Given the scheduled class end time passes When the grace period elapses (default 30 minutes, configurable) Then any unsent queued check-in intents auto-expire and are not transmitted And the app notifies the host with the count of expired items and provides a view to review and dismiss them And expired items are labeled Expired – not applied and are purged from the device within 24 hours
Encrypted Local Cache and Offline Permission Enforcement
Given check-in intents and roster data are stored on the device When data is written to local storage Then it is encrypted at rest and cannot be read in plaintext via file inspection And accessing the offline queue requires the user to unlock the app via PIN or biometric if enabled And role permissions are enforced offline so that unauthorized actions cannot be queued And on reconnection the server revalidates roleId for each intent and rejects any that violate permissions with a PERMISSION_DENIED reason

Snap Switch

Swipe to hop between back-to-back or adjacent rosters without leaving the camera. Sticky filters (e.g., first-timers, members) and clear class headers prevent wrong-class check-ins—ideal for tight turnarounds and shared spaces.

Requirements

In-Camera Swipe Roster Navigation
"As an instructor checking in attendees with the camera, I want to swipe between adjacent class rosters without leaving the scanner so that I can handle back-to-back classes quickly."
Description

Within the check-in camera view, enable left/right swipe gestures (and optional tap arrows) to switch instantly between eligible rosters without exiting the scanner. Provide smooth, low-latency transitions, subtle haptics, and visual affordances indicating previous/next classes. Maintain scan readiness during transitions and preserve current scan mode, flashlight state, and search input. Integrates with the existing check-in module as an overlay that occupies minimal screen space and respects mobile safe areas.

Acceptance Criteria
Swipe and Arrow Navigation Between Eligible Rosters
Given I am in the check-in camera view with multiple eligible rosters available When I swipe left or tap the right arrow Then the next eligible roster becomes active without leaving the camera view Given I am viewing the last eligible roster When I attempt to swipe left or tap the right arrow Then the navigation affordance indicates no further rosters (disabled arrow and subtle edge-bounce) and no navigation occurs Given I am in the check-in camera view When I swipe right or tap the left arrow Then the previous eligible roster becomes active Given tap arrows are displayed Then each arrow has a minimum 44x44 pt touch target and is hidden or disabled when no adjacent roster exists Given eligible rosters are supplied by the check-in module Then only eligible rosters are navigable and they appear in chronological order
Scan Mode, Flashlight, and Scanner Continuity
Given scan mode is set (e.g., QR vs manual) When I switch rosters via swipe or arrow Then the selected scan mode remains unchanged Given the flashlight is ON When I switch rosters via swipe or arrow Then the flashlight remains ON Given the camera is scanning When a QR code is presented during a transition Then the scan is processed without error and attributed to the roster visible at the end of the transition Given I switch rosters 20 times in succession Then the scanner remains responsive on each landing roster without requiring a reload
Low-Latency Transitions
Given a supported device under typical app load When I switch rosters Then the transition completes within 200 ms and the camera preview maintains at least 24 FPS during the transition Given a roster switch completes Then the scanner is ready to accept a scan within 100 ms of the transition end Given 100 consecutive roster switches Then no crashes occur and no fatal errors are logged
Haptic Feedback on Roster Switch
Given device haptics are enabled When a roster switch completes Then a single light impact haptic is emitted Given device haptics are disabled at OS level or within the app When a roster switch occurs Then no haptic is emitted Given I attempt to navigate past the first or last roster When the edge-bounce feedback occurs Then no haptic is emitted Given I perform rapid successive switches When multiple switches complete within 1 second Then at most one haptic is emitted per completed switch
Visual Affordances and Class Headers
Given the current roster is displayed Then a persistent header shows the class title and scheduled start time and remains visible during scanning and transitions Given adjacent rosters exist Then peek indicators or labeled chevrons show the names and start times of previous and next classes without obstructing the scan area Given no adjacent roster exists on a side Then the corresponding indicator is hidden or visually disabled Given a check-in is confirmed Then the class header remains visible to confirm the class context
Safe Areas and Minimal Overlay Footprint
Given devices with notches, cutouts, or gesture home indicators Then all navigation affordances and headers are fully within platform safe areas and do not overlap system gestures Given the overlay is rendered over the camera Then it occupies no more than 15% of the vertical screen height and does not occlude the central scanning reticle Given interactive elements are displayed Then each meets a minimum 44x44 pt touch target and a minimum 3:1 contrast ratio against the background Given VoiceOver or TalkBack is active Then arrows and headers have accessible labels announcing their role and the class name and start time
Sticky Filters and Search Persist Across Rosters
Given the First-timers or Members filter is active on the current roster When I switch to another roster Then the same filters remain active and are applied to the new roster’s attendee list Given a search term is entered in the attendee search field When I switch rosters Then the search term remains and filters the new roster’s attendees accordingly Given I clear filters or the search term When I switch rosters Then the cleared state persists across subsequent roster switches Given filters or search yield no results on a roster Then an empty state message is shown and the scanner remains available
Adjacent Roster Auto-Discovery
"As an instructor in a shared space, I want the app to automatically surface the rosters before and after my current class so that I can switch to the right roster with a single gesture."
Description

Automatically assemble the switchable roster set based on time adjacency windows (e.g., within ±45 minutes), location/room, and instructor ownership, with sensible defaults and admin-configurable rules. Prioritize upcoming and just-ended sessions; handle overlapping classes and shared spaces gracefully. Expose an API to fetch ordered adjacent roster IDs for prefetch and UI. Fallback to a manual roster picker if no eligible rosters are detected.

Acceptance Criteria
Default time-window auto-discovery in same room and instructor
Given the default adjacency window is 45 minutes and the current roster runs from S to E in Room R owned by Instructor I When Snap Switch initializes for the current roster Then the system computes eligibility using [S - 45m, E + 45m] as the allowed start-time window And the returned roster set includes only rosters whose start time falls within that window, are in Room R, and are owned by Instructor I And ordering places upcoming rosters (start time >= now) first by ascending start time, followed by just-ended rosters (end time <= now) by descending end time And ineligible rosters (outside the time window, different room, or different owner) are excluded
Overlaps and shared spaces handling
Given multiple eligible rosters overlap in time or start at the same minute When assembling the roster set Then each roster appears at most once and is deduplicated by roster ID And ties on start time are resolved by ordering first by same room before other rooms in the same location, then by instructor-owned rosters before co-hosted or other-instructor rosters, then by class title ascending And if the location is configured as a shared space group, rosters in different rooms within the same location may be included when eligible; otherwise cross-room rosters are excluded
Admin-configurable discovery rules
Given an organization admin updates Snap Switch discovery settings When they set windowMinutes to a value between 15 and 120 inclusive, choose locationScope ∈ {same_room, same_location, any_location}, toggle includeSharedSpaces ON/OFF, and toggle allowCrossInstructor ON/OFF Then the settings save successfully, are audit-logged with admin ID and timestamp, and become active for new discovery requests within 60 seconds And the sensible defaults are windowMinutes=45, locationScope=same_room, includeSharedSpaces=ON, allowCrossInstructor=OFF And invalid inputs (e.g., windowMinutes outside range) are rejected with a 400 validation error explaining the constraint
Adjacent roster API returns ordered IDs
Given a staff user with permission to view the current roster When they call GET /api/v1/adjacent-rosters?currentRosterId={id}&windowMinutes={optional}&locationScope={optional}&allowCrossInstructor={optional} Then the response is 200 with JSON containing currentRosterId, orderedRosterIds (array of strings), and meta {windowMinutes, locationScope, allowCrossInstructor, generatedAt} And orderedRosterIds reflects the same eligibility and ordering rules as the client, and returns an empty array (not an error) when no eligible rosters exist And unauthorized or forbidden requests return 401 or 403 respectively, and P95 latency is ≤ 200 ms for tenants with up to 10k rosters in the relevant time window And the endpoint is idempotent, cacheable for up to 30 seconds per currentRosterId and settings, and respects tenant scoping
Fallback to manual roster picker when no eligible rosters
Given no rosters meet the discovery rules for the current roster When Snap Switch is invoked Then the manual roster picker is displayed immediately (UI render ≤ 500 ms) and the auto-discovered list is hidden And selecting a roster in the picker loads its roster context without leaving the current camera view and completes in ≤ 300 ms, and the selection is tracked in analytics as a manual switch And if discovery later yields eligible rosters during the session, the picker remains available and an auto-discovered list can be revealed without overriding the user's current selection
Time zone, day boundary, and DST correctness
Given the current roster and candidate rosters are scheduled in the location’s time zone When computing the adjacency window across midnight or a DST transition Then eligibility uses wall-clock minutes in the location’s time zone (ZonedDateTime) and includes/excludes rosters correctly And a roster that starts exactly at S - windowMinutes or exactly at E + windowMinutes is considered eligible; starts earlier or later are ineligible And test cases covering standard time, DST start, DST end, and cross-day boundaries all produce identical inclusion results as if evaluated by wall-clock time
Session-Persistent Sticky Filters
"As an instructor, I want my selected attendee filters to persist while I switch rosters so that I keep focus on the same subset without reapplying filters."
Description

Enable participant filters (e.g., first-timers, members, unpaid, waitlist) that persist across roster switches for the duration of the check-in session. Display active filter chips and counts; provide quick clear/reset. Persist state on app backgrounding for a short window (e.g., 15 minutes). Ensure consistent filtering logic client/server to avoid mismatches and reflect in roster totals.

Acceptance Criteria
Persist Filters Across Snap Switch Roster Changes
Given an active check-in session with at least one filter applied and the user is viewing Roster A via the camera When the user uses Snap Switch to move to adjacent Roster B Then the same filters remain active and are applied to Roster B and the filtered list reflects those filters Given filters are modified on any roster during the session When switching to any other roster via Snap Switch Then the modified filter set is immediately applied on arrival Given active filters When returning to a previously viewed roster within the same session Then the roster opens with the same active filters applied
Display Active Filter Chips and Accurate Totals
Given at least one filter is active Then active filter chips are visible above the roster list and display their labels Given any filter state Then the roster header displays "X of Y" where X equals the count of rendered participants after filtering and Y equals the total participants in the roster When a filter is added or removed Then X updates to match the number of rendered list items and Y remains the total roster size When switching rosters via Snap Switch with filters active Then the header "X of Y" updates to reflect the new roster's totals and filtered count with no mismatch
One-Tap Clear Filters
Given at least one filter is active When the user taps Clear Filters Then all active filters are removed, all filter chips disappear, and the roster list shows all participants Given filters are cleared on one roster When the user switches to another roster in the same session Then no filters are active on arrival Given no filters are active When the user taps Clear Filters Then no UI or data changes occur
Background Resume Within 15 Minutes Preserves Filters
Given at least one filter is active When the app is backgrounded and resumed within 15 minutes Then the same filters remain active and applied to the current roster Given the app is backgrounded with active filters When the app is resumed after more than 15 minutes or after a cold start Then all filters are reset and the roster shows all participants Given a resume within 15 minutes followed by a Snap Switch Then the preserved filters are applied on the next roster
Client–Server Filtering Consistency
Given a filter set is active When the client requests roster data Then the request includes the active filter parameters and the server returns only matching participants Then the number of participants rendered equals the server-reported filteredCount, and the header "X of Y" matches server totals for that roster When adding or removing filters Then the client and server remain in sync within a single refresh, with no discrepancies between list items, chip state, and totals
Empty-Result State With Filters
Given filters are applied that produce zero matches for a roster Then the roster displays an empty state message and a prominent Clear Filters action Then the header displays "0 of Y" where Y equals the total roster size, and no participant items are rendered When the user taps Clear Filters from the empty state Then all participants for that roster are shown and the header becomes "Y of Y"
Prominent Class Header and Cross-Class Guardrails
"As an instructor, I want a clear class header and prompts that prevent cross-class check-ins so that I avoid accidentally checking someone into the wrong session."
Description

Render a fixed header inside the camera overlay showing class name, start/end time, location, and instructor for the active roster. When a scan or manual action targets a participant not on the active roster but present on an adjacent roster, prompt to switch before confirming check-in. Provide clear visual warnings, allow override based on permissions, and log prevented cross-class errors. Ensure headers update instantly on roster switch.

Acceptance Criteria
Fixed Class Header Content and Placement
Given the camera overlay is open on an active roster When the header renders Then it displays class name, start time, end time, location, and instructor pulled from the active roster And it remains fixed at the top during scanning and manual actions and does not overlap scan controls And header text meets a minimum contrast ratio of 4.5:1 and uses device locale for time formatting
Instant Header Update on Roster Switch
Given the user switches to an adjacent roster using Snap Switch When the active roster changes Then the header updates all fields (class, times, location, instructor) within 200 ms And the next scan or manual check-in targets the new active roster only And no stale header information appears after the switch
Cross-Class Detection and Switch Prompt
Given a participant is scanned or selected manually And the participant is not on the active roster but is on an adjacent roster When the system identifies the roster mismatch Then it blocks immediate check-in and shows a prompt to switch to the participant’s roster, with options: Switch and Cancel And check-in cannot complete until a switch occurs or an authorized override is used And if multiple adjacent rosters match, a selection list is displayed to choose the correct roster
Permission-Based Override Without Switching
Given the cross-class prompt is displayed And the user has CrossClassOverride permission When the user selects Override Then the participant is checked into the matched adjacent roster without switching the current view And a confirmation step is required before completion And users without permission do not see or cannot activate the Override action
Visual Warning and Accessibility Cues
Given cross-class detection is triggered When the warning UI is presented Then it visually differentiates the active roster and matched roster names and times with an alert banner and icon And it provides non-color cues (text and icon) and haptic feedback where supported And all warning text and controls are accessible via screen readers with descriptive labels And warning elements meet a minimum 4.5:1 contrast ratio
Cross-Class Incident Logging and Auditability
Given a cross-class event occurs (blocked, switched, or overridden) When the event is resolved Then the system logs timestamp (UTC), user ID, device ID, participant ID, active roster ID, matched roster ID, action type (blocked|switched|overridden), and outcome (success|canceled) And the log entry is persisted and visible in admin analytics or export within 5 minutes And failed logging surfaces a non-blocking error metric for monitoring
Roster Prefetch and Low-Latency Switching
"As an instructor during rush check-ins, I want roster switches to feel instantaneous so that I maintain flow and reduce lines."
Description

Prefetch next/previous rosters’ attendee lists, statuses, and essential assets to achieve sub-100ms perceived switch time on modern devices. Use memory-aware caching and eviction, and apply incremental updates via existing real-time channels or short polling. Degrade gracefully on poor networks with minimal loading indicators that do not block scanning.

Acceptance Criteria
Sub-100ms Adjacent Roster Switch on Modern Devices
Given I am in Snap Switch camera on Roster A with network quality Good (downlink ≥ 10 Mbps, RTT ≤ 100 ms) on a modern device (2019+ mid-tier or better) When I swipe to the next or previous roster that has been prefetched Then the target roster header and first screenful of the attendee list (≥10 rows) render within 100 ms of gesture release And the correct class header is clearly visible before any check-in action can be taken And the camera preview maintains ≥24 fps during and after the switch
Memory-Aware Caching and Eviction
Given the app maintains a prefetch cache with a budget of 20 MB by default (configurable) When the total size of cached rosters exceeds the budget or OS memory pressure is signaled Then the least-recently-used prefetched roster is evicted within 50 ms And the current roster and the immediately adjacent next/previous rosters remain cached And eviction does not drop camera preview below 24 fps or block scanning actions
Incremental Updates to Prefetched Rosters
Given the real-time channel is connected When an attendee’s status or roster membership changes on the server for a prefetched roster Then the prefetched roster reflects the change within 3 seconds without a full reload Given the real-time channel is unavailable and short polling is active When server-side changes occur for a prefetched roster Then the prefetched roster reflects the change within 15 seconds without a full reload And any conflicting local check-in edits are merged using last-writer-wins with user-visible resolution if needed
Graceful Degradation on Poor or Offline Networks
Given network quality is Poor (downlink < 1 Mbps or RTT > 400 ms) or the device is temporarily offline When I swipe to switch rosters Then a lightweight skeleton state appears within 150 ms without blocking the camera And the camera preview maintains ≥20 fps and scanning remains available And attendee data and assets progressively render as they arrive And no full-screen blocking spinners are shown
Sticky Filters Persist and Apply Before Render
Given I have applied a sticky filter (e.g., First-timers, Members) on Roster A When I switch to an adjacent roster Then the same filter is applied to the target roster before the attendee list is displayed And the active filter chip is visible on the target roster And filtered counts match server truth after sync within 5 seconds And clearing the filter on any roster clears it for subsequent switches in the same session
Fallback to Fresh Fetch on Stale Prefetch
Given a prefetched roster is older than 2 minutes or failed hydration When I switch to that roster Then the UI renders immediately from the stale cache with a subtle Refreshing label and starts a background refresh And on a Good network the fresh data replaces stale within 5 seconds And if refresh fails, the roster remains usable, a Retry action is available, and the failure is logged once per session
Switch Performance and Reliability Telemetry
Given telemetry is enabled When users perform roster switches Then p95 perceived switch render time on modern devices and Good networks is ≤100 ms over a rolling 7-day window And p50 perceived switch render time is ≤60 ms And the switch error rate (failed render or crash) is <0.5% And metrics are tagged by device class and network class for analysis
Usage Analytics and Admin Controls
"As an owner, I want analytics on Snap Switch usage and settings to configure adjacency rules so that I can measure impact and tailor the experience to my studio."
Description

Capture metrics on switches per session, time-in-camera, prevented wrong-class check-ins, and filter usage to quantify impact. Provide admin toggles to enable/disable Snap Switch per organization, set adjacency windows, and define default filters. Surface insights on the dashboard to inform operational tweaks and ROI tracking.

Acceptance Criteria
Session Analytics: Switches & Time-in-Camera
Given an instructor is in a live session with Snap Switch enabled, When they switch between rosters N times, Then the system records N switch events with timestamps and increments session.switch_count by N. Given the camera view is open in the foreground, When it remains active for T seconds, Then session.time_in_camera increases by T with ±1s accuracy and pauses when the app is backgrounded or the camera view is closed. Given network connectivity is intermittent, When events are generated offline, Then they are queued and persisted within 60 seconds of reconnect without duplication. Given the session ends, When analytics are queried, Then switch_count and time_in_camera are available via API and dashboard within 5 minutes.
Track Prevented Wrong-Class Check-ins
Given an instructor attempts to check in a student not on the active class roster, When the check-in is blocked by class headers or sticky filters, Then a prevented_checkin event is recorded with active_class_id, attempted_class_id, and timestamp, and session.prevented_wrong_class_count increments by 1. Given valid same-class check-ins occur, When analytics are processed, Then no prevented_checkin events are recorded for valid check-ins (0 false positives in test cases). Given offline conditions, When prevented events occur, Then they are queued and persisted within 60 seconds of reconnect. Given the dashboard is refreshed, When within 5 minutes of session end, Then prevented_wrong_class_count is visible at session and aggregate levels.
Capture Filter Usage Metrics
Given an instructor applies, changes, or clears a filter (e.g., first-timers, members), When the action occurs, Then a filter_event is logged with filter_key, action_type (applied|changed|cleared|applied_default), and timestamp. Given a session completes, When analytics are available, Then total filter_changes and unique filters used are reported per session and aggregated per date range. Given default filters auto-apply, When the camera opens, Then an applied_default event is logged once per session. Given network interruptions, When filter events occur, Then they are queued and persisted within 60 seconds without duplication.
Org-Level Enable/Disable Snap Switch
Given an org admin sets Snap Switch = Disabled, When an instructor opens the camera, Then the Snap Switch UI and swipe gesture are not available across all org users within 60 seconds or next app refresh, and a notice indicates it is disabled by admin. Given an org admin sets Snap Switch = Enabled, When instructors refresh, Then the Snap Switch controls become available. Given any toggle change, When saved, Then an audit log entry is created with admin_id, timestamp, and previous/new values. Given config is cached on devices, When a toggle change occurs, Then the device config updates on next sync or within 60 seconds, whichever comes first.
Configure Adjacency Windows
Given an org admin sets adjacency_window_before = X minutes and adjacency_window_after = Y minutes (allowed range 0–60), When classes are scheduled, Then Snap Switch only offers rosters whose start/end times fall within the configured windows relative to the active class. Given an invalid value is entered (e.g., <0, >60, non-integer), When saving, Then validation prevents save and displays an error message. Given a valid change is saved, When instructors use the app, Then the new windows apply within 60 seconds or next sync and respect the location’s timezone. Given no custom values are set, When determining adjacency, Then system defaults are applied.
Set and Apply Default Filters
Given an org admin defines default filters for Snap Switch, When an instructor opens the camera for a session, Then those defaults auto-apply and are visibly indicated, and the instructor may override them during the session. Given a session ends or a new session begins, When the camera is reopened, Then admin-defined defaults re-apply. Given no admin defaults exist, When the camera opens, Then system defaults apply. Given an admin updates default filters, When saved, Then changes are audit-logged and propagate to devices within 60 seconds or next sync.
Dashboard Insights & ROI Tracking
Given an admin opens the dashboard, When selecting a date range and grouping (daily/weekly/monthly), Then the dashboard displays: average switches per session, total/average time-in-camera, total prevented wrong-class check-ins, and filter usage counts. Given large data volumes (≤10k sessions in range), When loading the view, Then metrics render within 3 seconds at the 90th percentile. Given new events are ingested, When viewing metrics, Then data freshness is ≤5 minutes end-to-end. Given an admin needs detail, When drilling down, Then session-level metrics are visible, and the data can be exported as CSV for the selected range.

Doorflow Insights

After each session, see arrival curves, punctuality rates, grace expiries, and waitlist conversions. Get actionable suggestions—adjust reminder timing, open doors earlier, or tweak grace windows—to reduce late arrivals and keep classes running on time.

Requirements

Arrival Event Capture & Sync
"As an instructor, I want arrivals automatically recorded at the door so that I can see who came and when without manual logging."
Description

Implement reliable capture of attendee arrival events tied to each scheduled session, supporting staff check-in, self-check via QR, and manual overrides. Persist first-arrival timestamps per booking, deduplicate multiple scans, and map events to the correct session, attendee, and location. Handle offline mode with queued sync, reconcile device clock drift, and store source metadata. Enforce role-based permissions for recording and viewing arrivals, and expose a normalized event stream for analytics.

Acceptance Criteria
RBAC-Controlled Staff Check-In
Given a staff user with permission CheckIn:Record for location L and session S When they mark booking B as arrived for session S at location L Then an arrival event is created with {bookingId=B, sessionId=S, locationId=L, attendeeId of B, sourceType=staff, actorUserId=staffUserId, deviceId, normalizedTs, rawDeviceTs} And the booking’s firstArrivalTs is set to normalizedTs if it is not already set And the API responds 201 Created Given a user without CheckIn:Record for L or S When they attempt to mark booking B as arrived Then the action is rejected with 403 Forbidden And no arrival event or timestamp is created Given a user with CheckIn:View only When they request arrivals for session S Then the list of arrivals is returned And any attempt by that user to create or modify arrivals is rejected with 403 Forbidden
QR Self-Check Dedup and First-Arrival Persistence
Given an attendee with a valid booking B for session S and a scannable QR that encodes bookingId B and sessionId S When the attendee scans the QR at the venue Then an arrival event is recorded with sourceType=qr and deviceId of the scanning device And the booking’s firstArrivalTs is set to this event’s normalizedTs if not already set Given booking B already has a firstArrivalTs When the QR is scanned again from the same or a different device Then no new arrival event is created (idempotent) And the existing firstArrivalTs remains unchanged And the API/app returns a success message indicating the attendee is already checked in Given a QR that does not match an existing active booking for session S When scanned Then the action is rejected with 404/422 and no arrival event is recorded
Manual Override with Audit Trail
Given a user with permission CheckIn:Override for session S When they set booking B’s arrival time to Toverride Then booking B’s firstArrivalTs is updated to Toverride (normalized) And an audit record is created capturing {bookingId, previousFirstArrivalTs, newFirstArrivalTs=Toverride, actorUserId, reason, occurredAt} And the arrival event stream includes a new event with sourceType=override linked to booking B Given a user without CheckIn:Override When they attempt to change firstArrivalTs for booking B Then the attempt is rejected with 403 Forbidden and no changes are persisted Given Toverride is later than now (server time) When the override is submitted Then the request is rejected with 422 Unprocessable Entity and the prior firstArrivalTs remains intact
Accurate Mapping to Session, Attendee, and Location
Given an attendee holds booking B for session S at location L and there are overlapping sessions at L and other locations When a check-in event is initiated via staff app or QR Then the system maps the event to sessionId=S, locationId=L, attendeeId from booking B using bookingId embedded in the action And if the location implied by the device/geofence/QR does not match L, the check-in is rejected with 409 Conflict Given a person without a valid booking for session S When they attempt to check in Then the system rejects the attempt with 404/422 and no event is stored Given multiple concurrent sessions and a booking mistakenly referencing a different session When a check-in occurs Then the system prevents cross-session mapping and returns a descriptive error without persisting an event
Offline Capture, Queueing, and Sync Deduplication
Given a device is offline When a staff check-in or QR scan occurs for booking B Then the event is stored locally with rawDeviceTs, deviceId, sourceType, and a pending sync status And the UI confirms the local capture Given the device reconnects When queued events are synced Then the server assigns definitive eventIds, computes normalizedTs, and marks local items as synced And if an arrival already exists for booking B, the server deduplicates and preserves the earliest firstArrivalTs across all sources And the client reflects the final synced status and any deduplication outcome Given a queued event cannot be reconciled (e.g., invalid booking) When sync runs Then the item is marked failed with an error code and is not persisted on the server
Device Clock Drift Detection and Timestamp Normalization
Given a device’s clock differs from server time by an offset Δ determined during sync/handshake When the device records an arrival with rawDeviceTs Then the server computes normalizedTs = rawDeviceTs − Δ and persists both values And the event metadata stores the applied offset and method used to derive it Given no prior offset is known When the first event from a device is received Then the server establishes Δ during the request/response and applies it to normalize timestamps Given Δ exceeds a configured maximum tolerance When events are received Then the system flags the events with a driftWarning=true field for analytics while still normalizing and persisting them
Normalized Arrival Events Stream for Analytics
Given a user with permission Analytics:Read When they request GET /v1/analytics/arrival-events with filters (date range, sessionId, locationId, sourceType) and pagination params Then the API returns 200 with a paginated, chronologically ordered list by normalizedTs asc And each item includes {eventId, bookingId, sessionId, attendeeId, locationId, normalizedTs, rawDeviceTs, sourceType, actorUserId (nullable), deviceId, isDeduplicated (boolean), createdAt, updatedAt} And results are limited to the user’s authorized locations Given invalid filters or unauthorized access When the request is made Then the API returns 400/403 with no data leakage Given the same query is repeated When the request is made Then results are consistent and stable in ordering by normalizedTs and eventId
Arrival Curves & Punctuality Metrics
"As a studio owner, I want to see when attendees typically arrive relative to class start so that I can plan staffing and door opening times."
Description

Transform arrival events into post-session analytics, including minute-by-minute arrival curves relative to start time, on-time/late rates using configurable thresholds, median and percentile arrival offsets, and trend comparisons by class, time of day, and instructor. Provide per-session and aggregate views, smoothing for small samples, and APIs to retrieve metrics. Update metrics immediately after session end and flag low-sample reliability.

Acceptance Criteria
Post-Session Arrival Curve Generation and Timeliness
Given a session has a scheduled start and end time and at least one arrival event When the session reaches its scheduled end time Then the system generates a minute-by-minute arrival curve relative to the start time covering at minimum -30 to +15 minutes and extending to earliest/latest arrival if outside this range And the curve reports integer minute bins with counts derived from unique attendee-session arrivals (deduplicated) And the curve uses the session location’s timezone And per-session arrival curve, on-time/late rates, median, and percentiles are available within 2 minutes of session end
Configurable On-Time/Late Threshold Calculation
Given an on-time threshold T (in minutes after scheduled start) is configured at org, class, or session level with session-level override highest precedence And a session has arrival offsets in minutes relative to start When metrics are computed with T=5 Then on-time count equals number of arrivals with offset <= 5 and late count equals total arrivals - on-time And on-time and late rates are percentages rounded to one decimal place And updating T to a new value triggers recalculation and UI/API reflect changes within 1 minute
Median and Percentile Arrival Offsets Computation
Given a session has N arrivals with minute offsets relative to start When N >= 5 Then median (p50), p10, and p90 offsets are computed as integer minutes using nearest-even rounding for .5 values And negative values indicate early arrivals and positive values indicate late arrivals When N < 5 Then percentile values are returned as null and a low-sample indicator is set for the session
Per-Session vs Aggregate Views in UI
Given a user opens Doorflow Insights for a class occurrence When the user selects View = Session Then metrics shown are specific to that occurrence only (arrival curve, on-time/late rates, median, percentiles) with no smoothing applied When the user selects View = Aggregate with Date Range = last 30 days and Scope = Class Series Then metrics shown aggregate all matching occurrences in range using the same definitions and display sample sizes for each metric And the last selected view and scope persist across reload for that user
Trend Comparisons by Class, Time of Day, and Instructor
Given a date range and comparison dimension are selected (Class, Time of Day, or Instructor) When trends are requested Then the system returns time series for on-time rate and median arrival offset per dimension bucket with weekly cadence And each data point includes the sample size (arrivals) contributing to that point And buckets with zero sessions in a week are omitted and gaps are rendered as breaks
Small-Sample Smoothing Behavior
Given aggregate trend series are computed When a weekly data point has fewer than 10 sessions contributing Then the displayed value uses a 3-point centered moving average smoothing (previous, current, next) where neighbors exist; endpoints use available neighbors only And per-session views never apply smoothing And a tooltip/footnote indicates when smoothing is applied
Metrics API Endpoints and Contracts
Given an authenticated client requests GET /api/v1/metrics/sessions/{session_id}/arrivals Then the response status is 200 with JSON containing: session_id, generated_at (ISO8601), sample_size, arrival_curve:[{minute_offset:int,count:int}], on_time_rate:float, late_rate:float, median_offset:int, p10:int|null, p90:int|null, reliability:{level:"low"|"normal",reason?:string} Given an authenticated client requests GET /api/v1/metrics/aggregates?scope={class|instructor|time_of_day}&id=...&start_date=...&end_date=...&tz=... Then the response status is 200 with JSON containing: scope, id, date_range, timezone, series:[{period_start:date,period_end:date,sample_size:int,on_time_rate:float,median_offset:int,smoothed:boolean,reliability:{level:"low"|"normal"}}] And p95 latency <= 500 ms for cached requests and <= 2 s for uncached over last 24h And requests with invalid params return 400 with error details; unauthorized returns 401
Grace Window Tracking & Expiry Outcomes
"As a class host, I want to quantify how often grace periods expire and what happens next so that I can tune the grace duration."
Description

Support configurable grace windows per class or template and record when each booking’s grace period expires without arrival. Attribute subsequent actions (spot released, fee applied, or mark as no-show) and quantify impact via metrics such as grace expiry rate, average late minutes, and openings created by expiry. Surface comparisons across classes and recommend candidate grace durations based on observed behavior.

Acceptance Criteria
Configure Grace Window at Template and Class Levels
Given a class template with default grace window of 10 minutes When a new class is created from this template Then the class inherits a 10-minute grace window Given a class with an explicit grace window override of 5 minutes When the template default is later changed to 8 minutes Then the class continues to use 5 minutes Given a user with edit permissions When they set a grace window value Then the UI and API accept values from 0 to 60 minutes inclusive in 1-minute increments and reject others with a validation error Given a grace window change is saved Then an audit entry records who, when, old value, new value, and scope (template or class) Given a class has no explicit override When the template default is updated Then all future instances of that class reflect the new default and past sessions remain unchanged
Record Per-Booking Grace Expiry and Arrival State
Given a class with grace window G and start time S When a booking is confirmed Then the system computes and stores the booking’s grace expiry timestamp E = S + G minutes Given an attendee checks in at time T <= E Then the booking status is Arrived and no grace expiry event is recorded Given an attendee has not arrived by time E Then the system records a GraceExpired event with timestamp E for that booking Given an attendee arrives at time T > S Then the system stores late_minutes = T - S (in whole minutes, rounded down) for reporting Given connectivity issues When check-in events are received late Then the system reconciles based on the actual check-in timestamp and updates late_minutes and expiry event presence accordingly, without duplicating events
Trigger and Attribute Actions on Grace Expiry
Given a booking reaches grace expiry with policy ReleaseSpot enabled and the class has a waitlist When the expiry event is recorded Then the spot is released, an opening is created, and the first eligible waitlist member is auto-offered the spot; all actions are attributed to the expiry event Given a booking reaches grace expiry with policy ApplyFee enabled and a valid payment method on file When the expiry event is recorded Then the configured fee is charged and a receipt is stored; on failure, the action is marked Failed with reason Given a booking reaches grace expiry with policy MarkNoShow enabled When the expiry event is recorded Then the booking status updates to No-Show and is attributed to expiry Given multiple policies are enabled When expiry occurs Then actions execute in the order: ReleaseSpot, ApplyFee, MarkNoShow, and each action logs timestamp, outcome, and linkage to the booking and class
Compute and Display Grace-Related Metrics
Given a selected date range and class/template filter When Doorflow Insights is opened Then the dashboard displays: grace expiry rate = (bookings with GraceExpired) / (total bookings) as a percentage; average late minutes = average late_minutes for arrived bookings; openings created by expiry = count of openings caused by expiry-triggered releases Given new arrivals or expiry events occur When 2 minutes have passed Then the metrics reflect the latest data Given the metrics are displayed Then hovering or tapping the metric shows the underlying counts and calculation formula Given a user requests export When CSV export is triggered Then a file is generated containing per-session and per-booking fields used to derive the metrics
Cross-Class Comparison and Filtering
Given classes across instructors and templates When the user opens the Comparison view Then a table lists each class with columns: total bookings, grace expiry rate, average late minutes, and openings created by expiry Given the table is visible When the user applies filters (instructor, template, location, day of week, time of day) Then the list updates to include only matching classes Given the table is visible When the user sorts by any metric column Then the rows reorder accordingly Given a class row is selected When the user drills down Then the session-level breakdown and booking details are shown
Grace Duration Recommendations
Given a class/template with at least 30 past sessions and 200 total bookings When the user opens Recommendations Then the system proposes 1–3 candidate grace durations with estimated impact on late arrivals and expiry rate, each with a confidence level Given insufficient data (<10 sessions or <50 bookings) When Recommendations is opened Then the system displays “Insufficient data” and no recommendation is shown Given a recommendation is accepted by a user with edit permissions When Apply is clicked Then the class/template grace window updates to the recommended value and an audit entry with recommendation metadata is recorded Given recommendations are shown When the user taps “Why?” Then an explanation summarizes the observed arrival distribution and the trade-off considered
Historical Integrity and Timezone Handling
Given a grace window value is changed for a template or class Then the change applies only to future sessions; historical sessions retain their original grace window and recorded events unchanged Given sessions are scheduled across time zones When times are displayed Then all timestamps are stored in UTC and presented in the class’s local time zone, accounting for daylight saving transitions Given a session spans a DST change When grace expiry is computed Then E = local start time + grace minutes uses the correct offset so the computed expiry matches the local wall clock Given a booking lacks historical grace configuration (legacy data) When metrics are computed Then the booking is excluded from grace-based metrics and flagged as Legacy in exports
Waitlist Offer Conversion Analytics
"As an operator, I want to know which waitlist offers convert and how quickly so that I can adjust offer windows and channels."
Description

Attribute openings to their source (cancellation vs. grace expiry) and track the lifecycle of auto-offers: send time, channel, acceptance/decline, time-to-accept, and resulting attendance and revenue. Provide conversion rates by class, time, and channel, plus optimal offer window insights. Enable drill-down to individual offers and aggregate cohort views to identify bottlenecks and improve fill rates.

Acceptance Criteria
Opening Source Attribution (Cancellation vs Grace Expiry)
Given a class opening created by attendee cancellation, When an auto-offer is generated, Then the opening source is recorded as "cancellation" and surfaced in analytics for that offer. Given a class opening created by grace window expiry, When an auto-offer is generated, Then the opening source is recorded as "grace_expiry" and surfaced in analytics for that offer. Given any reporting date range and filters, When viewing aggregated offers by opening source, Then 100% of offers are attributed to exactly one source and totals equal the filtered offer count. Given a data export or API pull, When retrieving offers, Then each record includes opening_source with allowed values {"cancellation","grace_expiry"}.
Offer Lifecycle Tracking (Send, Channel, Accept/Decline, Time-to-Accept)
Given an auto-offer is sent via SMS and/or email, When the send occurs, Then the system records a send event with UTC timestamp and channel(s). Given a recipient accepts or declines an offer, When the action is captured, Then the system records the decision with UTC timestamp and computes time_to_accept in whole minutes from the earliest send timestamp. Given multiple send attempts across channels, When an acceptance occurs, Then channel_of_acceptance is recorded and time_to_accept is based on the first send across any channel. Given an offer reaches its expiry without response, When the expiry time passes, Then the offer status is set to "expired" and time_to_accept is null. Given lifecycle events are displayed, When viewing an offer, Then events appear in chronological order and are immutable audit entries.
Conversion Rates by Class, Time, and Channel
Given a selected date range and filters, When viewing conversion metrics, Then the UI shows for each dimension (class, start-time bucket, channel): acceptance_rate = accepted_offers / total_offers_sent and attendance_conversion = attended_from_accepted / accepted_offers, with denominators displayed on hover. Given any segment with fewer than 30 offers, When rendering rates, Then the metric displays "—" with tooltip "insufficient sample (<30 offers)" and is excluded from cohort averages. Given segment rows are summed, When comparing totals, Then total_offers_sent equals the sum of segment totals and rates are rounded to one decimal place without exceeding 100%. Given channel filter is applied (e.g., SMS), When viewing metrics, Then totals and rates reflect only offers sent via the selected channel(s).
Optimal Offer Window Insight
Given a cohort with at least 100 historical offers, When computing the optimal offer window, Then the system recommends an expiry window (minutes after send) that captures ≥90% of historical acceptances before class start for that cohort and displays a confidence badge (High ≥500, Medium 100–499, Low <100). Given the recommended window exceeds remaining time to class start for a session, When displaying, Then the recommendation is capped at the time before class and labeled "capped by class start". Given filters or date range change, When recomputing the recommendation, Then the result updates within 5 seconds and shows the last computed timestamp. Given a cohort has fewer than 100 offers, When requesting a recommendation, Then the panel displays "insufficient data" and no value is recommended.
Drill-Down: Individual Offer Detail
Given a user clicks a segment, chart point, or row, When navigating to details, Then the offer detail view shows: offer_id, class_id/name/start_time, opening_source, send_timestamp(s) with channel(s), acceptance/decline with timestamp, expiry timestamp, final status (accepted|declined|expired), time_to_accept (minutes), attendance (present|absent), and revenue_for_booking (currency). Given multiple send attempts or retries, When viewing the detail, Then all attempts are listed with delivery status (delivered|failed|unknown) and provider message ID when available. Given an offer has no acceptance, When viewing the detail, Then time_to_accept is null and status reflects expired or declined appropriately. Given permissions are standard studio admin, When accessing details, Then the page loads within 3 seconds for 95th percentile of requests over the selected date range.
Cohort Aggregations and Bottleneck Funnel
Given filters for class, instructor, location, day of week, channel, and date range, When applied, Then the cohort view aggregates and displays funnel counts: offers_sent → delivered → accepted → booked → attended for the selected grain (e.g., class, instructor). Given any funnel step has a drop-off ≥40% relative to the previous step within a cohort, When rendering, Then the step is highlighted as a bottleneck and a suggestion chip is displayed (e.g., "Increase SMS share" if SMS conversion > email by ≥15pp). Given the user changes aggregation grain (e.g., from class to start-time bucket), When recomputing, Then the funnel and rates refresh within 3 seconds and reflect the new grouping. Given the date range is updated, When reloading data, Then totals and bottleneck identification recalculate to match the new range.
Attendance and Revenue Attribution from Offers
Given an accepted offer creates a booking and the attendee is marked present, When the class session completes, Then attendance is attributed to the originating offer_id and counted in attendance_conversion. Given a booking tied to an accepted offer has payments totaling X and refunds totaling Y within the reporting window, When computing revenue from offers, Then revenue_attributed = X − Y and is included once in cohort and total revenue. Given a booking uses a prepaid pass or comp with no incremental charge, When computing revenue, Then revenue_attributed = 0 while the acceptance and attendance metrics are still counted. Given a booking is canceled and fully refunded before class start, When reporting, Then attendance = false and revenue_attributed = 0 for that offer.
Actionable Suggestions Engine
"As an instructor, I want clear suggestions with one-click apply so that I can reduce late arrivals without deep analysis."
Description

Generate data-driven recommendations after each session to reduce late arrivals and improve on-time starts. Use rules and heuristics to propose changes such as shifting reminder timing, opening doors earlier, or adjusting grace windows, with estimated impact and confidence. Provide one-click application to class templates and policies, A/B test mode for safe rollout, and explainability that cites the underlying metrics driving each suggestion. Track outcomes to continuously refine suggestions.

Acceptance Criteria
Post-Session Suggestion Generation with Impact and Confidence
Given a session has ended and attendance/arrival data is ingested When Doorflow Insights processing completes within 5 minutes of session end Then the engine evaluates the last 30 days or last 8 matching sessions (whichever yields at least 100 attendee records) and generates 0-5 actionable suggestions And each suggestion includes: recommendation type (reminder timing, door open time, grace window), proposed change value with units, estimated impact as percent change to on-time starts or late arrivals with 90% CI, confidence score (0-1) and bucket (Low/Med/High), and default scope And suggestions below minimum effect size (>=5% improvement) or confidence (<0.6) are not shown And if no suggestions qualify, a No suggestions card is shown with the top reason (insufficient data, no projected benefit, conflicting signals)
Suggestion Explainability and Metrics Traceability
Given a suggestion is displayed Then its details show Why information citing the exact metric values that triggered it: punctuality rate, arrival curve percentile(s), grace expiry rate, waitlist conversion rate, data window (from-to dates), and sample size (sessions and attendees) And it links to the underlying charts, pre-filtered to the cohort used (class template, location, time-of-day) And the rule or heuristic ID and threshold(s) that fired are visible to admins And all times and dates reflect the class location timezone and correctly account for daylight saving changes
One-Click Apply with Scope, Preview, and Rollback
Given a user with Manager role views a suggestion When they click Apply and choose a scope (This class only, This class template, All classes under Policy X) Then a confirmation modal shows before/after values, count of impacted future sessions, effective date/time, and estimated impact at the chosen scope When the user confirms Then the change is applied within 2 minutes, a success notice is shown, and an audit log entry records user, timestamp, scope, old/new values, and suggestion ID And an Undo option is available for 24 hours to revert the change entirely, creating a second audit entry And users without sufficient permission cannot apply changes and see an explanatory message without making changes
A/B Test Mode for Safe Rollout
Given a suggestion is eligible for experimentation When a user enables A/B Test and selects a split between 10/90 and 90/10 and a stopping rule (minimum 5 sessions and 100 attendees per arm or a fixed duration) Then upcoming sessions for the relevant class template are randomized into Control and Treatment arms accordingly And the system tracks KPIs per arm: primary (on-time start rate) and secondary (late arrivals %, grace expiries %, waitlist conversions %) When the stopping rule is met Then results show lift %, p-value, 90% CI, and a recommendation (Promote, Keep Testing, or Stop) based on p<0.05 and observed lift >=5% And the user can Promote to 100% rollout or Stop to revert to Control, with immediate application/rollback and audit logging
Outcome Tracking and Continuous Refinement
Given a suggestion has been applied (via Apply or Promote) Then the engine stores a baseline from the previous 8 matching sessions and compares against the next 8 sessions post-change And it displays observed impact with 90% CI, noting directionality vs predicted impact and tagging the outcome as Improved, No Change, or Regressed And heuristic weights are adjusted to favor rules with repeated observed improvements and de-emphasize rules that underperform And any suggestion that regresses KPIs by >3% or fails to achieve significance after 8 sessions is marked Failed and is not re-suggested for the same cohort for 30 days
UI Delivery and Notifications
Given a session ends When processing completes Then suggestions appear in the Doorflow Insights session summary and the Suggestions tab within 5 minutes And instructors can filter suggestions by type, class template, and location; dismiss or snooze a suggestion; and opt into a daily email digest of new suggestions And opening a suggestion shows details with Apply and A/B Test actions available if the user has permission
Insights Dashboard & Session Summary
"As a busy instructor, I want a concise post-class summary on my phone so that I can make quick improvements."
Description

Deliver a mobile-first dashboard and post-session summary showing arrival curves, on-time percentage, late counts, grace expiries, and waitlist conversions alongside recommended actions. Include filters for timeframe, class, instructor, and location, plus drill-down to session details. Support CSV export and scheduled weekly email summaries. Ensure fast load times, accessibility, and role-based access within the ClassNest admin and booking workflows.

Acceptance Criteria
Mobile Dashboard KPIs and Post-Session Summary
Given a completed session with recorded check-in timestamps and a configured grace window in the venue timezone When the user opens Doorflow Insights on a mobile viewport ≤ 414px width Then the dashboard displays an arrival curve (line chart) from T-30 to T+30 minutes relative to class start in 1-minute bins and in venue timezone And shows KPIs: On-time %, Late Count, Grace Expiries, Waitlist Conversions, and Waitlist Conversion Rate for the selected scope And metrics are defined as: On-time % = count(check_in_time ≤ start_time + grace_window) ÷ count(checked_in) × 100 (rounded to 1 decimal); Late Count = count(check_in_time > start_time + grace_window); Grace Expiries = count(auto-canceled due to grace expiry); Waitlist Conversions = count(waitlist_offer_accepted AND attended); Conversion Rate = conversions ÷ count(waitlist_offers_sent) × 100 (1 decimal) And values refresh within 5 minutes of session end or filter change And number values use thousands separators; times show with timezone abbreviation
Actionable Recommendations After Session
Given post-session metrics are computed for a session When thresholds are met Then up to 3 recommendations are displayed with title, rationale including the observed metric and threshold, and a deep link to the relevant setting And each recommendation supports Dismiss (persists for that session) and View Settings actions And examples include: if Late Rate > 20% and only one reminder ≥ 60 min pre-start is configured, suggest adding a 15-min reminder; if Grace Expiry Rate > 5%, suggest extending grace window by +5 minutes; if arrivals peak after start_time, suggest opening doors earlier by 5–10 minutes And recommendations are generated within 2 minutes after session end and logged with user actions (view, dismiss, link click)
Filtering by Timeframe, Class, Instructor, and Location
Given the user opens the Insights dashboard When the user applies filters for Timeframe (Last 7/30/90 days, Custom), Class (multi-select), Instructor (multi-select), and Location (multi-select) Then results update to reflect filters where selections within the same dimension are ORed and across dimensions are ANDed And the default is Timeframe = Last 7 days, others = All And selected filters persist for the session and are encoded in the URL as query parameters so that reload or share restores the same state And applying or clearing a filter provides visual loading feedback and the UI updates within 300 ms after data returns
Drill-Down to Session Details
Given a user is viewing aggregated metrics or charts When the user taps a session data point, card, or list row Then a Session Insights Summary opens showing: arrival timeline (1-minute bins), on-time %, late count, grace expiries, waitlist offers sent/accepted/expired with timestamps, and attendee list with check-in times And the page includes a shareable URL containing session_id And using Back returns the user to the previous dashboard state (filters and scroll position preserved)
CSV Export of Insights
Given the user has the Insights.Export permission and filters are applied When the user selects Export CSV Then a UTF-8 CSV downloads containing one row per session in scope with columns: session_id, class_name, instructor_name, location_name, start_time_iso, on_time_pct, late_count, grace_expiries, waitlist_offers_sent, waitlist_conversions, waitlist_conversion_rate And values respect the selected timezone; percentages have 1 decimal; integers have no decimals And the filename is classnest_insights_{YYYY-MM-DD}_{TZ}.csv And for ≤ 10,000 sessions the file is generated and the download begins within 10 seconds
Scheduled Weekly Email Summaries
Given a user with Insights.ScheduleEmails permission configures a schedule with day of week, send time, timezone, recipients, and saved filters When the scheduled time occurs Then recipients with valid roles receive an email within ±10 minutes containing KPIs for the prior week vs previous week (trend arrows) and the top 3 recommendations And links in the email deep-link to the dashboard with the saved filters applied And recipients can unsubscribe via a link; unsubscribed users are not sent subsequent summaries And delivery failures (bounces) are logged and surfaced to the scheduler
Performance, Accessibility, and Role-Based Access Compliance
Given the production dataset includes up to 5,000 sessions over the last 90 days When loading the Insights dashboard on a 4G mobile connection Then initial render completes with p75 < 1.0s and p95 < 2.5s, and subsequent filter changes render within p95 < 1.0s after API response (API p95 < 800 ms) And charts and KPIs meet WCAG 2.1 AA: contrast ≥ 4.5:1, keyboard focus visible, all interactive elements keyboard operable, charts provide an accessible data table and aria-label summaries And access is restricted: Owner/Admin/Manager roles can view all Insights; Instructor role sees only their own sessions; others receive 403 and the menu item is hidden And view, export, and schedule actions are written to the audit log with user, timestamp, and scope
Time Zone & Data Quality Normalization
"As a product manager, I want robust, accurate timestamps and metrics so that insights are trustworthy across locations."
Description

Normalize all timestamps to the venue’s time zone with DST awareness, capture device/server offsets, and enforce consistent event ordering. Implement deduplication, outlier detection, and gap-filling rules to protect metric accuracy. Monitor data latency and completeness with alerts, provide a test data generator for validation, and document retention and privacy constraints to ensure compliant, trustworthy insights across regions.

Acceptance Criteria
Normalize Timestamps to Venue Time Zone with DST Boundaries
Given a session scheduled at a venue with IANA time zone "America/Los_Angeles" spanning a DST change And inbound events originate from devices in arbitrary time zones and the server in UTC When Doorflow Insights ingests and stores events and renders metrics Then all stored canonical event_time_local values are converted to the venue time zone with correct DST offsets And no event is assigned an impossible local time (e.g., the skipped hour during spring-forward) And events landing in the repeated hour during fall-back are disambiguated using UTC and offset to maintain correct order And all user-facing charts and exports display venue-local timestamps and label the time zone
Capture and Persist Device/Server Clock Offsets
Given each inbound event contains device_local_time and device_tz/offset and a monotonic server_received_at When the event is processed Then the system computes device_clock_skew_ms and persists it to event_metadata.skew_ms And if |skew_ms| exceeds 120000 ms, a Data Quality warning is recorded and visible in the session quality panel And aggregate skew percentiles (p50, p95) are available per venue per day via the Insights API
Enforce Causal Event Ordering per Session
Given events for a single session (reminders_sent, doors_opened, check_in, grace_expired, waitlist_offer_sent, waitlist_converted) And events may arrive out of order within a 10-minute window When events are ingested Then the system orders them by canonical_event_time and causality rules (e.g., reminders_sent before session_start; check_in before grace_expired unless marked late) And any event that violates causality after reordering is flagged invalid and excluded from metrics with a reason_code And ordering is idempotent and repeatable across pipeline replays
Deduplicate Retries and Double-Taps
Given events may be retried by clients or triggered by double-taps When multiple events share the same idempotency_key or match the dedup signature (user_id, session_id, event_type, within 5s, same payload hash) Then only one canonical event is retained and duplicates are linked via duplicate_of reference And deduplication rate per session is surfaced in Data Quality metrics And false-positive dedup rate is below 0.1% based on weekly sampling audits
Outlier Detection and Gap-Filling for Arrivals
Given arrival-related events outside plausible windows (earlier than 90 minutes before start or later than 60 minutes after start) When such outliers are detected Then they are excluded from punctuality metrics and marked with outlier_reason And when a check-in is missing but adjacent signals (e.g., door_open + device_presence) suggest presence, the system imputes a check-in with source=imputed and a confidence score And imputed records are flagged in exports and do not exceed 2% of events per venue-week without raising a warning in the admin health dashboard
Monitor Latency and Completeness with Alerts
Given an ingestion SLA of P95 < 60 seconds from server_received_at to availability in Insights When latency exceeds the SLA for 3 consecutive 5-minute intervals for a venue Then a critical alert is sent to on-call with venue and shard identifiers And daily completeness per session (expected vs received events) is computed and if completeness < 98% a warning is posted in the admin health dashboard And latency and completeness are exposed via API endpoints with time-bucketed series for the last 30 days
Validation Toolkit and Data Governance Compliance
Given a test data generator accessible via admin CLI and API When generating datasets for venues across multiple IANA time zones, including DST transitions and data quality edge cases (duplicates, out-of-order, missing events, clock skew) Then datasets are reproducible via a seed, contain labeled ground truth, and are loadable into a sandbox pipeline And a validation report compares pipeline outputs vs ground truth within tolerances (arrival curve MAPE <= 2%, punctuality error <= 0.5 percentage points) And data retention policies are enforced (raw events 30 days, canonical facts 400 days, aggregates 3 years) with region-specific overrides And PII fields are minimized, encrypted at rest, and purgeable; right-to-erasure requests complete within 30 days and are auditable

TapTrust Sessions

Remember trusted devices with configurable durations per role. Uses device biometrics or a quick SMS check to re-confirm only when risk signals spike (new IP, late-night access). Stay logged in on personal phones while shared devices auto-expire—less friction with strong security.

Requirements

Role-Based Trust Duration Policies
"As an owner, I want to set different trust durations for admins, instructors, staff, and students so that security matches each role’s risk without adding friction to everyday bookings."
Description

Provide configurable “remembered device” durations and session timeouts per role (Owner/Admin, Instructor, Staff, Student). Support organization-level defaults with location-level overrides and exception rules (e.g., admin max 12 hours, instructors 30 days, students 60 days). Allow additional constraints such as business-hours windows, IP/network allowlists, and prohibition on trusting devices marked as shared or detected as private-browsing. Enforce policies in the session middleware at login, token refresh, and policy change events, automatically expiring sessions or revoking trust when limits are reached or settings change. Persist policies in the existing settings service with audit trails and apply them consistently across web and mobile clients.

Acceptance Criteria
Apply Role Defaults and Location Overrides at Login
Given an Instructor user with Organization default trust duration = 30 days and Session Idle Timeout = 4 hours And Location X overrides Instructor trust duration to 7 days and does not override Session Idle Timeout And the login occurs from a non-shared device, not in private browsing, during business hours, from an allowlisted IP When the Instructor logs in and chooses "Remember this device" Then the trusted device TTL is set to 7 days And the session idle timeout is set to 4 hours And the trust token and expiry are stored and visible in device/session diagnostics
Enforce Role Caps and Exception Rules
Given an Admin user and Organization default trust duration is 24 hours with an exception rule "Admin max trust = 12 hours" When the Admin logs in and chooses "Remember this device" Then the trusted device TTL is capped at 12 hours Given a Student role with Organization default trust duration set to 90 days and a rule "Student max trust = 60 days" When saving the policy Then validation fails with a clear error explaining the maximum is 60 days and the policy is not saved Given an Instructor location override attempts to set trust duration to 60 days while Organization default is 30 days and no max rule prevents it When saving the location override Then the location override of 60 days is saved and takes precedence for that location
Business-Hours Window Constraint
Given Organization business hours are 08:00–20:00 local time And Instructor trust duration is 30 days When an Instructor attempts to "Remember this device" at 21:30 Then the "Remember this device" option is disabled and no trusted token is issued Given a previously trusted Instructor session makes a token refresh at 21:30 When the token refresh occurs outside business hours Then the trust is not extended and if the trust TTL has elapsed, a step-up re-authentication is required before access
IP/Network Allowlist Enforcement
Given Organization/Location allowlist includes 203.0.113.0/24 and 2001:db8::/32 When a Staff user logs in from IP 198.51.100.10 Then no trusted device token is issued and only a standard session is created Given a Student has an active trusted session established on 203.0.113.15 When the device network changes to 198.51.100.10 and the next request or token refresh occurs Then the trusted session is revoked immediately and the user is prompted for re-authentication
Prohibit Trust on Shared or Private-Browsing Devices
Given a device is marked as Shared in the device registry When any role attempts to enable "Remember this device" Then the option is hidden or disabled and no trust token is created Given private/incognito browsing is detected by the client When a user logs in Then no trusted device token is issued and session follows non-trusted timeout rules
Policy Change Triggers Re-evaluation and Auto-Expiry
Given multiple active trusted sessions exist for Instructors with trust ages ranging from 1 to 45 days And the Organization updates Instructor trust duration to 7 days effective immediately When the policy change event is propagated Then within 60 seconds the session middleware reevaluates all active Instructor sessions And any session whose trust age exceeds 7 days is revoked and requires step-up re-authentication on the next request And an audit log entry is recorded for each forced revocation with session ID, user ID, role, location, timestamp, and reason "PolicyChanged"
Persist Policies with Audit Trail and Cross-Client Consistency
Given an Owner updates trust duration for Instructor to 30 days at the Organization level and to 7 days at Location X, and sets Admin max trust to 12 hours When the settings are saved Then the settings service persists the values with version, scope (org vs location), actor ID, timestamp, before/after values, and change reason And a GET to the settings service returns the effective policy for a given role and location (Instructor at Location X = 7 days; Admin max trust = 12 hours) And both web and mobile clients, on next login and token refresh, enforce the same effective policy computed by the middleware
Device Trust Enrollment with Biometrics & SMS Fallback
"As a returning student, I want my phone remembered using Face ID or a quick code so that I can book classes faster without typing passwords every time."
Description

Implement a post-auth enrollment flow to mark a device as trusted. Prefer platform biometrics via WebAuthn/Passkeys where available; otherwise confirm via a one-time SMS code. Capture a privacy-preserving device fingerprint and user-provided device name, along with metadata (OS, browser/app version, last seen, coarse location). Store trust tokens securely (web: secure, HttpOnly, SameSite cookies; mobile: OS secure storage). Enforce per-role limits on the number of trusted devices, provide clear UX to accept/decline trust, and apply rate limiting and anti-abuse controls for OTP requests. Ensure consent text covers SMS charges where applicable and that all trust data is encrypted at rest.

Acceptance Criteria
WebAuthn Enrollment on Supported Device
Given a user has completed primary authentication on a device that supports WebAuthn/Passkeys and their role is eligible for trusted devices When the user selects “Trust this device” and grants consent Then a WebAuthn registration ceremony is initiated and successfully completed using platform authenticator And a device-bound trust token is created with an expiration equal to the configured per-role duration And the user sees a success confirmation that the device is now trusted
SMS Fallback Enrollment on Unsupported or Denied Biometrics
Given a user has completed primary authentication on a device without WebAuthn support or has declined biometric enrollment When the user requests SMS verification to trust the device Then a one-time code is sent to the user’s verified phone number including brand identification and SMS charges disclosure And the code is valid only for the configured TTL (default 5 minutes) and single use And entering the correct code within TTL enrolls the device as trusted and shows a success message And entering an incorrect code increments the failed attempt count; after 5 failed attempts the flow is blocked for 15 minutes
Explicit Consent and Decline Trust UX
Given a user is shown the post-auth prompt to trust the device When the user selects “Not on this device” or dismisses the prompt Then no trust token is created, no SMS is sent, and no WebAuthn registration occurs And the current session follows standard expiration for untrusted devices And the user is informed they can trust the device later from Account > Devices And the consent text presented includes disclosure that SMS rates may apply when using SMS verification
Capture and Persist Device Metadata and Fingerprint
Given a device is successfully enrolled as trusted Then the system stores a privacy-preserving device fingerprint (hash derived from non-PII signals with salted tenant scope) And the system stores the user-provided device name (required, 2–50 characters, sanitized of HTML) And the system records OS name/version, browser or app version, coarse location (city/region/country from IP), and last seen timestamp And if any metadata is unavailable, the field is stored as null without blocking enrollment And on subsequent logins from the same device/browser, the fingerprint matches the previously stored value (barring user storage reset or major environment change)
Per-Role Trusted Device Limits and Duration
Given per-role policies define a maximum trusted device count (L) and trust duration (D) When a user with role R attempts to enroll a new trusted device and already has L trusted devices Then the enrollment is blocked with error code TR-ROLE-LIMIT and a message offering a link to manage devices And when the user is below L devices, enrollment proceeds And the issued trust token expires at now + D, after which the device is treated as untrusted until re-confirmed And removing a trusted device immediately decrements the count and invalidates its server-side token
Secure Trust Token Storage (Web and Mobile)
Given a device is enrolled as trusted on web Then the trust token is stored in a cookie with Secure=true, HttpOnly=true, SameSite=Lax (configurable), and scoped to the service domain only And JavaScript cannot read the cookie value Given a device is enrolled as trusted on mobile apps Then the trust token is stored only in OS secure storage (iOS Keychain / Android Keystore) with background backup disabled where applicable And in all cases, server-side trust records and metadata are encrypted at rest using the platform key management service And on logout or when a device is untrusted, the client token is deleted and the server token is revoked immediately
OTP Request Rate Limiting and Abuse Protections
Given a user requests an SMS code for device trust enrollment Then requests are limited to the configured threshold (default 5 sends per hour per user and IP) and return HTTP 429 with retry-after when exceeded And a cool-down (exponential backoff) is applied after each failed verification attempt And after 3 sends in 15 minutes, a CAPTCHA challenge is required before sending another code And all sends, verifications, failures, and blocks are logged with user, device fingerprint (if available), IP, and timestamp for audit And rate limits reset according to configuration at the end of the window
Risk-Based Re-Authentication Triggers
"As an instructor, I want the app to re-check it’s really me only when something looks risky so that I stay secure without constant prompts."
Description

Continuously evaluate session risk and prompt lightweight re-confirmation only when needed. Inputs include IP/ASN change, geo-velocity, late-night access outside configured hours, device fingerprint drift, unusual device attributes, and sensitive actions (payout updates, export clients, refund approvals). Configure per-role thresholds for when to prompt and which factor to prefer (biometric first, SMS fallback). The risk engine must respond in under 150 ms, log all decisions with reason codes, and degrade gracefully if the risk service is unavailable. Integrate prompts seamlessly into web and mobile flows so that low-risk actions remain frictionless while high-risk scenarios require quick step-up verification.

Acceptance Criteria
Adaptive Prompting Across Low- and High-Risk Actions
Given a signed-in user performing a low-risk action within role thresholds When the risk engine evaluates the request Then no re-auth prompt is shown and the action completes successfully without additional user interaction Given a signed-in user initiates a sensitive action (payout updates, export clients, refund approvals) When the risk engine evaluates the request Then a step-up re-authentication is required before the action executes Given a sensitive action is initiated When the user fails, cancels, or times out on the step-up check Then the action is blocked with no side effects and an error is returned (HTTP 403 or equivalent) Given the user successfully completes the step-up verification When the sensitive action is retried within the same flow Then the action completes and only occurs once Given web and mobile clients When a step-up is required Then the prompt renders inline/in-app without full page reload and preserves any unsaved form inputs
Anomalous Network/Location Triggers
Given an active session with a stable IP/ASN When the IP or ASN changes mid-session Then a step-up re-authentication prompt is shown on the next request before continuing Given consecutive requests from locations implying geo-velocity over the configured threshold (e.g., >500 km within 15 minutes) When the next protected action is attempted Then a step-up prompt is required Given network/location anomalies are detected When the user completes step-up successfully Then the session continues; when step-up fails Then the action is blocked and the session remains signed in but restricted from sensitive actions Given no anomalies are present and risk remains below the role threshold When subsequent actions are performed Then no step-up prompts are shown
Late-Night Access Thresholds per Role
Given organization-configured access hours per role (e.g., Instructor 05:00–22:00, Admin 24/7) When a user accesses outside their role’s allowed hours Then a step-up prompt is required prior to proceeding Given two users with different roles and configured hours When both access at 23:30 local time Then the Instructor is prompted for step-up and the Admin is not Given a user outside allowed hours completes step-up successfully When continuing within the same session Then the action proceeds without re-prompt unless new risk signals are detected
Factor Preference and Fallback Behavior
Given step-up is required and the device supports platform biometrics/WebAuthn with an enrolled credential When the prompt is shown Then biometric/WebAuthn is offered first Given biometric/WebAuthn fails, is declined, or is unavailable within 15 seconds or 2 attempts When step-up is still required Then SMS OTP to a verified phone number is offered as fallback Given neither biometric/WebAuthn nor a verified SMS number is available When step-up is required Then access to the protected action is denied with guidance to contact an admin Given step-up via the preferred factor or fallback succeeds When the protected action is retried Then it completes without further prompts in that flow
Device Fingerprint Drift and Unusual Attributes
Given a remembered session with a stored device fingerprint When the current fingerprint drift exceeds the configured role threshold (e.g., multiple key components changed) Then a step-up prompt is required before proceeding Given the device reports unusual attributes (e.g., emulator/root/jailbreak flags or time-zone/device-language anomalies) When the user attempts a protected action Then a step-up prompt is required Given minor, expected changes (e.g., browser patch update only) keep drift below threshold When the user continues normal activity Then no step-up is prompted
Risk Engine Performance and Resilience
Given steady-state traffic at nominal load When evaluating risk for requests Then p95 decision latency is ≤150 ms and p99 is ≤250 ms measured over rolling 5-minute windows Given burst traffic at 2× nominal load When evaluating risk for requests Then p95 decision latency remains ≤150 ms Given the risk service is unavailable or times out (e.g., >120 ms to respond) When a decision is needed Then the system degrades gracefully by allowing low-risk actions to proceed without prompt and requiring step-up for sensitive actions, producing a decision in ≤50 ms via fallback rules Given the system is in degraded mode When service health is restored Then normal risk evaluation resumes automatically without user disruption
Decision Logging and Auditability
Given any risk evaluation or prompt decision is made When logging the event Then a log entry is recorded with timestamp (UTC), correlation_id, user_id, session_id, decision (allow/step-up/block), reason_codes[], evaluated signals, chosen factor (if any), outcome, and evaluation latency Given a step-up is triggered by a specific signal (e.g., IP_CHANGE, GEO_VELOCITY, LATE_HOUR, FINGERPRINT_DRIFT, SENSITIVE_ACTION) When the log is inspected Then the corresponding reason code is present and matches the trigger Given a decision is taken in degraded mode When the log is inspected Then a reason code includes RISK_SERVICE_UNAVAILABLE and the fallback policy used
Shared Device Auto-Expire Mode
"As front-desk staff using a shared tablet, I want sessions to auto-expire and disallow trusted devices so that client data stays protected between users."
Description

Provide a mode for kiosks and front-desk/shared tablets where devices cannot be trusted and sessions auto-expire aggressively. Enforce short inactivity timeouts, maximum session lifetimes, and mandatory re-auth for sensitive actions. Expose a simple toggle at location level and an on-login prompt to mark a device as shared. Store no long-lived refresh tokens on shared devices; rely on short-lived session cookies only. Integrate with check-in and booking flows to enable quick sign-out and “end of shift” bulk logout options.

Acceptance Criteria
Location-Level Shared Mode Toggle & Login Prompt
Given I am an Org Admin with Location Settings permission, When I navigate to Location > Security, Then I can see a toggle "Shared Device Auto-Expire Mode" with a help tooltip and default OFF. Given I turn the toggle ON and click Save, When I reload the page, Then the setting persists and an audit log entry is recorded with user, location, timestamp, and old/new values. Given the location toggle is ON, When a user opens the location's login page, Then the login form displays a "This is a shared device" prompt preselected and the "Remember this device" option is hidden or disabled. Given the location toggle is OFF, When a user opens the login page, Then the "This is a shared device" prompt is available but not preselected and "Remember this device" is available.
Shared Mode Inactivity Timeout
Given a session is in Shared Device Mode, When there is no user interaction for 2 minutes (configurable 1–5 minutes), Then the session is terminated server-side and the client is redirected to the login screen with the message "Session ended due to inactivity". Given the client remains open without network connectivity, When 2 minutes elapse, Then the next API call or page action returns 401 and triggers forced logout. Given the inactivity timer expires, Then reloading the page does not auto-log in and no user data persists in the UI.
Shared Mode Maximum Session Lifetime
Given a session is in Shared Device Mode, When 20 minutes have elapsed since authentication (configurable 10–60 minutes), Then the session is terminated regardless of activity and the next action requires full re-authentication. Given the maximum lifetime is reached, Then any "extend session" prompts are not shown and the access/session cookie is invalidated. Given an admin updates the maximum lifetime value, When a new shared session is created, Then the new value applies; existing sessions keep their original expiry.
Sensitive Actions Require Re-Authentication
Given a user is in a Shared Device Mode session, When they attempt a sensitive action (view/export customer PII, issue a refund, change payout/billing settings, manage staff), Then they are prompted to re-authenticate via password or SMS OTP; biometric options are not offered. Given the user fails re-authentication 5 times within 10 minutes, Then sensitive actions are locked for 5 minutes and an audit log event is recorded. Given the user completes re-authentication, Then they have a 60-second elevated access window to complete only the initiated action; this does not reset inactivity or maximum session timers.
No Long-Lived Tokens on Shared Devices
Given a session is created with "This is a shared device" selected or enforced by location setting, Then the server does not issue a refresh token and only sets a short-lived session cookie (HttpOnly, Secure, SameSite=Lax) expiring in ≤10 minutes. Given a shared session attempts to call the token refresh endpoint, Then the request is rejected with 401 Unauthorized and no new tokens are issued. Given a shared session ends (timeout, logout, or end-of-shift), Then localStorage/sessionStorage contains no auth tokens and all auth cookies are cleared.
Quick Sign-Out and End-of-Shift Bulk Logout
Given the app is running in Shared Device Mode, When any page in check-in or booking flows is displayed, Then a persistent "Sign out" control is visible and signs the current user out within 1 second, returning to the login screen. Given a user with Manager role opens the staff menu, When they select "End of Shift" and confirm, Then all active shared sessions for that location are invalidated within 5 seconds, an audit log records the count and user, and affected devices show the login screen on their next action. Given End of Shift is executed, Then personal trusted devices and non-shared sessions outside the location are not affected.
Trusted Device Management Console
"As an admin, I want to see and revoke any trusted device in my studio so that I can quickly respond to lost phones or suspicious activity."
Description

Add user- and admin-facing views to list, rename, and revoke trusted devices. Show device name, last seen time, rough location, trust method (biometric/SMS), and sign-in history summary. Allow users to revoke specific devices or sign out everywhere. Provide org-level controls for admins to set per-role device limits, force revoke by user, and view audit logs. Send alerts (email/SMS/push) on new device enrollment with a one-click revoke link. Expose secure APIs for device management actions and ensure all events are captured for compliance reporting.

Acceptance Criteria
User Manages Trusted Devices List
Given an authenticated user with two or more trusted devices When the user opens Security > Trusted Devices Then the list shows for each device: Device Name, Last Seen (converted to the user’s timezone), Rough Location (city and country or "Unknown"), Trust Method (Biometric or SMS), and a Sign-in History Summary (count of sign-ins in the last 30 days and timestamp of the most recent sign-in) And devices are sorted by Last Seen in descending order And only devices belonging to the user are returned and rendered And each device row exposes actions: Rename and Revoke And the list loads within 2 seconds for up to 50 devices on a median mobile connection When the user renames a device with a value between 2 and 50 characters (after trimming), using allowed characters [A–Z a–z 0–9 space - _] Then the new name persists, appears immediately in the list, and remains after refresh And invalid input returns HTTP 422 with field-level errors and does not change the stored name And names are not required to be unique per user And an audit event DeviceRenamed is recorded with actor=user, deviceId, oldName, newName, and timestamp
User Revokes Device or Signs Out Everywhere
Given a user viewing their trusted devices When the user clicks Revoke on a specific device and confirms Then the device’s trust token is invalidated and removed from the list within 5 seconds And any active sessions on that device are terminated within 10 seconds And subsequent API calls using that device token return 401 Unauthorized And a success confirmation is shown to the user And an audit event DeviceRevoked is recorded with actor=user, deviceId, ip, userAgent, and timestamp When the user selects Sign out everywhere and confirms Then all active sessions across web and mobile are terminated, including the current session, within 10 seconds And all trusted device tokens for the user are revoked And the user is redirected to the sign-in screen And a confirmation email is sent to the account email And an audit event GlobalSignOut is recorded with counts of sessions and devices revoked
Admin Configures Per-Role Device Limits
Given an organization admin with permission to manage security policies When the admin opens Admin > Security > Trusted Devices Policy Then the admin can set a per-role device limit (integer 0–10) and a limit action policy (BlockNew or ExpireOldest) And saving changes persists the policy and applies to new enrollments within 1 minute When a user at limit attempts to enroll a new trusted device and policy=BlockNew Then enrollment is blocked with a user-facing message stating the role limit has been reached and to remove an existing device And an audit event PolicyLimitEnforced is recorded with userId, role, attemptedDevice, and policy=BlockNew When a user at limit attempts to enroll a new trusted device and policy=ExpireOldest Then the oldest trusted device is revoked and the new device is enrolled successfully And both events (DeviceRevoked for oldest and DeviceEnrolled for new) are recorded with linkage via correlationId And an audit event PolicyUpdated is recorded when the admin saves changes, including changed fields and actor=admin
Admin Force-Revokes User Devices and Audits Activity
Given an organization admin When the admin searches for a user by name or email and opens their Trusted Devices Then the admin sees the user’s device list with: Device Name, Last Seen, Rough Location, Trust Method, Sign-in History Summary, and Enrollment Date And the admin can revoke a single device or revoke all devices for that user And revocations terminate sessions within 10 seconds and mirror user-initiated behavior And audit events AdminDeviceRevoked and AdminGlobalRevoke are recorded with actor=admin, userId, deviceId (if applicable), reason, and timestamp When the admin opens Audit Logs Then logs are filterable by date range, user, event type, and actor, with pagination and CSV export And each log entry includes: timestamp (UTC), orgId, userId, deviceId (if applicable), actorId, actorRole, action, outcome, ip, userAgent, and correlationId And audit logs are read-only in the UI and cannot be altered or deleted
New Device Enrollment Alerts with One-Click Revoke
Given a new trusted device enrollment succeeds When the event is recorded Then the system sends an alert within 60 seconds via channels enabled by user/org preferences (email, SMS, push) And the alert includes device name, rough location, trust method, timestamp, and a one-click revoke link And the revoke link is single-use and expires after 24 hours When the recipient clicks the revoke link Then the referenced device is revoked, active sessions on that device terminate within 10 seconds, and a confirmation is displayed And invalid or expired links show a safe error without revealing account details And alert delivery attempts and link actions are logged as AlertDelivered and AlertRevocationLinkUsed with correlationId And alert notifications are rate-limited to a maximum of 3 per 10 minutes per user
Secure Device Management APIs
Given API clients authenticated via OAuth2 When calling device management endpoints (GET /v1/devices, PATCH /v1/devices/{id}, DELETE /v1/devices/{id}, POST /v1/devices/signout-all, GET /v1/admin/users/{userId}/devices, POST /v1/admin/users/{userId}/devices/revoke-all) Then access is authorized by scopes (device.read, device.write, admin.device.manage) and restricted to the caller’s org And requests are rate-limited to at least 60 requests/minute per user or client and return 429 with Retry-After when exceeded And write operations support idempotency via Idempotency-Key header returning the same result for retries within 24 hours And responses use standard codes (200/204 success; 400/401/403/404/409/422 errors) with JSON problem details including correlationId And all successful and denied API actions generate audit events with actorType=api and include clientId
Compliance Event Capture and Reporting Export
Given the system processes device-related actions When any of the following occurs: DeviceEnrolled, DeviceRenamed, DeviceRevoked, GlobalSignOut, PolicyUpdated, PolicyLimitEnforced, AdminDeviceRevoked, AdminGlobalRevoke, AlertDelivered, AlertRevocationLinkUsed, ApiAccessDenied Then an immutable audit/compliance event is recorded containing: eventId, timestamp (UTC), orgId, userId (if applicable), deviceId (if applicable), actorId, actorType (user/admin/system/api), action, outcome, ip, userAgent, location (if available), reason (if provided), and correlationId And events are retained for at least 24 months And admins can export audit events from Admin > Reports as CSV or JSON for a selected date range and filters And exports up to 100,000 events complete within 2 minutes and are available for secure download with a logged AuditExport event And events are retrievable via API (GET /v1/audit/events) with pagination and filter parameters
Secure Session Persistence & Token Rotation
"As a studio owner, I want sessions to persist reliably on personal phones and end quickly when revoked so that my team stays productive and secure."
Description

Implement sliding session persistence optimized for mobile with refresh-token rotation and reuse detection. Bind tokens to device fingerprints and enforce per-role TTLs and inactivity windows. On web, use Secure, HttpOnly, SameSite cookies; on mobile, store tokens only in the OS keychain/keystore and gate app re-entry with biometric check after configurable idle periods. Propagate revocations instantly across devices, support single sign-out, and ensure graceful fallback when biometrics are unavailable (prompt SMS). Encrypt all device trust artifacts at rest and in transit, and add monitoring for anomalous token reuse to auto-revoke compromised sessions.

Acceptance Criteria
Sliding Session Persistence by Role
Given a signed-in user on a personal mobile device with role "Coach" and activity within the configured inactivity window When the user performs any API call within the window Then extend the session expiration by the sliding interval without exceeding the role TTL Given a signed-in user on a shared kiosk with role "FrontDesk" idle beyond the inactivity window When the user resumes activity Then require re-authentication and do not slide the session Given any role with no activity for the full role TTL When the user attempts any action Then the session is expired and re-authentication is required
Refresh Token Rotation and Reuse Detection
Given a valid refresh token bound to a device fingerprint When it is used to refresh Then issue new access and refresh tokens, invalidate the prior refresh token, and bind the new tokens to the same fingerprint Given a previously used (invalidated) refresh token is presented again When the refresh endpoint is called Then deny the request with 401, revoke the associated session on that device, log an anomaly event, and notify security monitoring Given refresh token reuse is detected from a different IP or device fingerprint When the event is processed Then revoke all active sessions for the user and require step-up verification on next login
Device Binding and Risk-Based Step-Up
Given a session bound to device fingerprint A When requests arrive with a mismatched fingerprint or a new IP during a high-risk time window Then prompt for biometric verification; if unavailable, send an SMS one-time code and proceed only on successful verification Given step-up verification fails three times within 10 minutes When the user retries Then lock the session, force full sign-in, and record an audit event Given step-up succeeds When the session continues Then update the risk state and avoid re-prompting within the configured cooldown period unless new risk signals arise
Web Cookie Security Flags Enforcement
Given a successful web authentication over HTTPS When setting session cookies Then set Secure and HttpOnly flags on all cookies and set SameSite=Lax for access tokens and SameSite=Strict for refresh tokens per policy Given the app is served over HTTP When attempting to set Secure cookies Then do not set cookies and log a security misconfiguration event Given a cross-site request from a third-party context When the browser attempts to send the refresh cookie Then the SameSite policy prevents transmission
Mobile Keychain Storage and Biometric Gate with SMS Fallback
Given a successful mobile authentication When storing tokens Then store tokens only in the OS keychain/keystore with access control tied to device unlock/biometric Given the app is reopened after the configured idle threshold When the user returns to the app Then require biometric unlock; if biometrics are unavailable or disabled, send an SMS one-time code and grant access only after successful code entry Given the device is detected as rooted/jailbroken When authentication is attempted Then deny token storage, require full re-auth each time, and flag the device as high risk
Single Sign-Out and Revocation Propagation
Given a user selects Sign Out on any device When the action completes Then revoke all access and refresh tokens for that user across all devices within 5 seconds and invalidate server-side sessions Given an administrator revokes a device trust record When revocation is saved Then invalidate sessions bound to that device fingerprint within 5 seconds and trigger re-auth on next request Given a previously offline device reconnects When the first API call is made Then the client detects revoked tokens and forces re-auth before accessing protected resources
Encryption of Trust Artifacts and Anomaly Monitoring
Given device trust artifacts (device fingerprints, remember-device records, tokens) are persisted When stored at rest Then they are encrypted using AES-256-GCM or stronger Given any network transmission of tokens or trust artifacts When data is sent Then it is protected with TLS 1.2+ end-to-end Given anomalous token reuse is detected by monitoring When the event is processed Then auto-revoke implicated sessions, create an audit entry with user, device fingerprint, IP, and timestamp, and notify the security webhook within 30 seconds

LinkShield

Harden magic links with short expiries, one-use tokens, domain lock, and device binding. Detect forwards and unsafe redirects, auto-issue a fresh link, and show a clear countdown UI. Prevents hijacks and mis-clicks while keeping the flow truly one tap.

Requirements

One-Use Short-Expiry Links
"As a returning customer booking a class, I want my magic link to work once and expire quickly so that my account and payment details remain secure even if the link is exposed."
Description

Generate magic links that are valid for a single successful use and auto-expire within a short, configurable TTL (default 10 minutes). On first redemption, the token is invalidated server-side to prevent replay. Supports per-link TTL overrides for high-risk actions (e.g., payment confirmation) and handles clock skew gracefully. Integrates with ClassNest’s booking and payment flows to preserve the one-tap experience while eliminating link hijacking and accidental reuse. Emits structured events for redemption, expiration, and replay attempts.

Acceptance Criteria
Default TTL: 10-minute expiry
Given a magic link L is created without a TTL override at server time T0 When L is redeemed at T0 + 9 minutes Then the redemption succeeds (HTTP 200) and the intended action executes And a magic_link.redemption event is emitted with link_id, token_id, user_id, action, occurred_at, result=success Given the same link L remains unused When server time reaches T0 + 10 minutes Then a magic_link.expired event is emitted within 60 seconds and only once Given L is now expired and unused When L is redeemed at or after T0 + 11 minutes Then the request is rejected (HTTP 410) with error_code=expired And no magic_link.redemption event is emitted for this attempt
Single-use token invalidation and replay prevention
Given a valid unused magic link L When L is redeemed successfully Then L's token is invalidated server-side atomically before responding And subsequent redemption attempts of L return HTTP 410 with error_code=already_used And a magic_link.replay_attempt event is emitted for each attempted reuse with link_id, token_id, user_id, occurred_at, reason=already_used And exactly one magic_link.redemption event exists for L Given two concurrent redemption requests for L arrive within 100 ms When they are processed Then exactly one request succeeds (HTTP 200) and one fails (HTTP 410 with error_code=already_used) And only one intended action is executed
Per-link TTL override for high-risk actions
Given a payment confirmation magic link LP is created with a TTL override of 2 minutes at server time T0 When LP is redeemed at T0 + 1 minute 55 seconds Then redemption succeeds (HTTP 200), the action executes, and a magic_link.redemption event is emitted When LP is redeemed at T0 + 2 minutes 30 seconds Then redemption is rejected (HTTP 410) with error_code=expired And a magic_link.expired event is emitted no later than T0 + 2 minutes + 60 seconds and only once Given a booking link LB is created without a TTL override Then LB's TTL defaults to 10 minutes
Clock skew handling and grace window
Given system maxSkew is 60 seconds and a magic link L is issued at server time T0 with TTL 10 minutes When redemption occurs at server time T0 + 10 minutes + 30 seconds Then redemption succeeds (HTTP 200) When redemption occurs at server time T0 + 10 minutes + 61 seconds Then redemption fails (HTTP 410) with error_code=expired And the decision relies solely on server time; client/device clock is ignored
Structured events for redemption, expiration, replay
Given any magic link lifecycle event occurs (redemption, expiration, replay attempt) Then an event is published to the event bus within 2 seconds And the payload includes at minimum: event_id (UUID), event_type ∈ {magic_link.redemption, magic_link.expired, magic_link.replay_attempt}, link_id, token_id, user_id (nullable), action, occurred_at (ISO-8601 UTC), request_ip, user_agent, result (success|failure), reason (present when result=failure) And for each link, at most one of magic_link.redemption or magic_link.expired is emitted; replay attempts may be multiple And events are idempotently delivered (the same event_id is not emitted more than once)
One-tap booking flow integration
Given an unauthenticated customer receives a booking confirmation magic link LB that is unused and within TTL When the customer taps LB on a mobile device Then the booking is confirmed and the customer is taken directly to the booking success page without additional authentication steps And LB is invalidated server-side and cannot be reused And the end-to-end round-trip completes within 1.5 seconds at p95 in production When the same LB is tapped again Then no duplicate booking occurs, the request returns HTTP 410 with error_code=already_used, and a magic_link.replay_attempt event is emitted
One-tap payment confirmation with idempotency
Given a customer receives a payment confirmation magic link LP with a 2-minute TTL override that is unused and within TTL When the customer taps LP Then the payment confirmation executes exactly once, the customer is taken directly to the payment success page, and no additional authentication is required And LP is invalidated server-side and cannot be reused And a magic_link.redemption event is emitted with result=success When LP is tapped again or concurrently by multiple devices Then no additional charge or duplicate confirmation occurs, the request returns HTTP 410 with error_code=already_used, and a magic_link.replay_attempt event is emitted
Device Binding and Domain Lock
"As a customer opening a booking link from my phone, I want the link to only work on my device and trusted domains so that malicious forwards or redirects can’t compromise my access."
Description

Bind each magic link to a lightweight, privacy-preserving device fingerprint (e.g., client hints + user agent hash) and enforce a same-device redemption policy with a tolerant threshold for minor changes. Restrict link navigation to an allowlist of ClassNest-owned domains and approved custom domains to block open-redirect and phishing attempts. Provide safe fallbacks: if binding or domain checks fail, gracefully invalidate the link and direct the user to request a fresh link. Integrates with deep links for mobile apps and browser-based flows without adding friction.

Acceptance Criteria
Same-Device Redemption with Tolerant Threshold
Given a magic link is issued with a stored device fingerprint derived from client hints and a user agent hash And the user opens the link on the original device with only minor UA/client-hint changes When the redemption request is evaluated Then redemption succeeds if either the fingerprint hash matches exactly OR at least 3 of 4 attributes match (device_type, browser_family, os_major, ch_mobile) And the audit log records device_match="soft" or "exact" and redemption_status="success" And no additional prompts or challenges are shown
Domain Lock with Redirect-Chain Enforcement
Given an allowlist that includes ClassNest-owned domains and verified custom domains And a magic link is opened via a URL that may include up to 5 redirects When the final landing host is not on the allowlist Then the link is not redeemed and the request is blocked And the UI displays "This link can only be opened on approved domains" And a "Send me a new link" action is presented And the original link is invalidated And a security event is logged with event_type="domain_lock_block", chain_length, and final_host
Graceful Fallback on Device Mismatch
Given a magic link bound to device A When the link is opened on device B and fails the tolerance rule (no exact hash match and fewer than 3 of 4 attributes match) Then the link is not redeemed and the token is immediately invalidated And the user sees guidance explaining the mismatch and a one-tap "Request a fresh link" option And upon tap, a fresh link is sent through the original delivery channel within 10 seconds And fresh-link requests are rate-limited to 3 per hour per user and logged
Deep Link Integration for App and Browser
Given the ClassNest mobile app is installed and registered for app links When the user taps a magic link from SMS or email Then the OS opens the app to the intended screen and the token is passed securely And device-binding checks are enforced and succeed if within tolerance And if the app is not installed, the link opens in the default browser on an allowlisted domain And in either path, no more than one user tap is required after opening the message to complete sign-in or booking
Privacy-Preserving Fingerprint Storage
Given a magic link is created When storing the device fingerprint Then only normalized client hints and user agent–derived values are used And the fingerprint is stored as a salted SHA-256 (or stronger) hash; raw UA/client-hint values are not persisted And no PII (e.g., IP address, email, phone) is included in the fingerprint And fingerprint records are retained no longer than the magic link expiry plus 24 hours
Custom Domain Allowlist Configuration
Given a studio owner adds and verifies a custom domain in admin settings When the configuration is saved Then magic links for that studio are redeemable on the new domain and on ClassNest-owned domains And enforcement nodes receive the allowlist update within 5 minutes And redemption attempts on non-allowlisted domains are blocked and logged
Forward/Redirect Detection with Auto-Reissue
"As a user who might accidentally access a forwarded link, I want the system to recognize the risk and send me a new secure link automatically so that I can continue without worrying about my security."
Description

Detect risky contexts such as email forwarding, atypical redirect chains, or materially different device/network characteristics during link redemption. When detected, immediately invalidate the current token and automatically issue a fresh link to the verified channel on file (SMS or email). Display a clear, non-blocking message explaining that a safer link has been sent, preserving the one-tap flow. All actions are logged for audit and anomaly analysis.

Acceptance Criteria
Forwarded Email Detected — Auto-Reissue to Verified Channel
Given a one-use magic link token bound to recipient R and channel email And a maintained list of known forwarders/rewriters is configured When the link is opened via a recognized forward/rewriter (e.g., Safe Links, Gmail redirector) or the token’s signed recipient hash does not match the delivery context Then the current token is marked invalid within 100 ms before any privileged action executes And a fresh token is generated and sent to the verified channel on file (prefer SMS if verified; else email to R) And the reissue send event is enqueued within 300 ms and delivered within 5 s at p95 And the UI shows a non-blocking notice that a safer link was sent, and the original link cannot be used thereafter (subsequent attempts return Invalid/Expired)
Atypical Redirect Chain — Invalidate and Reissue
Given an allowlist of redirect domains and a maximum redirect depth of 2 When redemption traverses more than 2 redirects, or any hop is not on the allowlist, or query tampering is detected (added/altered critical params) Then the request is classified as risky with reason_code=redirect_chain And the current token is invalidated prior to target action And a new token is issued and sent to the verified channel on file within 5 s p95 And the user is shown a non-blocking notice that a safer link was sent
Device/Network Mismatch — Reissue While Preserving One-Tap
Given tokens embed a signed snapshot of device/network (UA hash, OS family, IP ASN, coarse geo) When redemption occurs with any of: OS family mismatch; ASN change; geo distance >250 km within 15 minutes of send; or device fingerprint hash mismatch beyond threshold Then classify as risky with reason_code=device_network_mismatch And invalidate the current token and auto-reissue to the verified channel on file And upon opening the fresh link, the user reaches the intended action in one tap without additional prompts 99% of the time (p99) And no more than 2 auto-reissues are performed for the same user within a 10-minute window (loop protection)
Domain Lock & Unsafe Redirect Handling
Given magic links are locked to the configured host(s) and path allowlist When redemption occurs on a non-allowed host, includes an external open-redirect target, or modifies the target path outside the allowlist Then block the privileged action, mark the token invalid, and classify as risky with reason_code=domain_lock And auto-reissue a fresh link to the verified channel on file within 5 s p95 And subsequent attempts to reuse the original token return Invalid/Expired
Non-Blocking User Notice Explaining Auto-Reissue
Given a risky context triggers auto-reissue When the user lands on the redemption page Then an inline, non-modal banner appears within 500 ms stating a safer link was sent and showing a masked destination (e.g., +1•••1234 or e•••@d•••.com) And the page remains fully interactive; keyboard focus is not trapped; no action is blocked by the banner And the banner meets WCAG 2.1 AA (contrast, screen reader announcement within 2 s) and supports i18n strings And no full email or phone number is displayed (only masked)
Audit Logging and Anomaly Traceability
Given auto-reissue is triggered for any reason When the event is processed Then a structured audit record is written within 1 s containing: user_id, token_id, new_token_id, reason_code, timestamps (detected, invalidated, reissued), channel_selected, redirect_chain (hosts), device_hash, IP hash, ASN, coarse geo And audit records are immutable, queryable in the admin console, and exported via API; retention >=90 days And if logging fails, the redemption is aborted and an operational alert is emitted within 60 s
Expiry Countdown Banner
"As a mobile user opening a booking link, I want a clear countdown showing how long the link remains valid so that I can act in time or request a new link if needed."
Description

Display a real-time countdown UI on the landing page showing the remaining validity window for the magic link. When the timer reaches zero, transition the UI to an expired state with a primary CTA to get a fresh link. The banner is accessible (ARIA-live updates), localized, and lightweight for mobile. Supports auto-refresh to apply a newly issued link without additional steps, preserving a one-tap experience.

Acceptance Criteria
Real-Time Countdown Rendering
Given a valid magic link with a server-provided expiresAt 120 seconds in the future When the landing page loads on a mobile device Then the countdown banner displays the remaining time in mm:ss within 200 ms of first contentful paint And the displayed time is within ±1 second of server time And the banner updates every 1 second without skipping or duplicating seconds And the timer never displays negative values or non-numeric text
Expiry Transition and CTA Swap
Given a valid magic link with 5 seconds or less remaining When the countdown reaches 0 Then the banner switches to an expired state within 500 ms And all actions using the expired token are disabled and marked aria-disabled="true" And a primary, focusable CTA labeled "Get a new link" becomes visible And activating the CTA calls the refresh endpoint and shows a loading state until a response is received
Accessible ARIA-Live Updates
Given the banner is rendered for a screen reader user When the countdown updates Then updates are announced via an aria-live="polite" role="status" region And announcements occur at most once every 30 seconds while remaining time > 60 seconds And announcements occur once per second during the final 10 seconds And on expiry, the live region announces that the link has expired and a button is available to get a new link And the CTA has an accessible name that matches its visible label
Localization and Time Format
Given the browser locale is fr-FR When the banner renders with 90 seconds remaining Then all countdown text and announcements are localized to French And time is displayed as zero-padded mm:ss according to locale conventions And if the locale is unsupported, English (en-US) is used as a fallback And providing ?locale=es-ES forces Spanish localization for both text and announcements
Mobile Performance Budget
Given a cold page load over simulated 4G on a mid-tier Android device When the banner is rendered Then the incremental JS+CSS for the banner is ≤ 15 KB gzip And the banner introduces no more than one additional network request And the banner contributes ≤ 0.05 cumulative layout shift (CLS) And no single main-thread task introduced by the banner exceeds 50 ms
Auto-Refresh with Fresh Link Applied
Given the backend issues a fresh magic link token for the current session When the client receives the new token via the refresh endpoint or on CTA activation Then the banner replaces the expired token with the new token without a full page reload And the countdown resets to the new expiry within 500 ms And the primary action updates to use the new token and remains a single tap to continue And no additional authentication input is required from the user
Server-Time Sync and Resilience
Given the client clock differs from the server clock by 7 seconds When the banner initializes Then it computes and applies a server-client time delta using the Date header or a time-sync endpoint And the displayed remaining time is accurate to within ±2 seconds of server truth And if the tab is backgrounded for > 30 seconds or the device sleeps, the countdown corrects on resume without showing stale values And if the system clock changes by > 5 seconds, the banner re-syncs within 2 seconds using the next network response
One-Tap Fresh Link Resend
"As a customer whose link expired, I want to request a fresh link with one tap and continue my booking where I left off so that I don’t have to start over."
Description

Provide a single-tap action to request a new magic link via the original channel, with fallback to an alternate verified channel if the original is unavailable. Enforce rate limits, CAPTCHA on abuse signals, and lockout rules to prevent spam and enumeration. Maintain session context so the new link resumes the user exactly where they left off (e.g., selected class and timeslot). Emit analytics for resend requests, conversions, and abuse detections.

Acceptance Criteria
One-Tap Resend via Original Channel
Given a user opens an expired or invalidated magic link for a booking/session with saved context And the original delivery channel is healthy When the user taps "Send a new link" Then the system dispatches a fresh, one-use, domain-locked, device-bound magic link via the original channel within 2 seconds And all prior tokens for the same session are immediately revoked And the UI confirms "New link sent" and shows a TTL countdown and a cooldown timer on the resend button And an audit entry with masked user identifier, session ID, channel, device, and reason=expired is recorded
Fallback to Verified Alternate Channel
Given the original channel is unavailable due to bounce/suppression/carrier error or channel_health=false And the user has at least one other verified channel on file When the user taps "Send a new link" Then a fresh link is dispatched to the highest-priority verified alternate channel within 2 seconds And the UI states which channel was used in a privacy-preserving way (masked) And the system does not disclose whether the original channel exists/works beyond generic messaging And analytics and audit logs record reason=channel_fallback And if no alternate verified channel exists, the UI offers a secure verification flow and no link is sent
Rate Limits Enforcement
Given per-identity resend limits are 3 per 5 minutes, 10 per hour, and 30 per day, and per-IP limit is 10 per 5 minutes (all configurable) When resend requests exceed any limit Then the system blocks the request with HTTP 429 and shows a generic cooldown message with remaining wait time And no message is sent and no token is issued And analytics emit rate_limit_applied=true with limit_bucket and counts And limits reset after their respective windows without manual intervention
Adaptive CAPTCHA Challenge on Abuse Signals
Given abuse signals such as high-velocity requests from the same IP/device, known bad ASN, forwarded-link access, or device-binding mismatch When the user taps "Send a new link" Then the user is presented with a CAPTCHA challenge And upon successful completion a fresh link is dispatched and analytics record captcha_pass And upon failure no link is sent, a retry is allowed after 30 seconds, and analytics record captcha_fail And the challenge is keyboard-navigable, screen-reader labeled, and has an accessible fallback
Protective Lockout on Sustained Abuse
Given a user/device/IP triggers 3 failed CAPTCHAs or 2 rate-limit violations within 10 minutes (configurable) When another resend is attempted within the lockout window Then the system denies the request, shows a non-enumerating lockout message with remaining time, and emits lockout_applied And no link is generated or sent until the lockout expires or an admin lift occurs And all events are logged with correlation IDs
Session Context Preservation on Fresh Link
Given a user had selected a class, timeslot, add-ons, promo code, and locale before needing a fresh link When the user opens the fresh link on the same device within its validity window Then the app restores the exact session context (class, timeslot, cart, promo, locale) without re-selection And if the timeslot is no longer available, the user sees a clear notice and is offered the smart waitlist with one tap And domain lock and device binding remain enforced And prior partial checkout state (including applied taxes and fees) is preserved
Analytics and Conversion Tracking for Resend
Given analytics is enabled When a resend is requested, dispatched, clicked, and results in successful authentication within 15 minutes Then the system emits: resend_requested, resend_dispatched, link_clicked, session_resumed, conversion_complete And each event includes channel, masked destination, device type, latency metrics, reason codes, and abuse flags without PII leakage And events are delivered to the analytics pipeline within 5 seconds at p95 and deduplicated via event IDs And an abuse_detected event is emitted on rate limit, CAPTCHA, or lockout triggers
LinkShield Policy & Analytics Console
"As a studio owner, I want to configure link security policies and monitor their impact so that I can balance security with conversion for my clients."
Description

Offer admin controls to configure link TTL, one-use enforcement, device-binding sensitivity, domain allowlist, and forwarding risk thresholds per workspace. Provide dashboards for redemption success rate, expiry rate, auto-reissues, suspected abuse, and conversion impact. Expose exportable audit logs and webhooks for security events to integrate with third-party SIEMs. Role-based access controls ensure only authorized admins can modify policies.

Acceptance Criteria
Workspace-level TTL Configuration Enforcement
Given I am a Workspace Admin on Workspace A And the current Magic Link TTL policy is 60 minutes When I change Magic Link TTL to 15 minutes and click Save Then the new policy is persisted and versioned with actor and timestamp in the audit log And all magic links issued in Workspace A after save have expires_at within 15 minutes ±30 seconds of issuance And redemption attempts after expiry return HTTP 410 with error_code "link_expired" And the Expiry Rate metric reflects these expirations within 5 minutes
One-Use Token Enforcement and Auto-Reissue
Given One-Use Enforcement is enabled for Workspace A And Auto-Reissue on reuse is enabled When a user redeems a valid magic link once Then the token is marked consumed and cannot be redeemed again And a second redemption attempt returns HTTP 409 with error_code "already_used" And a fresh link is auto-issued to the original recipient within 30 seconds And the Auto-Reissues metric increments by 1 and is visible within 5 minutes
Device-Binding Sensitivity Policy Enforcement
Given Device Binding Sensitivity is set to High for Workspace A When a magic link issued to Device X is redeemed from Device Y that differs in device_id and user_agent Then redemption is blocked with HTTP 403 and error_code "device_mismatch" And the event is counted as "suspected_abuse" with reason "device_binding" in analytics within 5 minutes And the denial and actorless redemption attempt are recorded in the audit log with risk signals
Redirect Domain Allowlist and Forwarding Risk Enforcement
Given Domain Allowlist for Workspace A contains ["classnest.app", "studio.example"] And Forwarding Risk Threshold is set to Medium (score ≥ 60) When a magic link includes redirect_uri "https://evil.example" Then the redirect is blocked and the user remains on the default success page with message_code "redirect_blocked" And the attempt is logged with outcome "redirect_denied" When a magic link is opened from an IP and user agent inconsistent with the original recipient yielding risk score 65 Then the original token is invalidated, a new link is auto-issued to the recipient, and the attempt is logged as "forwarding_risk" And Auto-Reissues and Suspected Abuse metrics reflect these events within 5 minutes
Analytics Dashboard Metrics, Filters, and Conversion Impact
Given redemption, expiry, auto-reissue, and abuse events exist for the last 30 days When an authorized admin opens Analytics and sets Workspace = A and Date Range = Last 7 Days Then the dashboard displays Redemption Success Rate, Expiry Rate, Auto-Reissues, Suspected Abuse, and Conversion Impact with counts and percentages And success + expired + blocked + reissued equals total redemption attempts for the selected period And exporting the dashboard as CSV reproduces the on-screen numbers within 0.1% And metric tiles and charts load within 4 seconds at p95 for datasets ≤ 100k events
Audit Logs Export and Security Webhooks Delivery
Given audit events exist across policy changes and link lifecycle When an admin requests an export for a specific time range Then a downloadable CSV and NDJSON are produced containing: timestamp, workspace_id, actor_id, actor_role, event_type, policy_version, token_id (when applicable), ip, user_agent, outcome, metadata And the export includes policy change, link created, redeemed, expired, auto-reissued, blocked, and RBAC events When the admin registers a webhook with endpoint URL and signing secret Then subsequent security events are POSTed within 10 seconds with an HMAC-SHA256 signature header And on 5xx responses the delivery is retried up to 5 times with exponential backoff and visible delivery status And admins can manually replay failed deliveries from the console
Role-Based Access Control for Policy Changes and Data Access
Given roles Owner, Admin, and Analyst (view-only) exist for a workspace When an Analyst attempts to modify LinkShield policies or webhook settings Then the action is blocked with HTTP 403 and no changes are persisted When an Owner or Admin modifies a policy Then the change succeeds and is recorded in the audit log with actor, role, timestamp, and new policy version And Analysts can view dashboards and read-only audit logs but cannot export or create webhooks And only Owners and Admins can export audit logs and manage webhooks
Signed Tokens and Key Rotation
"As a security-conscious operator, I want magic links to be strongly signed with rotating keys so that token tampering or leakage cannot be used to access my customers’ accounts or bookings."
Description

Issue cryptographically signed, tamper-evident tokens (e.g., JWS/JWT or HMAC) managed by KMS with automated key rotation and emergency revocation. Include nonce, issued-at, expiry, and intended action scopes to prevent misuse across flows. Implement idempotent redemption endpoints and strict replay protection with low-latency lookups. Provide health checks and alarms for signing/verification errors to ensure high availability of one-tap authentication.

Acceptance Criteria
Issue Signed Token with Required Claims and Short Expiry
Given a request to generate a one-tap magic link for a specific action When a token is issued Then the token is cryptographically signed via KMS using an approved algorithm and includes a header with alg and kid And the payload includes jti (nonce), iat, exp, and scope that maps to the intended action And exp is set to a value ≤ 10 minutes from iat (default 5 minutes) with a clock skew tolerance of ±60 seconds And the token is URL-safe and its signature verifies against the active public key for the specified kid And any tampering of payload or header causes verification to fail with 401
Verify Signature and Enforce Intended Action Scope
Given a token is presented to a redemption endpoint When signature verification succeeds Then the endpoint enforces exact match between token scope (and optional audience) and the endpoint action And a scope or audience mismatch returns 403 with no side effects And a match proceeds and only the permitted action is executed
Idempotent Redemption and Strict Replay Protection
Given a valid token with jti is redeemed When the first successful redemption occurs Then the jti is stored with a TTL equal to the token's remaining lifetime and marked consumed And any subsequent redemption attempts with the same jti return 410 Gone (or 409 Conflict) with no side effects And concurrent redemption attempts result in exactly one success and all others rejected, under 1000 rps with ≤0.1% contention anomalies And if the initial success response is lost, a client retry within 5 minutes returns the original success metadata without duplicating side effects
Automated Key Rotation with Zero-Downtime Verification
Given the rotation schedule triggers or a manual rotation is invoked When a new signing key is created and set Active Then issuance immediately uses the new key and publishes its kid to verifiers within 60 seconds And verifiers accept the previous key for verification during a configurable grace window (default 24 hours) And rotation completes with <0.1% increase in verification failures and zero unhandled 5xx due to key mismatch And no tokens are signed with a deprecated key after cutover
Emergency Key Revocation and Rapid Invalidation
Given a security event requires revoking a kid When an authorized admin revokes the key Then issuance stops using the revoked key immediately and switches to a new key within 60 seconds And verifiers reject tokens signed with the revoked kid within 120 seconds And an alert is emitted within 2 minutes with context (kid, reason, impact) And audit logs record the revocation event with actor, timestamp, and affected kid
Low-Latency Verification and Lookup Performance
Given normal peak traffic (e.g., 200 RPS redemption per region) When verifying tokens and performing jti lookups Then verification p95 latency is ≤ 30 ms and p99 ≤ 60 ms And jti store read/write p95 latency is ≤ 15 ms and p99 ≤ 40 ms And end-to-end redemption p95 is ≤ 150 ms and p99 ≤ 300 ms And these SLOs are met for 99% of 5-minute windows in a 30-day period
Health Checks, Metrics, and Alerting for Signing/Verification
Given the service is running When the health endpoint /health/crypto is queried Then it returns HTTP 200 with status for KMS connectivity, active kid, key age, rotation schedule, verification canary, and error rates And metrics are emitted for sign/verify success and failure rates, latencies, and KMS errors with tags {kid, scope} And alerts trigger when verify error rate > 0.5% over 5 minutes, sign error rate > 0.2% over 5 minutes, or KMS latency p95 > 200 ms over 5 minutes And dashboards display current active kid, next rotation ETA, and last revocation time

Scope Invites

Create time-boxed, role-scoped invites in seconds. Prebuilt templates (Front Desk, Instructor, Accountant) set permissions, data access, and allowed devices; optional shift windows auto-revoke after events. Onboard helpers fast without over-permissioning, backed by a full audit trail.

Requirements

Prebuilt Role Templates
"As a studio owner, I want to assign prebuilt role templates to helpers so that I can onboard them quickly without granting unnecessary access."
Description

Provide a library of editable, pre-configured role templates (Front Desk, Instructor, Accountant) that define default permissions, data visibility (e.g., mask customer PII), and allowed actions across ClassNest modules such as bookings, rosters, customer profiles, payments, and reports. Admins can apply a template in one step, preview effective access before sending, and customize per studio needs (add/remove capabilities, restrict to locations or classes). Templates are versioned, exportable/importable between accounts, and include sensible mobile-first defaults to speed onboarding while minimizing over-permissioning. The system persists template assignments to invites and enforces them consistently across API and UI layers, with compatibility for future roles and custom permissions.

Acceptance Criteria
Prebuilt Templates Library Availability and Defaults
Given an Admin is on the Role Templates Library page When the page loads Then the library lists at least three prebuilt templates named Front Desk, Instructor, and Accountant with human-readable descriptions and current version numbers And each template shows its default enabled modules/actions across bookings, rosters, customer profiles, payments, and reports And non-essential/destructive actions (e.g., delete payments, export full customer lists) are disabled by default for Front Desk and Instructor And default data visibility for non-admin templates masks PII fields (email, phone, full address, full card PAN) in customer profiles and reports And defaults are executable from the mobile web app without requiring desktop-only screens (no action requires a desktop-only page)
One-Step Apply Template to Invite
Given an Admin has entered an invitee email and selected a prebuilt template When the Admin clicks Apply Template Then the invite record displays the assigned template name and pinned version And the assignment persists after page refresh and in backend storage And an audit event is recorded with actor, timestamp, invitee email, template ID, and version And no additional configuration is required to complete the invite (one-step application)
Preview Effective Access Before Sending Invite
Given an Admin has selected a template and optional scope restrictions (locations/classes) When the Admin clicks Preview Access Then a summary panel renders allowed modules, allowed actions, data visibility rules (including which PII fields are masked), and scope (locations/classes) And an API parity check shows the list of permitted API endpoints/verbs corresponding to the same access And the preview reflects the current selections (including any customizations) within 1 second And if conflicting selections exist (e.g., payments allowed but reports denied), the preview displays a validation warning explaining the conflict
Per-Invite Customization and Scope Restrictions
Given an Admin has chosen a base template for an invite When the Admin adds/removes specific capabilities and restricts access to one or more locations or class types Then the effective permissions update immediately in the UI and in the preview And saving the invite stores the customization as an overlay without mutating the base template And a Reset to Template Defaults action restores the base template settings And validation prevents selecting capabilities outside the account’s global constraints
Template Enforcement Across UI and API (Permissions and PII)
Given a user accepts an invite governed by a template When the user attempts actions across bookings, rosters, customer profiles, payments, and reports in the UI Then only actions granted by the template succeed and all others are blocked with a 403-equivalent error message And when calling the corresponding API endpoints with the user’s token, only permitted endpoints/verbs succeed and others return HTTP 403 And where the template specifies PII masking, the UI displays masked values and the API omits or redacts those fields; attempts to access unmasked PII are denied And all allowed/denied events are captured in the audit log with user, resource, action, result, and timestamp
Template Versioning, Pinning, and Migration
Given a template v1 is in use by active invites When an Admin edits the template and publishes v2 Then v2 is created with a changelog and unique version identifier And new invites default to v2 while existing invites remain pinned to v1 And an Admin can migrate selected invites from v1 to v2 via a guided flow with impact summary And audit logs record author, source version, target version, affected invites, and timestamp And historical audits resolve permissions based on the version active at the time of each event
Export/Import Templates Between Accounts with Validation
Given an Admin exports selected templates When the exported file is imported into a different ClassNest account Then the system validates schema, module/action mappings, and version identifiers And templates are deduplicated by stable ID; identical versions are skipped with a notice And unknown or future capabilities are preserved as custom permissions and flagged with warnings without blocking import And a results report lists imported, skipped, and failed templates with reasons
Time-Boxed Access Windows
"As a studio owner, I want to specify when an invite is valid so that access is automatically restricted to the helper’s shift times or a specific event."
Description

Enable invite creators to set explicit validity windows via start/end times and optional event-based triggers (e.g., valid only during the 30 minutes before and after a specific class or through a defined shift). Invites are timezone-aware, handle daylight saving time, and display a live countdown to the helper. Outside the window, access is automatically denied and sessions gracefully signed out. The system supports single-use or multi-day windows, with policy options such as grace periods and minimum/maximum durations. Auto-revocation is scheduled at window end or upon event completion, and all transitions are logged for compliance.

Acceptance Criteria
Fixed Window Access Enforcement
Given an invite with start time T1 and end time T2 in org timezone Z When the helper attempts to sign in before T1 Then access is denied with message "Access begins at {local T1}" and a 403 is logged When the helper attempts to sign in between T1 and T2 Then access is granted and the session token TTL does not exceed T2 + grace And an auto-revocation job is scheduled for T2 + grace When the window spans multiple days Then access remains available continuously between T1 and T2 across date boundaries When the clock reaches T2 + grace while the helper is active Then the session is gracefully signed out within 5 seconds and access is denied on refresh And a revocation event is logged with reason "window-ended"
Event-Relative Access Window (Class-Based)
Given an invite scoped to Event E with rule "valid 30m before until 30m after" And E is scheduled from tsStart to tsEnd When the helper attempts sign-in at tsStart - 29m Then access is granted and logged with reason "within-window" When the helper attempts sign-in at tsEnd + 31m Then access is denied with reason "outside-window" When E ends early at tsEndEarly Then auto-revocation reschedules to tsEndEarly + 30m When E is canceled before start Then the invite is invalidated immediately and any scheduled jobs are canceled, and access attempts are denied with reason "event-canceled"
Timezone and DST-Accurate Windows
Given org timezone Z = America/Los_Angeles and helper local timezone H = America/New_York And a window set 08:00–12:00 Z on 2025-11-02 (DST end) When the system computes the window in UTC Then the effective duration is exactly 4 hours in Z regardless of DST shift And helper-facing times are shown in H with correct offsets When a DST transition occurs during the window Then countdown, access checks, and sign-out honor the intended wall-clock interval without gaining/losing an hour And audit logs include both UTC and Z timestamps for each transition
Live Countdown Display and Boundary Behavior
Given an active session within a window ending at T2 When the helper views the banner Then a countdown displays remaining time in mm:ss with accuracy ±1s and updates at 1s intervals When remaining time ≤ 60s Then the banner highlights "ending soon" and shows a non-dismissable warning When the device clock is skewed by > 2 minutes Then the countdown uses server time with a sync indicator When remaining time reaches 0 Then the user is logged out within 5 seconds and action CTAs are disabled When the client is offline Then the countdown continues from last server sync and reconciles on reconnect without extending access beyond T2 + grace
Single-Use Invite Redemption Controls
Given an invite marked single-use with window T1..T2 When Helper A redeems and signs in at T1 + 1m Then the invite is bound to Helper A’s account and device fingerprint When any other account or device attempts to use the invite during T1..T2 Then access is denied with reason "single-use-consumed" When Helper A logs out and signs in again within T1..T2 Then access is permitted only via the bound account and device When T2 + grace elapses Then the invite cannot be reused and surfaces status "expired" on subsequent attempts
Policy Enforcement: Grace Periods and Min/Max Duration
Given org policy: graceAfter = 5m, minDuration = 15m, maxDuration = 14d When an invite creator sets window duration to 10m Then creation is blocked with validation error "minimum duration is 15 minutes" When an invite creator sets window duration to 30d Then creation is blocked with validation error "maximum duration is 14 days" When a valid window of 1h is created Then new sign-ins are denied after T2, but existing sessions sign out at T2 + 5m And API and UI validations return consistent error codes and messages
Comprehensive Audit Trail of Access Window Transitions
Given any invite lifecycle event (create, update, grant, deny, revoke, expire) When the event occurs Then an immutable log entry is recorded within 500ms including: inviteId, actorId, action, reasonCode, timestampUTC, timestampOrgTZ, previousState, newState, requestId, ip/device metadata When exporting logs for a date range Then results include all entries and are filterable by inviteId and actorId When a sign-out occurs due to window end Then an audit entry "revoke:window-ended" is present and correlated to the sessionId
Invite Delivery & Acceptance Flow
"As an owner, I want to send a link a helper can tap on their phone to accept and set up access so that onboarding takes minutes without manual setup."
Description

Generate secure, single-use invite links that can be sent via SMS and/or email with configurable expiration. The acceptance flow is mobile-first and guides the helper through identity verification, optional MFA setup, agreement to studio policies, and device registration if required. It supports both new and existing ClassNest accounts, preventing duplicate accounts by linking to an existing user when applicable. Admins can track invite status (pending, delivered, accepted, expired, revoked), resend or invalidate invites, and receive notifications on acceptance or failure. The system enforces rate limits, token signing and rotation, and localized content for a smooth, secure onboarding experience.

Acceptance Criteria
Invite Link Creation & Configurable Expiration
Given an admin sets an expiration of 72 hours and selects a role scope with optional device registration, When the invite is created, Then the system generates a unique, single-use token signed with the currently active key and stores an immutable expiration timestamp. Given signing key rotation occurs, When a new invite is created, Then it is signed with the new key; When an invite signed with the previous active key is redeemed before expiration and before the key is retired, Then redemption succeeds; When the previous key is marked retired, Then tokens signed by it are rejected. Given an invite token has not been redeemed, When the same token is presented more than once, Then only the first redemption succeeds and subsequent attempts are blocked and logged.
Multi-Channel Delivery & Rate Limiting
Given SMS and Email channels are selected and localized templates exist, When the invite is created, Then SMS and Email are dispatched within 5 seconds per channel and per-channel status (queued, sent, failed) with provider message IDs is recorded. Given rate limits are configured to 5 invites per minute per admin and 2 invites per hour per recipient per channel, When attempts exceed these thresholds, Then the system returns 429 Too Many Requests, blocks further sends until the window resets, and records an audit event; Then no provider calls are made beyond the limit. Given a transient provider error occurs, When retries are configured to 2 with exponential backoff, Then retries execute and final status is accurate; Given a permanent error, Then no retries occur and the status is failed. Given the invite locale is set to es-MX, When messages are sent, Then content renders in es-MX with studio timezone formatting; Given an unsupported locale, Then content falls back to en-US.
Mobile-First Acceptance — New User Onboarding
Given a recipient without a ClassNest account opens the invite link on a mobile device, When they begin acceptance, Then identity is verified via OTP to the invite's email or phone and the OTP expires in 10 minutes with a maximum of 5 attempts. Given identity is verified, When the studio requires MFA, Then the user must set up MFA (TOTP or SMS) before proceeding; When MFA is optional, Then the user can skip and return later; Then recovery codes are shown once and stored securely. Given studio policies are attached, When the user continues, Then they must view and accept each policy version; Then a timestamped consent record with policy version IDs is stored. Given device registration is required, When acceptance completes, Then the current device is registered and named; If max devices are exceeded, Then the user must remove an existing device to proceed. Then the account is created, linked to the studio with the invite's role, the invite is marked accepted, and the token is invalidated. Then all acceptance screens render correctly between 320–414 px widths and form controls meet WCAG 2.1 AA for labels and contrast.
Acceptance — Existing Account Linking & Duplicate Prevention
Given the invite email or phone matches an existing ClassNest user, When the recipient opens the link, Then they are required to authenticate as that user and are not permitted to create a new account with the same identifier. Given the user authenticates as the matching account, When acceptance proceeds, Then the studio role and permissions from the invite are attached to the existing user and no duplicate account is created. Given the user signs in with a non-matching account, When mismatch is detected, Then acceptance is blocked and the user is prompted to switch accounts or contact the studio admin. Then the invite status becomes accepted, the token is invalidated, and the audit trail records the linkage with user ID and role.
Invite Lifecycle Tracking, Admin Actions, and Audit Trail
Given an invite is created, Then its status initializes to pending. When at least one delivery channel reports success, Then status updates to delivered. When the link is redeemed successfully, Then status updates to accepted. When the expiration timestamp passes without redemption, Then status updates to expired. When an admin invalidates an unaccepted invite, Then status updates to revoked. Given an invite is pending or delivered, When the admin clicks Resend for selected channels, Then messages are resent using the same token and each attempt is logged with channel, timestamp, and outcome. Given an invite is not yet accepted, When the admin clicks Invalidate, Then the token becomes unusable immediately and status becomes revoked. Then all state changes, deliveries, and admin actions are recorded in an immutable audit log with actor, timestamp, IP, and reason.
Security — Expiration, Revocation, Single-Use, and Token Integrity
Given an invite is expired or revoked, When its link is opened or redemption is attempted, Then the flow is blocked with a localized message and the API returns 410 Gone for expired or 403 Forbidden for revoked. Given a token is malformed or tampered, When verification runs, Then the API returns 401 Unauthorized and no invite metadata is disclosed. Given a token has already been redeemed, When a subsequent redemption is attempted, Then the API returns 409 Conflict and no side effects occur. Given signing key rotation occurs, When tokens signed with inactive keys are presented, Then verification fails per policy and a security incident is logged.
Notifications on Acceptance or Failure
Given studio notification preferences enable email and in-app alerts for invite outcomes, When an invite is accepted, Then the studio owner and invite creator receive a notification within 30 seconds containing recipient identity, role, and timestamp. Given a final delivery failure or acceptance failure occurs, When the outcome is determined, Then configured recipients receive a failure notification including the reason code and recommended next actions (e.g., resend or update contact info). Given multiple notifications are triggered for the same invite within 5 minutes, When notifications are dispatched, Then duplicates are suppressed and a single consolidated notification is sent; Then notification logs capture send status and any retries.
Device Restrictions & Binding
"As an owner, I want to limit a helper’s access to specific devices so that logins from unknown devices are blocked."
Description

Allow admins to constrain invite usage to specific device types and a limited set of registered devices. On acceptance, the helper can register a device, which is bound to their invite via a secure token and device fingerprint. Policies can require the same device for subsequent logins, limit the number of devices, and optionally restrict by platform (iOS/Android/Web). Admins can view and revoke registered devices at any time, forcing re-authentication on access attempts from unrecognized devices. The enforcement layer integrates with session management and authentication to block access from disallowed devices, improving account security for short-term helpers.

Acceptance Criteria
Invite Acceptance: First Device Registration and Binding
Given an active Scope Invite with device binding enabled and no device yet registered for the invitee When the helper accepts the invite and completes authentication on a device Then the system generates a unique device ID from a stable device fingerprint and stores it with the invitee record And issues a device-bound authentication token tied to that device ID And marks the device as Registered with timestamp, platform, OS version, and browser/app version And writes an audit log entry with actor=helper, action=device_registered, inviteId, deviceId, ip, userAgent
Same-Device Policy Enforcement on Login
Given the invite's policy requires same-device-only access and a device is already registered When the helper logs in from the registered device Then access is granted and a session is established And the audit log records actor=helper, action=login_success, deviceId When the helper attempts to log in from a different device Then access is denied before session creation with error code DEVICES_NOT_ALLOWED and a user-facing message explaining the same-device policy And the audit log records actor=helper, action=login_blocked_device_mismatch, attemptedDeviceId
Maximum Device Limit Enforcement
Given the invite's policy allows a maximum of N registered devices and the helper currently has M < N registered devices When the helper attempts to register a new device Then the registration succeeds and total registered devices equals M+1 And an audit log entry action=device_registered is recorded Given the helper has N registered devices When the helper attempts to register an additional device Then registration is blocked with error code DEVICE_LIMIT_REACHED and instructions to contact an admin or remove a device And no new device record is created And the audit log records action=device_registration_blocked_limit
Platform Restriction Enforcement (iOS/Android/Web)
Given the invite's policy allows only a specified platform set (e.g., iOS and Web) When the helper accesses ClassNest from an allowed platform Then access is permitted subject to other policies And audit logs record action=platform_allowed with platform When the helper accesses from a disallowed platform Then access is blocked pre-auth with error code PLATFORM_NOT_ALLOWED and a user-facing message listing allowed platforms And the audit log records action=platform_blocked with attemptedPlatform
Admin Device Management: View and Revoke Registered Devices
Given an admin with permission to manage Scope Invites When the admin opens a helper's Devices tab Then the admin can see a paginated list of registered devices including deviceId, platform, lastSeenAt, registeredAt, status, and revoke control When the admin clicks Revoke on a device Then the device status changes to Revoked and the device can no longer authenticate And any active sessions for that device are invalidated so subsequent requests return 401 Unauthorized And the audit log records actor=admin, action=device_revoked, deviceId, helperId
Unrecognized Device Access Attempt Handling and Messaging
Given device binding is enabled When a helper presents valid user credentials from an unrecognized device (no matching registered deviceId) Then authentication is denied with error code UNRECOGNIZED_DEVICE and guidance to use a registered device or contact the admin And no session is created And the audit log records action=login_blocked_unrecognized_device with attemptedDeviceId
Token Binding and Fingerprint Integrity
Given a device-bound token has been issued to a registered device When that token is exported and reused on a different device with a different fingerprint Then authentication fails due to device fingerprint mismatch and the token is invalidated And the audit log records action=token_binding_violation with originalDeviceId and attemptedDeviceId When the token is reused on the original device with an unchanged fingerprint Then authentication succeeds
Fine-Grained Permission Matrix
"As a studio owner, I want to control exactly what each role can see and do so that helpers only access what they need for their tasks."
Description

Implement a capability-based permission model that scopes access at the action and data level (e.g., view roster, check in attendee, issue refund, view payouts, export reports) and supports row-level and field-level filters (e.g., only classes at Location A, hide customer contact info). Role templates map to this matrix, and admins can override per-invite to restrict to locations, instructors, or events. Permission checks are enforced server-side across all APIs and mirrored in the UI for clarity, with an admin preview mode to simulate access as the invitee. The matrix is extensible to accommodate new ClassNest features without breaking existing roles.

Acceptance Criteria
Server-Side Enforcement Blocks Unauthorized Refunds
- Given an invite without capability 'refund.issue', When the user calls POST /v1/refunds with a valid booking_id, Then the API responds 403 with error_code 'capability_missing' and no refund record is created. - Given the same invite, When the user attempts the same action via any client (UI, direct API, mobile), Then all requests are denied with the same 403 and idempotency keys (if provided) are not consumed. - Given the denied request, Then an audit event is recorded containing actor_id, invite_id, capability 'refund.issue', resource booking_id, decision 'denied', timestamp, and request_id.
UI Mirrors Permissions Contextually
- Given an invite with capability 'roster.view' and without 'reports.export', When viewing a class roster in the web app, Then the 'Export' control is not rendered and there is no alternative UI path to trigger export. - Given an invite where row-level scope excludes the currently viewed resource, When the user navigates to that deep link URL, Then the UI shows a scoped 404 page and the backing API returns 404 with error_code 'out_of_scope'. - Given any page, Then a scope indicator is visible showing active filters from the invite (e.g., Location(s), Instructor(s)) and matches the server-enforced scope.
Location-Scoped Row-Level Access
- Given an invite scoped to location_id = 'A', When calling GET /v1/classes for any date range, Then the response contains only classes whose location_id = 'A' and excludes all others. - Given the same invite, When calling GET /v1/classes/{id} for a class not at location 'A', Then the API responds 404 with error_code 'out_of_scope' and does not reveal the class's existence via metadata. - Given the same invite, When generating a roster export for any class outside location 'A', Then the export endpoint responds 404 and no file is produced. - Given the same invite, When the UI displays totals or counts, Then they reflect only records within location 'A'.
Field-Level Redaction of Customer Contact Info
- Given an invite without capability 'customer.contact.view', When requesting GET /v1/rosters/{class_id}, Then attendee email and phone fields are returned as null or 'REDACTED' and are omitted from CSV exports. - Given the same invite, When the user attempts to search, sort, or filter by a redacted field, Then the UI disables those controls and the API rejects such queries with 400 and error_code 'field_not_permitted'. - Given the same invite, When viewing an attendee detail page, Then contact actions (call, email, SMS) are not shown anywhere in the UI.
Role Template 'Front Desk' Applies Predefined Capabilities
- Given an admin creates an invite using the 'Front Desk' template, Then the invite is populated with capabilities: ['roster.view','attendee.check_in','booking.create'] and explicitly excludes ['refund.issue','payouts.view','reports.export','customer.contact.view'] as defined in the permission matrix. - Given the same invite, When saved without overrides, Then the effective capability set exactly matches the template definition and is retrievable via GET /v1/invites/{id}. - Given the same invite with allowed_devices = 'any' and a shift window 08:00–18:00 local, When the current time is outside the window, Then all API calls return 403 with error_code 'invite_expired' and the UI shows an access expired message.
Per-Invite Overrides Restrict by Instructor
- Given an admin applies the 'Instructor' template and overrides scope.instructors = [instructor_id 'X'], When the invitee lists classes, Then only classes taught by 'X' are returned across API, UI, and exports. - Given the same invitee, When attempting to check in an attendee to a class not taught by 'X', Then the UI disables the check-in action and the API returns 404 with error_code 'out_of_scope'. - Given the same invite, When the admin later adds instructor_id 'Y' to the override, Then access updates within 60 seconds and both 'X' and 'Y' classes become available without requiring re-authentication.
Admin Preview Simulates Invitee Access
- Given an admin clicks 'Preview as invitee' for invite ID 123, When navigating the app, Then the UI displays a persistent 'Previewing invitee 123' banner and hides any admin-only controls. - Given the same preview session, When fetching data via the UI, Then all backing API calls are executed with the invite's scoped permissions and return data identical to what the invitee would receive. - Given the same preview, When performing an allowed action (e.g., 'attendee.check_in'), Then the action succeeds and the audit log records actor_admin_id, preview_invite_id, capability, resource, decision 'allowed', and context 'preview'.
Comprehensive Audit Trail
"As a studio owner, I want a complete audit trail of invites and actions so that I can verify who accessed what and when."
Description

Record an immutable audit log for the full invite lifecycle and subsequent access: who created, modified, sent, accepted, revoked; what permissions and time windows were applied; when tokens were used; and from which device and IP. Surface readable timelines in the admin UI with filters and export, and retain logs per data retention settings. Integrate with alerting to notify owners of risky patterns (e.g., repeated failed acceptances, access attempts outside windows, device changes). All audit events include correlation IDs to trace actions across bookings, payments, and roster management within ClassNest.

Acceptance Criteria
Immutable Audit Trail for Invite Lifecycle
Given a scope invite is created, updated, sent, accepted, or revoked When any lifecycle event occurs Then the system writes an append-only audit entry with fields: event_id, event_type ∈ {created, updated, sent, accepted, revoked}, invite_id, actor_id (nullable), actor_role, permissions_snapshot, time_window_snapshot, timestamp (UTC ISO 8601), source_ip (if available), device_id (if available), correlation_id (UUIDv4) And attempts to update or delete any audit entry via API or UI return 403 Forbidden and no mutation occurs And an additional audit entry with event_type = tamper_attempt is recorded including actor_id and reason
Token Usage and Access Attempt Logging with Device/IP
Given an invite token exists When the token is redeemed or an access attempt is made (success or failure) Then an audit entry is recorded with fields: event_type ∈ {token_redeemed, access_attempt}, outcome ∈ {success, failure}, invite_id, token_id, device_id (or fingerprint), user_agent, source_ip, geo_country (if derivable), timestamp (UTC ISO 8601), correlation_id And subsequent redemption from a new device for the same invite produces event_type = device_change_detected linking previous and new device identifiers
Admin Timeline View with Filters and Pagination
Given an admin opens the Audit Timeline for Scope Invites When they filter by any combination of date range, event_type, actor_id, invite_id, correlation_id, outcome, device_id, and source_ip Then the timeline returns only matching events, sorted by timestamp descending, with 100 events per page and a total count And the API responds within ≤2 seconds for datasets ≤10,000 events And selecting an event opens a details view showing the full stored payload for that event
Audit Log Export (CSV/JSON) Respecting Filters and Retention
Given the admin selects Export on the current filtered timeline When they choose CSV or JSON and confirm Then an export job is created and completes within ≤60 seconds for ≤100,000 events, producing a downloadable file with exactly the filtered result set And the export includes a header/schema row (CSV) or top-level keys (JSON), and a SHA-256 checksum And an audit entry event_type = export_generated is recorded with requestor_id, filter_params, format, row_count
Alerting on Risky Patterns (Failures, Time-Window Violations, Device Change)
Given alerting is enabled for the workspace owner When ≥5 failed acceptances for the same invite occur within 10 minutes Then send a repeated_failed_acceptances alert via email and in-app within 1 minute including invite_id, count, time_window, and correlation_ids When any access attempt occurs outside the invite's allowed time window Then send an access_outside_window high-severity alert immediately and record policy_violation in the audit log When an acceptance or access occurs from a new device_id for the same invite within 24 hours of the last acceptance Then send a device_change medium-severity alert and record device_change_detected in the audit log And identical alerts are deduplicated (no repeat sends) within a 15-minute suppression window
Data Retention Enforcement for Audit Logs
Given a workspace has an audit log retention setting of N days When the scheduled retention job runs Then audit events older than N days are permanently purged and no longer appear in UI, API, or exports And an aggregate audit event retention_purge is recorded with from_timestamp, to_timestamp, and purged_count And changes to N apply prospectively and do not restore previously purged data
Correlation IDs Across Bookings, Payments, and Roster Actions
Given invite-driven actions trigger bookings, payments, or roster changes When those actions occur Then the same correlation_id (UUIDv4) is propagated and present on all related audit events and is filterable in UI and API And the event details view displays linked entities by correlation_id, and exports include correlation_id for each row And correlation_id uniqueness is enforced per flow and collisions are rejected with 409 and retried with a new id
Auto-Revoke & Emergency Pause
"As an owner, I want to automatically revoke or instantly pause access when a shift ends or if there’s an issue so that the account can’t be used beyond its intended window."
Description

Provide automated revocation that terminates invites and active sessions at the end of configured windows or on event completion, plus a one-click emergency pause to immediately disable a specific invite or all invites for a user. Revocation invalidates tokens, ends sessions across devices, and denies further API calls, with confirmation prompts and optional notifications to affected users. Admins can schedule future revokes, bulk-revoke by role or event, and review results in the audit trail. The system ensures consistency even under partial connectivity by validating session state server-side on each privileged action.

Acceptance Criteria
Auto-Revoke at End of Shift Window
Given an invite with a configured shift window ending at time T and the invitee has active sessions on multiple devices When server time reaches T Then all active sessions for that invite are terminated across all devices within 10 seconds And all access and refresh tokens issued for that invite are invalidated within 10 seconds And any API call using those tokens returns HTTP 401 with error_code "INVITE_REVOKED" And subsequent token refresh attempts return HTTP 401 with error_code "TOKEN_INVALIDATED" And an audit trail entry is recorded with action "auto_revoke", cause "window_expired", invite_id, actor "system", affected_sessions_count, and occurred_at timestamp
Auto-Revoke on Event Completion
Given an invite scoped to event E that is configured to auto-revoke on event completion and the invitee has an active session When event E transitions to status "completed" Then the invite is revoked within 10 seconds And all active sessions for that invite are terminated across devices within 10 seconds And any API call using tokens from that invite returns HTTP 401 with error_code "INVITE_REVOKED" And an audit trail entry is recorded with action "auto_revoke", cause "event_completed", invite_id, event_id, actor "system", and occurred_at timestamp
Emergency Pause for Specific Invite
Given an active invite I visible to an admin When the admin clicks "Emergency Pause" on invite I and confirms the prompt Then invite I is set to paused within 5 seconds And all active sessions for invite I are terminated across devices within 10 seconds And any API call using tokens for invite I returns HTTP 403 with error_code "INVITE_PAUSED" And if "Notify affected user" is enabled, the invitee receives an SMS or email notification within 60 seconds using template "invite_paused" And an audit trail entry is recorded with action "pause_invite", actor admin_id, invite_id, notification_sent (true|false), and occurred_at timestamp And when the admin clicks "Resume" on invite I, new sessions can be created but any previously issued tokens remain invalid and token refresh attempts return HTTP 401 with error_code "TOKEN_INVALIDATED"
Emergency Pause All Invites for a User
Given a user U with multiple active invites and sessions across devices When an admin selects "Pause All Invites" for user U and confirms Then all invites for user U transition to paused within 10 seconds And all active sessions across devices for user U are terminated within 10 seconds And subsequent API calls using tokens from any of user U's invites return HTTP 403 with error_code "INVITE_PAUSED" And the system displays a summary with invites_paused count and sessions_terminated count And an audit trail parent entry is recorded with action "pause_user_invites", actor admin_id, user_id, and correlation_id, with child entries per invite
Schedule Future Revoke with Confirmation and Notifications
Given an admin schedules a revoke for invite I at future time T and selects timezone TZ and optional notification When the admin confirms the schedule Then the system stores the schedule in UTC with TZ metadata and displays T converted to TZ in the UI And if the admin cancels before T, no revoke occurs and an audit entry "scheduled_revoke_canceled" is recorded And when server time reaches T, the revoke executes within 10 seconds, terminating sessions, invalidating tokens, and optionally sending notifications within 60 seconds And audit entries are recorded for "scheduled_revoke_created" and "scheduled_revoke_executed" with invite_id, actor (admin_id or system), and occurred_at timestamps
Bulk Revoke by Role or Event with Results Summary
Given an admin selects a role or event filter yielding N invites and initiates bulk revoke and confirms When the bulk revoke job starts Then all matching invites are processed and revoked within 60 seconds for N ≤ 1000 or via background batches for larger N And the operation is idempotent; re-running the same bulk revoke does not change already revoked invites And the system returns a result summary including total_selected, revoked_success, revoked_failed, and failed_invite_ids And failed revoke attempts are retried up to 3 times with exponential backoff And audit trail records a bulk action entry with correlation_id and individual invite entries referencing that correlation_id
Server-Side Validation Blocks Privileged Actions Post-Revoke Under Partial Connectivity
Given a device holds a cached session token for invite I and experiences intermittent connectivity And invite I is revoked or paused while the device is offline When the device reconnects and attempts any privileged API action Then the server validates invite state and denies the request with HTTP 401 and error_code "INVITE_REVOKED" if revoked or HTTP 403 and error_code "INVITE_PAUSED" if paused And no privileged data is returned in the response And server logs show zero successful privileged actions for invite I after the revoke timestamp And subsequent token refresh attempts fail with HTTP 401 and error_code "TOKEN_INVALIDATED"

Phone-to-Desktop

Approve desktop logins from your phone. Scan a QR on the web app or tap the SMS link, and your active mobile session signs the browser in instantly—no passwords or inbox hopping. Ideal for shared studio computers to start classes faster and avoid lockouts.

Requirements

Instant QR Login Handshake
"As an instructor, I want to scan a QR with my phone to sign into the studio computer instantly so that I can start classes on time without typing passwords or checking email."
Description

Generate a short-lived, single-use QR code bound to the requesting desktop browser session. When scanned by an authenticated mobile session (app or mobile web), the phone cryptographically approves the request and the backend establishes a new desktop session without a password. The QR token expires quickly (e.g., 60 seconds), is invalidated on use or refresh, and cannot be replayed. The flow uses a secure channel (WebSocket or long-poll) to update the desktop UI in real time on approval/denial/timeout. The resulting desktop session inherits the user’s org context, roles, and least-privilege permissions, and surfaces clear success and error states for instructors using shared studio computers.

Acceptance Criteria
QR Generation: Short-Lived, Single-Use, Session-Bound
Given a desktop browser on the ClassNest login page and the user selects "Approve from Phone" When a QR is generated Then the QR token is unique, opaque, and bound to the requesting desktop session ID and user-agent fingerprint And the token TTL is 60 seconds ± 5 seconds And the QR regenerates on page refresh and immediately invalidates any prior token for that session And no user-identifying data is present in the QR payload
Mobile Approval: Authenticated Scan Creates Desktop Session
Given a user is authenticated on the ClassNest mobile app or mobile web When they scan a valid, unexpired QR and confirm "Approve" Then the server verifies the mobile session and cryptographic approval tied to the QR token And a new desktop session is established for the same user without password entry And the desktop session start time is within 2 seconds of approval And the mobile receives a success confirmation
Real-Time Status Updates and Timeouts
Given a desktop is waiting for approval When approval is granted Then the desktop UI transitions to "Approved — Signing you in" within 2 seconds via WebSocket, or within 5 seconds via long-poll fallback Given a desktop is waiting for approval When the QR token expires after 60 seconds Then the desktop UI shows "QR expired" and presents a "Generate new QR" action Given a desktop is waiting for approval When the mobile explicitly denies the request Then the desktop UI shows "Request denied" and presents "Generate new QR"
Replay and Superseded Token Protection
Given a QR token has been used for an approval or denial When any device attempts to reuse the same token Then the server rejects with "Invalid or used token" and no session is created Given multiple QR codes are generated for the same desktop session via page refresh When an older QR is scanned Then the server rejects it as superseded and keeps the desktop in the waiting state Given a valid QR token is intercepted and altered When a scan attempt is made with a modified token Then the server rejects due to signature or integrity check failure and no session is created
Org Context, Roles, and Least-Privilege Inheritance
Given a user belongs to one or more orgs and is authenticated on mobile When they approve a desktop login Then the new desktop session inherits the same active org context as the mobile session at approval time And the desktop permissions match the user's roles and least-privilege policies And routes and actions outside those permissions are inaccessible and return 403
SMS Link Approval Path and Deep-Link Fallback
Given the user selects "Send me a link instead" on the desktop When they tap the received SMS link on their phone Then the link opens the ClassNest app if installed, otherwise the mobile web approval screen And upon confirming approval, the desktop session is established as in the QR scan flow And all single-use, timeout, and replay-protection rules are enforced identically
SMS One-Tap Approval
"As a busy studio owner, I want a one-tap SMS link to approve desktop login so that I can get into the front desk computer even if the QR camera scan isn’t convenient."
Description

Provide an option on the desktop login screen to "Text me a link" that sends a signed, single-use, time-limited deep link to the user’s verified phone number. Tapping the link opens the mobile app (universal link) or mobile web, verifies the user’s active session, presents an approval prompt showing the requesting browser details, and upon confirmation creates the desktop session. Implement throttling, deliverability tracking, fallback to short code entry if deep linking fails, and regional-compliant SMS templates and opt-in management. Links expire (e.g., 5 minutes) and are invalidated on use or when the user signs out.

Acceptance Criteria
SMS Link Request from Desktop Login
Given a user account with a verified phone number and an active mobile session When the user selects "Text me a link" on the desktop login screen Then an SMS containing a signed, single-use universal deep link is queued and sent to the verified number within 10 seconds And the desktop UI confirms "Link sent" with masked phone digits And deliverability events (queued, sent) are recorded with timestamp, provider message ID, and request correlation ID And if the user lacks a verified phone or active mobile session, no SMS is sent and a non-revealing error is shown with guidance to sign in on mobile
Mobile Approval Prompt and Desktop Session Creation
Given the recipient taps the SMS universal link within 5 minutes and is logged into the mobile app When the app opens the approval prompt Then it displays the requesting browser name and version, OS, approximate location (city), request time, and the ClassNest web domain When the user approves the request Then the desktop session is created and the desktop transitions to the authenticated home within 3 seconds And the used link is invalidated immediately When the user denies the request Then the desktop login remains blocked with a "Request denied" message and the link is invalidated And if the app is not installed, the mobile web opens to the same prompt after verifying the active mobile session
Link Expiration and Single-Use Enforcement
Given a login link was issued When any client attempts to use it after 5 minutes from issuance Then the attempt fails with "Link expired" and no desktop session is created When a link is used once (approve or deny) Then subsequent attempts return "Link invalid" and are audited When the user signs out of the mobile session before approving Then any pending link becomes invalid immediately When a new SMS link is requested Then any prior pending link for that user is invalidated
Throttling and Abuse Prevention
Given repeated requests from the same user or IP When more than 3 SMS links are requested within 10 minutes per user or more than 10 per IP per hour Then further requests are blocked for 10 minutes and a generic rate-limit message is shown on desktop And a CAPTCHA challenge is required before another request is allowed And throttling events are logged with user or IP, timestamp, and counters
SMS Deliverability Tracking and Retry Policy
Given an SMS is queued When the provider callback indicates delivery failure Then the system retries via a secondary route/provider up to 2 times within 2 minutes, respecting throttle rules And delivery status transitions (queued, sent, delivered, failed) are stored with timestamps and are queryable for support And if final failure occurs, the desktop UI displays "We couldn't send a link" and offers alternative options (e.g., try again, short code)
Fallback Short Code Entry Flow
Given the user cannot open the deep link on mobile When they choose the fallback "Get a code" option Then a 6-digit one-time code tied to the pending request is generated and shown on mobile web And the desktop login displays a field to enter the code When the correct code is entered on the originating desktop within 5 minutes Then the desktop session is created and both the code and its parent link are invalidated When 5 incorrect attempts are made Then the flow locks for 10 minutes and counts toward rate limiting And codes are scoped to the originating desktop session; reuse or mismatch returns "Invalid code"
Regional Compliance and SMS Opt-In Management
Given the user's phone number region When composing the SMS message Then the system uses a region-compliant template including brand name (ClassNest), purpose, one-tap link, and required STOP/HELP keywords or sender ID When a user lacks SMS opt-in or has opted out (STOP) Then no SMS is sent, and the desktop UI provides guidance and alternative login options When STOP/UNSTOP/HELP messages are received Then preferences are updated within 60 seconds and logged with region and source for audit export
Active Session Verification and Risk Checks
"As a security-conscious instructor, I want approvals to be verified and risk-checked so that my account isn’t used to sign in on suspicious devices."
Description

Before approving a desktop login, verify that the mobile device holds a recent, valid session and perform contextual risk checks (geo/IP distance from the requesting browser, device fingerprint changes, time anomalies, and org policy). On elevated risk, require a step-up challenge (e.g., MFA) or block with a clear reason. Bind approvals to the device’s stored key/material, record the approval intent with nonce and origin, and prevent cross-tenant approvals. Provide user-facing prompts with recognizable browser/device metadata for informed consent.

Acceptance Criteria
Approve Desktop Login with Recent Valid Mobile Session
Given the mobile device has an active ClassNest session that is not expired or revoked And the last authenticated or refreshed activity occurred within the past 10 minutes And the device’s signing key is present and accessible When the user scans the QR code or taps the SMS approval link Then the desktop browser is signed in under the same account within 5 seconds And the approval outcome is persisted with timestamp and request ID
Block When Mobile Session Is Invalid or Stale
Given the mobile session is expired, revoked, or the last activity was more than 10 minutes ago When an approval is attempted from the mobile device Then the desktop login is blocked and no session tokens are issued And the user sees “Approval blocked: session not valid or recent” on both devices
Elevated Risk Assessment and Outcome Handling
Given the desktop IP geolocation is more than 500 km from the mobile IP within a 10‑minute window, or the requesting browser fingerprint changed by 3 or more core attributes, or the client clock skew exceeds 5 minutes When the approval request is evaluated Then the request is marked Elevated Risk with a computed risk reason And if org policy is “require MFA on elevated risk,” a step‑up MFA challenge is presented on mobile and must pass before sign‑in proceeds And if org policy is “block on elevated risk,” the request is blocked with the specific reason shown to the user
Cryptographic Binding with Device Key and Signed Nonce
Given the web app presents a one‑time nonce and origin for the approval request And the mobile device holds the account’s stored private key material When the user approves the login on mobile Then the mobile signs the nonce and origin with the device key and sends the signature to the server And the server verifies the signature, nonce freshness (<= 60 seconds), and origin matches the expected domain And if any verification fails, the desktop login is denied with reason “signature/origin invalid”
Prevent Cross‑Tenant Approvals
Given the mobile session belongs to tenant/org A And the desktop approval request targets tenant/org B where A ≠ B When the user attempts to approve Then the request is blocked with reason “cross‑tenant mismatch” And no session tokens are issued and no tenant B details are disclosed
User Prompt Shows Recognizable Request Metadata
Given an approval prompt is shown on the mobile device Then it displays the requesting browser name and OS, approximate city/region of the desktop IP, org name, and request time in the user’s local timezone And it shows the desktop device nickname if known, or “Unknown device” And the user can Approve or Deny; Deny cancels the request and notifies the desktop immediately And all displayed metadata values are sourced from server‑verified data
Replay Protection, Single‑Use Nonce, and Rate Limiting
Given each QR/SMS approval request carries a unique nonce and request ID When a nonce is redeemed once or older than 60 seconds Then any subsequent use is rejected as “expired or already used” And the system enforces at most 3 approval attempts per minute per account and per device And all rejected attempts are logged with reason and request ID
Kiosk Mode for Shared Studio Computers
"As a studio manager, I want a kiosk-friendly login and session policy so that staff can rotate on shared computers without exposing sensitive data or getting locked out."
Description

Enable organizations to mark specific browsers/computers as kiosks with policies tailored for shared environments: shorter idle timeouts, quick sign-out, restricted access to sensitive account settings and PII, and optional "remember this kiosk" to streamline re-approvals for a limited window (e.g., 24 hours). Provide one-tap remote sign-out from the phone, visible session banners on the kiosk, and easy user switching between staff. Ensure no sensitive data caches locally and that kiosk indicators are clearly presented.

Acceptance Criteria
Kiosk Enrollment via Phone-to-Desktop with Trust Window
Given an org admin has an active mobile session and opens the web app on a shared computer When they scan the QR or tap the SMS link and select "Approve as Kiosk" Then the desktop browser is marked as a kiosk for that org, the user is signed in without a password, a persistent "Kiosk" indicator is displayed, and an audit log entry is recorded Given the admin enables "Remember this kiosk for 24 hours" during approval When subsequent approvals occur within 24 hours Then sign-in completes with a one-tap confirm on phone (no re-scan, no password/2FA), and the remember window expiration timestamp is shown on the kiosk banner Given the remember window expires or is revoked by an admin When the kiosk attempts a new approval Then the full approval flow is required and the previous remember token is invalidated
Admin-Configurable Kiosk Policies
Given an org admin opens Organization Settings > Kiosk Policies When they configure idle timeout (1–15 minutes), access restrictions (Settings, Billing, Payouts, API Keys, Customer Export), PII masking level, download/print restrictions, and trust window duration (4–48 hours) Then the values can be saved and validated with constraints, and changes are versioned with who/when When policies are saved Then new kiosk sessions apply them immediately and active sessions receive updates within 30 seconds When a configured value is less strict than the platform security baseline Then the stricter baseline is enforced and the admin sees an inline notice explaining the override
Enforced Short Idle Timeout and Auto-Clear
Given a kiosk session is inactive for the configured idle timeout (default 5 minutes) When the timeout elapses Then the user is signed out, all session tokens/cookies/local state are cleared, and the kiosk returns to the QR splash screen within 2 seconds Given the user resumes interaction before timeout When activity is detected Then the timer resets and a visible countdown appears starting at 60 seconds remaining Given background network calls occur without user interaction When no input events are detected Then the idle timer is not reset by background activity
Restriction of Sensitive Settings and PII in Kiosk
Given the app is in kiosk mode When a user navigates to Account Settings, Billing, Payouts, API Keys, Customer Export, or Staff Management Then navigation is blocked server- and client-side, no sensitive data requests are executed, and a non-dismissible "Restricted in Kiosk" notice is shown When viewing attendee/customer lists in kiosk mode Then phone numbers and emails are masked except the last 2 characters/digits, and exports are disabled Given a user attempts to reveal full PII When they request reveal Then it requires a fresh on-phone approval and reveals for a maximum of 10 seconds with audit logging, after which values re-mask automatically
Kiosk Session Banner and Remote Sign-Out
Given kiosk mode is active Then a persistent banner shows "Kiosk", organization name, signed-in staff name, device label, and trust window "Remember until" timestamp; it is visually distinct and cannot be dismissed Given the banner provides a QR/code and URL for remote sign-out When the staff initiates "Sign out kiosk" from their phone via the app/link Then the kiosk session is invalidated within 5 seconds (or queued and applied on reconnect if offline), the user is signed out, the screen returns to QR splash, and an audit log entry is recorded
Fast Staff User Switching with Clean Slate
Given a kiosk has an active session When "Switch User" is selected Then the current user is signed out, all local state is cleared, and the QR approval screen appears within 2 seconds Given the next staff member scans the QR or taps the SMS link When they approve on their phone Then sign-in completes within 3 seconds using their active mobile session, and no prior user data appears in history, forms, or UI Given a kiosk browser When a second staff attempts to sign in concurrently Then the system prevents overlapping staff sessions on the same kiosk
No Local Sensitive Data Persistence
Given kiosk mode is enabled Then password managers/autofill are disabled, downloads of CSV/PDF from restricted pages are blocked, printing of PII pages is prevented, and no PII is stored in localStorage/indexedDB Given any sign-out (idle, remote, manual) or user switch occurs When the kiosk is inspected via storage and cache APIs Then no recoverable tokens, PII payloads, or cached API responses are present; a forced reload while offline does not expose prior data Given navigation using the browser back/forward buttons When returning to previously viewed PII routes Then the app prevents BFCache usage for those routes and re-fetch is blocked in kiosk mode, showing masked placeholders
Audit Logging and Security Notifications
"As an org admin, I want a clear audit trail and alerts for login approvals so that I can monitor access and respond quickly to suspicious activity."
Description

Record immutable audit events for all Phone-to-Desktop actions: QR generated, approval requested, approved/denied/expired, method (QR/SMS), device/browser metadata, IPs, and user/organization IDs. Expose an admin view and export for compliance (CSV/JSON) with retention controls. Notify users of unusual approvals and provide a mobile interface to view and revoke active desktop sessions. Ensure logs are tamper-evident and time-synchronized.

Acceptance Criteria
Audit Event Coverage and Field Completeness
Given a user initiates any Phone-to-Desktop action (QR generated, approval requested, approved, denied, expired) When the action occurs Then an audit event is written capturing at minimum: event_type, method (QR/SMS), user_id, organization_id, session_id, related_mobile_session_id, desktop_session_id (if applicable), device/browser metadata (UA string, device model, OS, browser version), IP address, geolocation lookup (country, region, city if available), request_id/correlation_id, and ISO-8601 UTC server_timestamp. Given a flow from QR generation through approval or expiration When the flow completes Then each step has a unique event_id and all events share a common correlation_id for traceability. Given a denied or expired approval When the event is logged Then outcome field equals "denied" or "expired" and no desktop session is activated. Given a multi-tenant environment When events are stored Then they are scoped to organization_id and are not accessible across organizations.
Immutable Tamper-Evident Audit Log
Given any attempt to insert, update, or delete audit records outside the append-only interface When such an action is attempted Then the operation is rejected and an admin-alert event is recorded. Given audit events are stored When they are written Then each record includes a content_hash and hash_chain_prev forming a verifiable hash chain per organization and per day. Given a verification job runs daily When it validates the hash chain Then it reports "Pass" if all sequences are intact, otherwise "Fail" with the first inconsistent event_id. Given read API access When an event is retrieved Then it includes a verification_status field computed on read (valid/invalid).
Time Synchronization and Timestamp Accuracy
Given the system NTP service is healthy When an audit event is written Then the timestamp is ISO-8601 with Z suffix, synchronized to UTC, and within ±1 second of NTP time. Given client device clocks may drift When both client and server timestamps are captured Then server_timestamp is authoritative and client_timestamp (if present) is included as a separate field. Given a sequence of events for one correlation_id When sorted by server_timestamp Then event ordering reflects the actual processing order or includes a monotonically increasing sequence_number to disambiguate. Given NTP drift greater than 1 second is detected When detected Then a health alert is raised and audit writes continue with a drift_ms field populated.
Admin Audit Console: Access, Filter, and Detail
Given an organization admin with Audit_Read permission When they open the Audit view Then they can see audit events only for their organization. Given events exist When the admin filters by date range, event_type, user, method, IP, device, or outcome Then results refresh within 2 seconds and show the first 100 items with pagination. Given an event row When clicked Then a detail panel shows all captured fields including hash verification result and correlation thread navigation. Given a non-admin or user without permission When they attempt access Then they receive a 403 and no data is leaked.
Compliance Export: CSV and JSON
Given an admin selects an export with filters and fields When requesting CSV Then the file is generated with header row, UTF-8 encoding, comma delimiter, quoted fields, and delivered within 60 seconds for up to 100k rows. Given an admin requests JSON export When generated Then it is a newline-delimited JSON (ndjson) stream where each line is a single event object. Given an export is generated When downloaded Then a corresponding audit event "audit_export_created" is logged with filter summary and requester user_id. Given PII policy rules When exporting Then fields marked sensitive respect org-level export settings (full/redacted/omitted).
Retention Controls and Legal Hold
Given a default retention policy of 365 days Unless an org config overrides it When events exceed retention Then they are hard-deleted and hash chain continuity records deletion markers without breaking verification. Given an admin updates retention settings (90–1825 days) When saved Then the new policy applies prospectively and is auditable. Given a legal hold is placed on a user or correlation_id When active Then matching events are excluded from deletion until the hold is cleared. Given a scheduled deletion job runs When completed Then a summary report is logged and visible in the admin console.
Unusual Approval Notifications and Mobile Session Control
Given an approval occurs from a new device, new browser, new IP/geo, outside 06:00–22:00 local, or impossible travel (>500 km within 30 min) When detected Then the user receives a push or SMS within 30 seconds containing device, browser, IP/geo, time, and a one-tap "Review Sessions" link. Given the user opens Review Sessions on mobile When viewing Then they see all active desktop sessions with device/browser, IP, location, start time, last activity, and originating method (QR/SMS). Given the user taps Revoke on a session When confirmed Then the desktop session is invalidated within 5 seconds, the user is notified of success, and an audit event "desktop_session_revoked" is recorded. Given an unusual approval notification is sent erroneously When the user marks it as Not suspicious Then the risk model suppresses similar alerts for that device for 30 days and logs an "alert_feedback" event.
Rate Limiting and Replay Protection
"As a platform owner, I want strong rate limiting and replay protections so that automated abuse and token reuse cannot compromise user accounts."
Description

Apply layered defenses: per-IP and per-user rate limits for QR generation and approval attempts, SMS send throttles, and automatic invalidation of all pending tokens on sign-out. Enforce single-use nonces, strict expirations, origin checks, SameSite protections, and CSRF mitigation for approval endpoints. Introduce incremental challenges (e.g., CAPTCHA) after thresholds, and instrument metrics to detect abuse patterns. All communication is TLS-encrypted with strict domain validation.

Acceptance Criteria
QR Generation Per-IP Rate Limiting
Given a single client IP, When it requests QR generation more than 10 times within 60 seconds, Then subsequent requests within that window return HTTP 429 with error code "RATE_LIMITED" and a Retry-After header indicating remaining seconds Given the 60-second window elapses, When the same IP requests QR generation, Then the request succeeds (HTTP 200) if under the new window's limit Given multiple users originate from the same IP, When their combined QR generation requests exceed 10 within 60 seconds, Then all further QR generation requests from that IP are rate limited until the window resets
Approval Attempt Per-User Rate Limiting
Given a user account, When approval attempts (QR or SMS link approvals) exceed 5 within 60 seconds across any IP or device, Then further approval attempts return HTTP 429 with error code "RATE_LIMITED_USER" and a Retry-After header Given the user remains under 5 approval attempts within 60 seconds, When an approval is submitted, Then the request is processed normally Given the rate limit window has reset, When the user attempts approval again, Then the request is accepted if under the new window's limit
SMS Send Throttling with Incremental Challenges
Given a user requests SMS approval links, When they request more than 3 SMS within 10 minutes, Then further SMS sends return HTTP 429 with error code "SMS_THROTTLED" and require solving a CAPTCHA challenge to proceed Given the user solves a valid CAPTCHA after throttling, When they request another SMS within the same 10-minute window, Then one additional SMS is allowed and subsequent sends are throttled again until the window resets Given a user attempts to send more than 10 SMS in 24 hours, When additional requests are made, Then they are blocked with HTTP 429 and a 30-minute cooldown regardless of CAPTCHA Given throttling events occur, When the user is notified, Then the UI message does not disclose whether the phone number exists and includes the remaining wait time
Single-Use Nonce with Strict Expiration
Given an approval token (nonce) is issued, When it is used once to approve a desktop login, Then any subsequent reuse of the same nonce is rejected with HTTP 409 and error code "TOKEN_REUSED" Given an approval token is issued at time T, When it is presented after T+120 seconds, Then it is rejected with HTTP 401 and error code "TOKEN_EXPIRED" Given an approval token is bound to a specific mobile session and intended web origin, When it is presented by a different session or mismatched origin, Then it is rejected with HTTP 403 and error code "TOKEN_CONTEXT_MISMATCH"
Automatic Invalidation of Pending Tokens on Sign-Out
Given a user signs out from any device, When sign-out completes, Then all pending QR and SMS approval tokens for that user are revoked within 5 seconds Given a revoked token is presented after sign-out, When an approval attempt occurs, Then it is rejected with HTTP 401 and error code "TOKEN_REVOKED" Given a browser was awaiting approval, When its associated token is revoked, Then the browser is informed within 5 seconds and prompted to initiate a new login flow
Approval Endpoint Origin/CSRF/SameSite Protections over TLS
Given a request to the approval endpoint, When it is not served over HTTPS with a valid certificate for an allowed domain, Then it is rejected before processing with HTTP 400 and no sensitive content is returned; HSTS is enabled with max-age ≥ 15552000 seconds Given a cross-origin request with Origin/Referer not in the allowlist, When it targets the approval endpoint, Then the request is rejected with HTTP 403 and no permissive CORS headers are returned Given a state-changing approval POST, When the CSRF token is missing or invalid, Then the request is rejected with HTTP 403 and error code "CSRF_INVALID" Given session cookies are set by the service, When they are issued, Then they include Secure, HttpOnly, and SameSite=Lax attributes
Abuse Detection Instrumentation and Alerting
Given rate limits, throttles, and token protections are enforced, When any are triggered, Then metrics are emitted for events: qr_rate_limit_hit, user_approval_rate_limit_hit, sms_throttle_hit, token_reused_rejected, token_expired_rejected, token_revoked_rejected, csrf_invalid_rejected, origin_check_failed, captcha_challenge_issued, captcha_solved Given metrics are emitted, When aggregated over 1 minute, Then an alert is fired if a single IP records >100 qr_rate_limit_hit or >50 origin_check_failed, or if a single user records >20 user_approval_rate_limit_hit Given logs are written for these events, When stored, Then they exclude raw tokens and full phone numbers (only last 2 digits) and include hashed user identifiers for correlation
Org Policy Controls and Enrollment
"As an admin, I want to configure how Phone-to-Desktop works for my studio so that it meets our security needs while keeping check-in fast for staff."
Description

Provide organization-level settings to enable/disable Phone-to-Desktop, choose allowed methods (QR, SMS, or both), require MFA for approvals, configure token lifetimes, and set kiosk timeout policies. Include a guided enrollment flow for users to verify phone numbers, install/enable the app for deep links, and grant camera permissions. Restrict policy management to admins, track configuration changes in audit logs, and offer safe defaults aligned with security best practices.

Acceptance Criteria
Admin Toggles Phone-to-Desktop at Organization Level
Given I am an org admin, When I navigate to Admin > Security > Phone-to-Desktop, Then I can enable or disable the feature at the organization level and the saved state persists after page refresh. Given a new organization is created, Then Phone-to-Desktop is Disabled by default. Given the feature is Disabled, When any user attempts a QR or SMS approval, Then the backend rejects with HTTP 403 and error code P2D_DISABLED and the UI shows a policy message. Given the feature state is changed, When I fetch GET /api/p2d/policy, Then the response reflects the new state and an incremented policy version. Given the feature is Enabled, When the org is later set back to Disabled, Then any previously generated approval requests become invalid immediately and return P2D_DISABLED on redemption.
Admin Configures Allowed Approval Methods (QR, SMS, Both)
Given I am an org admin, When I set Allowed Methods to QR only and save, Then QR-based approvals succeed (HTTP 200) and SMS link approvals are blocked with HTTP 403 P2D_METHOD_NOT_ALLOWED. Given Allowed Methods is SMS only, When a QR scan approval is attempted, Then the server returns HTTP 403 P2D_METHOD_NOT_ALLOWED and the web app displays a policy notice. Given Allowed Methods is Both, When QR and SMS approvals are attempted under the same conditions, Then both succeed (HTTP 200). Given a new organization is created, Then Allowed Methods default to QR only. Given Allowed Methods are updated, When I reload the policy UI, Then the previously saved selection is shown and GET /api/p2d/policy matches it.
Require MFA for Approvals
Given Require MFA is enabled at the org level, When a user attempts to approve a desktop login, Then the mobile app prompts for an enrolled second factor and the approval is only issued after successful verification. Given Require MFA is enabled and the user has no MFA enrolled, When they attempt to approve, Then the approval is blocked with HTTP 412 P2D_MFA_REQUIRED and the UI presents a deep link to MFA enrollment. Given Require MFA is disabled, When a user approves, Then no MFA prompt is shown and the approval proceeds if other policies pass. Given Require MFA is enabled, When MFA verification fails 3 times or exceeds 60 seconds, Then the approval fails with HTTP 401 P2D_MFA_FAILED and no desktop session is created. Given a new organization is created, Then Require MFA for approvals is Enabled by default.
Configurable Token Lifetimes
Given Approval Token TTL is set to N minutes (allowed range 1–5, default 2), When an approval token is issued, Then it expires exactly N minutes after issuance and token redemption after expiry returns HTTP 401 P2D_TOKEN_EXPIRED. Given Desktop Session Trust Duration is set to M hours (allowed range 1–24, default 8), When a desktop session is created via Phone-to-Desktop, Then the session is valid for at most M hours or until explicit sign-out, whichever occurs first. Given invalid TTL values are submitted (outside allowed ranges), When I save the policy, Then the UI shows a validation error and the API responds with HTTP 422 VALIDATION_ERROR and no change is persisted. Given an approval QR is generated, When it is scanned after TTL expiry, Then the desktop login fails with HTTP 401 P2D_TOKEN_EXPIRED and a new QR must be generated. Given a new organization is created, Then Approval Token TTL defaults to 2 minutes and Desktop Session Trust Duration defaults to 8 hours.
Kiosk Timeout Policy for Shared Computers
Given Kiosk Mode is enabled and idle timeout is set to T minutes (allowed range 5–30, default 10), When the desktop session is idle for T minutes, Then the user is automatically signed out and the app returns to the QR login screen. Given Kiosk Mode is enabled, When user activity occurs before T minutes elapse, Then the idle timer resets and no auto sign-out occurs. Given Kiosk Mode is disabled, When the desktop is idle beyond T minutes, Then no kiosk-driven sign-out is triggered. Given Kiosk Mode triggers an auto sign-out, Then an audit event kiosk_auto_signout is recorded including orgId, userId, device fingerprint, and UTC timestamp. Given a new organization is created, Then Kiosk Mode is Disabled by default and the idle timeout defaults to 10 minutes when Kiosk Mode is enabled.
Admin-Only Access and Audit Logging of Policy Changes
Given I am a non-admin user, When I attempt to view or modify Phone-to-Desktop policies via UI or API, Then I receive HTTP 403 FORBIDDEN and no changes are saved. Given I am an org admin, When I create or update any Phone-to-Desktop policy, Then an immutable audit record is written capturing adminId, action, changed fields with old and new values, source IP, user agent, and UTC timestamp. Given a policy change is saved, When I view the Audit Log filtered by category "Phone-to-Desktop", Then the entry appears within 5 seconds of save. Given multiple fields are changed in a single save, When I open the audit detail, Then all changed fields and prior values are displayed. Given a new organization is created, Then only admins can access policy endpoints and audit logging for policy changes is enabled by default.
Guided Enrollment Flow for Phone Verification, App/Deep Links, and Camera
Given a user is not enrolled, When they start Phone-to-Desktop enrollment, Then they see a guided flow with steps: Verify Phone, Enable App/Deep Links, Grant Camera, each with a visible completion indicator. Given an SMS OTP is sent for phone verification, When the correct 6-digit code is entered within 5 minutes, Then the phone number is marked verified; after 5 incorrect attempts the flow is rate-limited for 15 minutes and further OTP sends are blocked with HTTP 429. Given deep links are not handled by the device, When the user taps the test deep link, Then the app prompts to install/enable the ClassNest app and does not allow progression until a deep link is successfully handled. Given camera permission is denied or no camera is detected, When reaching the Camera step, Then the flow presents a fallback to use SMS approval and instructions to enable camera permissions; QR method remains unavailable until permission is granted. Given enrollment completes all steps, When the user returns to the desktop login, Then QR scan or SMS link approval works according to the organization's Allowed Methods and Require MFA policies without additional setup.

Kiosk Login

Put a tablet into secure kiosk mode with a one-time magic link from an admin phone. Lock to selected classes and hours, hide admin menus, require a PIN to exit, and auto-sign-out at day’s end. Enables safe self-serve check-in without exposing studio data.

Requirements

Magic Link Kiosk Pairing
"As a studio admin, I want to pair a tablet via a one-time magic link so that I can set up a secure kiosk without entering passwords and limit access to specific classes and hours."
Description

Implements a one-time, time-limited magic link (and QR) flow that lets an admin initiate kiosk setup from a verified mobile device without sharing credentials. The link carries a single-use, short-TTL token scoped to location and preselected classes/hours. When opened on the tablet, the kiosk app validates the token, binds the device to the organization, assigns a least-privilege “kiosk” session, and stores a friendly device name. Security controls include token replay prevention, device fingerprint checks, remote revoke/rotate, and an auditable pairing log. Integrates with ClassNest’s roles/permissions, class scheduling, and location settings to ensure the kiosk is provisioned with only the endpoints and data necessary for check-in. Reduces setup time and eliminates credential exposure risk while enabling on-the-spot deployment.

Acceptance Criteria
Verified Admin Generates Scoped, Time-Limited Magic Link and QR
Given I am a ClassNest admin signed in on a verified mobile device with permission for Location L And I have selected Location L and preselected classes/hours C for the kiosk And a token TTL T is configured When I request a kiosk magic link Then the system generates a cryptographically signed, single-use token scoped to Location L and C with TTL T And persists the token server-side with status "unused", its scope, issuer admin id, and expiry timestamp And returns both a magic link URL and a QR code representation that embed only the token, not credentials And the generated token cannot be derived or predicted from prior tokens
Tablet Completes Kiosk Pairing with Valid Token
Given a kiosk tablet with internet access and the ClassNest Kiosk app installed And a valid, unused magic link token scoped to Location L and classes/hours C exists When the tablet opens the magic link or scans the QR Then the app sends the token and a device fingerprint over TLS to the pairing endpoint And the server validates token signature, TTL, scope, and unused status And binds the device to the organization/location and prompts for or accepts a friendly device name (validated length/charset) And creates a kiosk session with least-privilege role "kiosk" scoped to Location L and C And marks the token as "used" And the kiosk app shows the check-in screen for classes/hours C only
Invalid or Expired Token Is Rejected Safely
Given a token that is expired, revoked, malformed, or scope-mismatched for the requesting organization/location When a device attempts to pair using that token Then the server responds with a 4xx error that indicates the failure reason category (expired | revoked | invalid | scope_mismatch) And no kiosk session is created and no device binding occurs And the attempt is recorded to the audit log with timestamp, device fingerprint, IP, and reason And the kiosk app displays a generic non-revealing error and returns to the pairing screen
Single-Use Token Replay and Cross-Device Attempts Are Blocked
Given a token has already been used to successfully pair Device A When Device A attempts to reuse the token Then the server rejects the request with a 409/4xx and does not alter the existing session or bindings And the replay attempt is logged with device fingerprint and reason "replay" When Device B attempts to use the same token Then the server rejects the request with a 409/4xx and creates no session And the cross-device attempt is logged with Device B's fingerprint and IP
Remote Revoke and Token Rotation
Given a kiosk device D is paired to Location L with an active kiosk session When an authorized admin revokes device D from the dashboard Then the server invalidates D's kiosk session and marks the device as revoked And any subsequent kiosk API requests from D receive 401/403 and are denied And the kiosk app displays a "Session ended" message and returns to pairing mode on next request And the action is recorded in the audit log with admin id and device fingerprint Given an unused magic link token T1 exists When the admin rotates the pending pairing token Then T1 is invalidated and a new token T2 with the same scope and a fresh TTL is issued And any attempt to use T1 is rejected and logged as "rotated"
Least-Privilege Kiosk Session and Scoped Data Access
Given a kiosk session created via magic link and scoped to Location L and classes/hours C When the kiosk requests any admin or non-kiosk endpoint or data outside L/C Then the server returns 403 Forbidden and logs the unauthorized access attempt When the kiosk fetches rosters Then only rosters for C at Location L are returned with the minimal fields required for check-in And only check-in related operations (read roster, mark attendance/check-in, view waitlist positions) are permitted And customer profile edits, payments, exports, reports, and admin settings endpoints are not accessible
Auditable Pairing Log
Given pairing-related events occur (token issued, pairing success, failure, revoke, rotate) When an authorized admin views the pairing log for a selected date range or location Then the log shows entries with timestamp, admin user (if applicable), device name, device fingerprint hash, IP, token id, scope (location/classes/hours), outcome, and reason And entries are immutable and exportable to CSV And the log is filterable by outcome, location, device name, and admin user And access to the log is restricted to users with the appropriate permission; kiosk role cannot view it
Scoped Kiosk Lockdown
"As a studio owner, I want the kiosk locked to specific classes and hours so that visitors can only access self-serve check-in and not view or modify studio data."
Description

Locks the kiosk interface to a restricted, read-only check-in experience for selected classes and scheduled time windows. Hides all admin menus and sensitive data, enforces strict route guards and URL whitelisting, disables external links, prevents data entry beyond check-in, and uses a headerless, full-screen kiosk skin. Provides guidance and automatic detection for OS-level kiosk modes (e.g., Guided Access/Screen Pinning) and enforces app-level protections like idle redirect, blocked context menus, and suppressed autofill. Limits API scopes to only necessary endpoints and masks non-essential PII. Configuration is per kiosk and location, supporting recurring schedules, exceptions, and holiday closures. Ensures visitors cannot navigate beyond check-in while protecting studio data.

Acceptance Criteria
Lock to Selected Classes and Time Windows
Given kiosk K1 is configured at Location A with allowed classes [Yoga 101, Pilates Intro] and active window 09:00–12:00 local When the kiosk loads between 09:00 and 12:00 Then only Yoga 101 and Pilates Intro are visible for check-in and all other classes are hidden Given the time is outside the configured window (e.g., 08:59 or 12:01) When the kiosk loads or refreshes Then a closed message is shown and check-in actions are disabled Given a deep link to an unselected class is opened in kiosk mode When navigation is attempted Then navigation is blocked and the user remains on the kiosk check-in screen Given a recurring schedule Mon–Fri 09:00–12:00 and a date-specific exception of 10:00–14:00 on 2025-12-24 When the date is 2025-12-24 Then the kiosk enforces 10:00–14:00 for that date Given a configured holiday closure on 2025-12-25 When the date is 2025-12-25 Then the kiosk remains closed for the entire day
Route Guards, URL Whitelisting, and External Link Blocking
Given kiosk mode is active When attempting to navigate to a non-whitelisted route such as /admin, /reports, or /settings Then navigation is blocked and the app redirects to /kiosk/check-in within 100 ms Given any anchor/link with external href or target=_blank, or a window.open call When activated Then navigation is canceled and no new window or tab opens Given a right-click, long-press, or context-menu key is used When triggered Then no context menu appears Given common navigation shortcuts (e.g., Ctrl/Cmd+L, Ctrl/Cmd+T, Alt+Left) When pressed Then any resulting navigation is immediately intercepted and the app returns to the kiosk check-in screen within 500 ms
Read-only Check-in UI with Headerless Full-screen Skin
Given kiosk mode is active When the kiosk loads Then the UI renders in a full-screen headerless layout with no admin bars, navigation tabs, or profile menus visible Given the DOM is inspected When searching for admin menu elements Then admin components are not present in the DOM (not hidden via CSS) Given a member profile card is viewed during check-in When displayed Then non-essential PII (email, phone) is masked (e.g., j***@e***.com, ***-***-1234) and no edit controls are rendered Given an input field receives focus When the browser attempts autofill Then autofill is suppressed and no saved-credential prompts appear
Exit via Admin PIN with Lockout and Audit Trail
Given a 6-digit admin exit PIN is configured for kiosk K1 When the exit gesture is invoked and the correct PIN is entered Then kiosk mode exits to the normal app state within 2 seconds Given 5 consecutive incorrect PIN attempts occur within 3 minutes When another attempt is made Then the exit flow is locked for 5 minutes and a 'Too many attempts' message is shown Given any exit attempt occurs (success or failure) When it is submitted Then an audit log entry is recorded with timestamp, kiosk ID, location, masked device info, and outcome Given attempts to exit via back button, OS gestures, or devtools When performed Then the kiosk remains locked or auto-redirects back to the check-in screen within 500 ms
Auto Sign-out and Idle Redirect
Given the kiosk operational end time is 21:00 local When local time reaches 21:00 Then all sessions are signed out, access tokens revoked, and the screen shows a 'Closed' state Given there is no user interaction for N minutes (configurable, default 2) When the idle timer elapses Then the app resets to the main check-in list and clears any typed data or partial check-ins Given sign-out has occurred When the user tries browser back/forward navigation Then previous pages are not accessible and the app remains on the kiosk start state Given the app regains focus after the end time due to power or network resume When focus returns Then the kiosk remains signed out until the next allowed window
API Scope Limitation and PII Redaction
Given the kiosk uses an access token scoped to [classes:read, checkins:create, waitlist:read] When calling any endpoint outside the scope (e.g., customers:write, payments:*) Then the API responds 403 Forbidden and no data is returned Given a classes:read or waitlist:read response When inspecting payloads Then non-essential PII (email, phone, DOB) is omitted or redacted and only minimal identifiers (first name and masked last name) are included Given network activity during check-in flow When reviewing browser console and network logs Then no PII appears in query strings or client-side logs and access tokens are never logged Given an API call fails due to scope limits When the UI handles the error Then a generic 'Not available' state is shown without revealing sensitive details and the kiosk flow remains uninterrupted
OS Kiosk Mode Detection and Enablement Guidance
Given the kiosk loads on iOS 15+ Safari without Guided Access enabled When the app initializes Then a guidance banner with step-by-step instructions appears and disappears automatically once Guided Access is enabled Given the kiosk loads on Android 10+ Chrome without Screen Pinning enabled When the app initializes Then an overlay prompts enabling Screen Pinning and is suppressed once pinning is active Given OS-level kiosk mode is unavailable or dismissed When the kiosk continues operation Then app-level protections (route guards, external link blocking, idle redirect) remain fully enforced Given OS-level kiosk mode disengages during use When this is detected Then the guidance prompt reappears within 1 second
PIN-Protected Exit & Admin Unlock
"As front desk staff, I want a PIN-protected exit from kiosk mode so that only authorized team members can change settings or exit the kiosk."
Description

Requires a configurable 4–8 digit PIN to exit kiosk mode, access kiosk settings, or escalate privileges. Supports per-location or per-device PINs, role-based permissions, attempt throttling, temporary lockout after repeated failures, and an emergency one-time admin code for recovery. Includes secure PIN management with rotation, encryption at rest, and audit trails for all unlock attempts and mode changes. Prevents unauthorized users from tampering with kiosk restrictions while enabling staff to quickly service the device when needed.

Acceptance Criteria
Exit Kiosk Mode Requires Correct PIN
Given a kiosk is locked with a configured PIN policy of 4–8 numeric digits and the active PIN is 5729 When a user taps Exit and enters 5729 Then kiosk mode exits within 2 seconds and the device returns to the staff home; an "Exit Success" event is recorded with timestamp, device ID, and outcome=success Given the same kiosk When a user enters a PIN of invalid length (e.g., 123 or 123456789) or containing non-numeric characters Then the input is rejected client-side, no attempt is sent to the server, and no attempt is counted Given the same kiosk When a user enters an incorrect 4–8 digit PIN Then access is denied, the kiosk remains locked, the message "Invalid PIN" is shown without revealing correctness, and a failed attempt is logged with timestamp and device ID
Access Kiosk Settings Requires PIN and Permission
Given a kiosk with a PIN that grants permission kiosk.settings When the correct PIN is entered from the Settings action Then the kiosk settings panel opens and admin menus remain hidden outside the panel Given a kiosk with a PIN that lacks kiosk.settings When that PIN is entered from the Settings action Then access is denied and a "Permission Denied" event is logged; kiosk remains in standard check-in view Given a kiosk with a PIN that grants kiosk.settings but not kiosk.exit When the PIN is entered Then settings open but the exit action remains disabled
Attempt Throttling and Temporary Lockout
Given attempt limit=5 and lockout duration=15 minutes configured for this device When 5 consecutive incorrect PIN entries occur within any rolling 15-minute window Then further PIN entry is disabled for 15 minutes and a "Temporary Lockout" event is logged Given the kiosk is in temporary lockout When an emergency one-time admin code is entered Then the code is evaluated (not throttled), and on success the lockout is cleared for that session while an "Emergency Unlock" event is logged Given the lockout period has elapsed When a user next attempts a PIN Then the failed-attempt counter resets and normal behavior resumes
Emergency One-Time Admin Code Recovery
Given an owner generates a one-time admin code that is single-use and valid for 10 minutes When the code is entered on the kiosk Then the kiosk unlocks with elevated privileges permitted by kiosk.escalate, the code is immediately invalidated, and an audit record includes issuer, device ID, location ID, timestamp, and outcome=success Given an expired or previously used one-time admin code When it is entered Then access is denied with "Code expired or used" and an audit record outcome=failed is stored Given a valid one-time admin code is generated and not used within 10 minutes When the validity window elapses Then the code is invalidated automatically and cannot be used
Per-Location and Per-Device PIN Resolution
Given a device has both a device PIN (9990) and a location PIN (1111) When a user attempts to unlock with 1111 Then access is denied and a failed attempt is logged Given the same device When a user unlocks with 9990 Then access is granted and an audit record includes resolver=device Given a device with no device PIN but a location PIN exists When the location PIN is entered Then access is granted and an audit record includes resolver=location Given a device is reassigned to a different location When a PIN from the previous location is entered Then access is denied and an audit record shows reason=location_mismatch
Role-Based Escalation and Unlock Permissions via PIN Scope
Given a PIN scoped with permissions [kiosk.exit] only When it is entered Then exiting kiosk is allowed but settings access is denied Given a PIN scoped with permissions [kiosk.settings] only When it is entered Then settings open while kiosk mode remains active; exit remains disabled Given a PIN scoped with permissions [kiosk.escalate] When it is entered Then the current session is elevated for 10 minutes to access settings; elevation auto-revokes after 10 minutes or on manual revoke, and all actions are audited Given a PIN with no kiosk.* permissions When it is entered Then no additional access is granted
Secure PIN Management: Rotation, Storage, and Audit Trails
Given a new device PIN is saved for a kiosk When the change is confirmed Then the previous PIN is invalidated across all instances within 60 seconds, and an audit "PIN Rotated" event is recorded with actor, device ID, and previous PIN masked (e.g., ****29) Given system logs and databases are inspected by security tooling When searching for PIN values Then no plaintext PINs are found; PIN material is stored as salted, work-factored hashes (e.g., Argon2id/bcrypt), encryption at rest is enabled for the secrets store, and logs mask PIN input Given any unlock attempt, settings open, exit, escalation, lockout, or emergency code use When the event occurs Then an immutable audit trail entry is written with timestamp (UTC), device ID, location ID, actor or scope, method (PIN/Code), result (success/fail/lockout), and reason codes where applicable
Auto Sign-Out & Daily Reset
"As a studio manager, I want the kiosk to auto sign out and reset at the end of the day so that no data persists overnight and the next day starts cleanly without manual steps."
Description

Automatically signs the kiosk out at a configurable end-of-day time, invalidates the kiosk session, clears cached data, and resets the interface to a “ready” state. Supports multiple daily windows, idle sign-out after a set period of no interaction (with on-screen warning), and timezone-aware scheduling that handles daylight saving changes. On next launch, the kiosk reloads with the previously assigned scope (classes/hours) and re-establishes a fresh least-privilege session. Ensures no lingering access after hours, reduces data exposure, and provides a clean start each day without manual intervention.

Acceptance Criteria
End-of-Day Auto Sign-Out at Configured Time
Given an active kiosk session and an end-of-day time configured to 21:00 in the kiosk’s effective timezone When the local civil time reaches 21:00:00 Then the kiosk signs out and navigates to the Ready screen within 10 seconds And the server invalidates the kiosk’s access and refresh tokens within 10 seconds And all local caches and storage (LocalStorage, SessionStorage, IndexedDB, Service Worker caches) are cleared And any in-progress check-in flow is canceled without persisting unsaved personal data
Multiple Daily Windows Sign-Out and Between-Window Lock
Given two daily windows configured: 06:00–11:00 and 17:00–21:00 in the effective timezone When the time crosses 11:00 or 21:00 Then the kiosk auto-signs out within 10 seconds and returns to the Ready screen And outside configured windows, the kiosk displays a Closed message and disables check-in actions And no duplicate sign-out events occur for back-to-back windows separated by less than 5 minutes And at the start of a new window, a fresh session is established before any check-in action is permitted
Idle Sign-Out With On-Screen Warning
Given an idle timeout of 3 minutes and a warning period of 15 seconds When there is no user interaction for 2 minutes 45 seconds Then a visible on-screen countdown warning appears with remaining seconds And if the user interacts before the countdown ends, the warning is dismissed and the idle timer resets And if no interaction occurs by countdown end, the kiosk signs out within 2 seconds and clears local data And no new API requests are made after the idle sign-out until a fresh session is created
Timezone Awareness and Daylight Saving Adjustments
Given the organization timezone is set to America/Los_Angeles and end-of-day is 21:00 When DST starts or ends Then auto sign-out occurs at 21:00 local civil time on that date And if the device timezone differs from the organization timezone, the organization timezone is used for scheduling And if the organization timezone changes, the kiosk updates its schedule within 60 seconds without requiring a restart
Session Invalidation and Access Revocation After Auto Sign-Out
Given a kiosk auto sign-out event has occurred When any API call is made using the pre-sign-out token Then the server responds 401 Unauthorized within 60 seconds of sign-out And any open WebSocket or SSE connection is closed within 10 seconds of sign-out And check-in and roster endpoints are inaccessible until a new session is established
Ready State Reset and Scope Persistence on Next Launch
Given the kiosk is scoped to specific classes and hours When an auto sign-out occurs and the app is relaunched Then the UI loads the Ready screen with no attendee or roster data visible And the previously assigned classes and hours are restored And a new least-privilege session is established before any data is fetched And admin menus and privileged actions are not visible
Self-Serve Check-In UI
"As a student, I want to quickly check myself in on the kiosk so that I can join class without waiting for staff assistance."
Description

Delivers a fast, touch-friendly interface for students to locate and confirm their booking by name, phone last-4, or QR from confirmation messages. Displays current/next class tiles limited by kiosk scope, indicates capacity status, and supports claiming auto-offered waitlist spots when present. Handles edge cases like waivers required, late arrival cutoffs, and duplicate names with minimal PII exposure. Provides large tap targets, high-contrast themes, multilingual support, and WCAG 2.1 AA accessibility. Works gracefully offline by queuing check-ins and reconciling when back online, with clear user feedback and staff notifications as needed. Optimized to minimize lines and reduce staff involvement while keeping data safe.

Acceptance Criteria
Locate and Check In via Name or Phone Last-4
Given the kiosk is scoped to a class within the active check-in window (default -15 to +10 minutes, configurable), When a student taps Search and enters at least 2 characters of first or last name or 4 digits of phone, Then only bookings for the selected class/time are returned within 500 ms. Given results are shown, When displayed, Then each row shows only first name and last initial, masked phone (***-***-1234), and booking status; no email or full phone is displayed. Given duplicate names or shared last-4 exist, When results are shown, Then rows include the student's booking time and last-4 to disambiguate without exposing additional PII. Given a matching booking exists and is eligible, When the student taps their row, Then the system confirms check-in within 1 second, updates the booking to Checked In, and displays a 2-second success screen before returning to the class list. Given no match is found after input, When the search completes, Then the UI offers Scan QR and Try different search options and does not reveal whether a person is on the roster. Given 5 consecutive failed searches (no matches) occur within 60 seconds, When the 6th attempt is made, Then the input is temporarily locked for 30 seconds and a generic message is shown to deter enumeration.
Scan QR Code to Check In
Given the kiosk is on the class screen, When Scan QR is tapped, Then the camera view opens within 300 ms with torch toggle and guidance frame. Given a valid ClassNest QR payload for the selected class and within the check-in window is scanned, When processed, Then check-in is confirmed in under 1 second and a success tone and visual confirmation are provided. Given the QR is valid but for a different class or outside the window, When processed, Then an error message Not valid for this class/time is shown without PII and options to switch to the correct class or search are offered. Given the QR is malformed or revoked, When scanned, Then the UI displays Code not recognized and prevents any data leakage. Given connectivity is unavailable, When scanning a QR with an offline-verifiable signed token, Then the kiosk validates the signature locally and queues the check-in; if signature cannot be verified, it rejects with error.
Scoped Class Tiles with Capacity Indicators
Given the kiosk was launched via a magic link with scope to specific classes and hours, When the class list loads, Then only Current (in progress or starting within 60 minutes) and Next class tiles within that scope/hours are visible; all others are hidden. Given class tiles are displayed, When rendered, Then each tile shows class name, start time, instructor first name or initial, and capacity status: X spots left, Full, or Waitlist (Y). Given class capacity changes, When the kiosk receives updates, Then the tile status updates within 5 seconds when online, or upon reconnect if offline. Given kiosk mode is active, When any screen is displayed, Then no admin menus or data are accessible from the UI. Given the scoped hours end, When the end time is reached, Then the kiosk prevents new check-ins until the next scoped window and returns to the welcome screen.
Claim Auto-Offered Waitlist Spot at Kiosk
Given a student has an active auto-offer for the selected class, When they identify themselves via QR or phone last-4, Then the UI presents a Claim spot call-to-action showing the remaining offer countdown in mm:ss. Given the student taps Claim spot within the countdown, When processed, Then the system atomically converts the waitlist hold to a confirmed booking and marks the student Checked In if within the check-in window, all within 2 seconds. Given two claim attempts occur for the same offer, When processed, Then the first successful claim wins and the second sees Spot just taken with an option to join the waitlist. Given the offer has expired, When the student attempts to claim, Then the UI displays Offer expired and does not expose other roster info. Given payment is required to claim and a saved payment method is on file, When claim is initiated, Then payment is charged successfully before check-in; if no payment method is on file, the UI shows See staff to complete payment and does not complete the claim.
Waiver Requirement and Late Arrival Cutoff Handling
Given the selected class requires a signed waiver and the student's waiver is not on file, When they attempt to check in, Then the kiosk presents the waiver in the selected language with a large signature field and Agree button; check-in completes only after signature capture and timestamp, with the entire flow under 90 seconds. Given connectivity is offline during waiver signing, When the student signs, Then the kiosk stores a time-stamped, hashed copy locally and queues the upload; the check-in is marked Pending waiver upload and the staff banner reflects a pending count. Given a late arrival cutoff of N minutes after start is configured, When a student attempts check-in after the cutoff, Then the kiosk blocks check-in with Late arrival — see staff and does not reveal roster information; if a staff override PIN is entered, check-in proceeds and the action is logged.
Accessibility, Touch Targets, and Multilingual Support
Given the kiosk is in use, When any tap target is displayed, Then its hit area is at least 44x44 dp with a minimum 12 px spacing between adjacent targets. Given text and UI elements are rendered, When measured, Then color contrast meets WCAG 2.1 AA (4.5:1 for normal text, 3:1 for large text/icons) and all content remains usable at 200% text size without loss of functionality. Given a screen reader is enabled, When navigating, Then all actionable elements have meaningful accessible names and the focus order matches the visual order with no keyboard traps. Given the language toggle is used, When a user selects a supported language (at minimum English and Spanish), Then all visible copy, buttons, dates/times, and error messages localize within 300 ms and the selection persists until session end. Given numeric entry is required, When entering phone last-4, Then a numeric keypad is presented and input is restricted to digits 0-9 with automatic masking.
Offline Check-In Queue, Feedback, and Reconciliation
Given the device loses connectivity, When the kiosk enters offline mode, Then an on-screen Offline — check-ins will be queued banner appears to staff, and student flows continue without error messages. Given a student completes check-in offline, When saved, Then the check-in is persisted locally with timestamp and booking ID, and the student sees a success screen with a subtle Queued badge; no PII is exposed on success screens. Given connectivity is restored, When reconciliation begins, Then queued check-ins are retried in FIFO order and synced within 10 seconds per 50 items; successful syncs clear the local queue and update counts. Given a queued check-in conflicts with server state (already checked in, canceled, or class full), When reconciling, Then the item is marked Needs review, no duplicate is created, and a staff banner lists the conflict count with a tap target to details. Given the offline queue reaches 10 or the oldest item exceeds 15 minutes, When thresholds are crossed, Then a high-visibility staff alert is shown; if kiosk alerts are configured, an admin notification is sent.
Kiosk Device Health & Alerts
"As an admin, I want visibility into kiosk device health and alerts so that I can proactively address issues and keep self-serve check-in running smoothly."
Description

Adds an admin dashboard listing all kiosk devices with status (online/offline), last heartbeat, app version, assigned scope, and recent activity. Enables remote actions such as refresh session, force sign-out, change scope, and send on-screen notices. Implements a lightweight heartbeat and connectivity quality metrics, with alerts (push/email) if a kiosk goes offline, has a large offline queue, or repeatedly fails check-ins. Exposes audit logs and export for compliance. Helps staff proactively resolve issues before class starts and maintain reliable self-serve operations across locations.

Acceptance Criteria
Live Device List & Status Overview
Given an admin is on the Device Health dashboard When kiosks exist in the organization Then the list shows for each device: Device Name, Location, Device ID, Online/Offline Status, Last Heartbeat (UTC and local TZ), App Version, Assigned Scope (classes/hours), and Recent Activity (last 10 events) And the list auto-refreshes at least every 15 seconds without a full page reload And the admin can filter by Status (online/offline), Location, App Version, and search by Device Name/ID And the admin can sort by Last Heartbeat (desc), Status, and Name When no kiosks exist Then an empty state is shown with guidance to add a kiosk
Heartbeat & Connectivity Quality Metrics
Given a kiosk is connected to the service When the heartbeat interval is configured to 30 seconds Then the service records a heartbeat at least every 30 ± 5 seconds per device And the system computes per-device rolling 5-minute metrics: average RTT latency (ms), heartbeat success rate (%), and consecutive missed heartbeats When 4 consecutive heartbeats are missed or no heartbeat is received for 2 minutes Then the device status changes to Offline and the last heartbeat timestamp is updated When heartbeats resume Then the device status changes to Online within 15 seconds and the missed heartbeat counter resets
Offline Kiosk Alerting (Push & Email)
Given a device transitions from Online to Offline When the offline threshold (no heartbeat for 2 minutes) is exceeded Then a push notification and an email are sent to the configured recipients within 60 seconds And the alert message includes device name, location, last heartbeat time, current status, and a link to the Device Health dashboard And duplicate offline alerts for the same device are suppressed for 30 minutes When the device returns Online Then a recovery notification is sent within 60 seconds
Remote Actions: Refresh Session, Force Sign-Out, Change Scope, Send Notice
Given an admin selects a device on the Device Health dashboard When the admin triggers Refresh Session Then the kiosk clears session data and reloads the check-in screen within 10 seconds and the dashboard shows status Success, or Queued if offline and Executed on reconnect within 2 minutes of reconnect When the admin triggers Force Sign-Out Then the kiosk signs out any active user, returns to the check-in screen, disables back navigation, and the action result is shown on the dashboard When the admin changes the device Scope to a specified class/hour configuration Then the kiosk applies the new scope within 10 seconds or on next refresh, showing only the newly assigned classes/hours When the admin sends an On-Screen Notice with message text up to 200 characters and a duration Then the kiosk displays a dismissible banner within 5 seconds without exposing admin menus And for all remote actions the dashboard displays real-time action state (Queued, In-Progress, Success, Failed) and error details on failure, and each action is recorded in the audit log
Offline Queue Monitoring & Alert
Given a kiosk is offline and check-ins are queued locally When the offline queue length exceeds 10 items or the oldest queued item age exceeds 5 minutes Then the dashboard displays a Large Offline Queue badge on the device and an alert is sent to recipients And the dashboard shows queue length and oldest item age for the device When the device reconnects Then queued check-ins are synced within 60 seconds and the badge clears automatically
Repeated Check-In Failure Alert
Given a kiosk is Online When 5 or more check-in attempts fail within a 3-minute window Then the device is marked with a Check-in Errors Detected status and an alert is sent to recipients containing error code summary and last failure timestamp And subsequent alerts for the same device and error signature are suppressed for 30 minutes When the failure rate drops below 1 per 5 minutes for 10 consecutive minutes Then the Check-in Errors Detected status clears automatically
Audit Logs & Export for Compliance
Given an admin opens the Audit Logs view Then they can filter by date/time range (timezone-aware), device, location, action type (status change, alert sent, remote action, scope change, check-in failure), actor (system/admin), and outcome (success/fail) And each log entry includes timestamp (UTC, ISO-8601), device ID, device name, location, action, parameters, actor, outcome, and a correlation ID And logs are retained for at least 365 days When the admin exports the current filtered set Then a CSV is generated within 30 seconds for up to 100,000 rows containing all listed fields with ISO-8601 timestamps and downloaded successfully

Step-Up Verify

Add lightweight re-checks for sensitive actions (payouts, policy edits, bulk refunds) using a fresh magic link or on-device biometrics. Triggers only when needed—new device, high amount, or admin-only screens—for strong protection without constant re-logins.

Requirements

Sensitive Action Guard & Resume
"As a studio owner initiating a payout, I want the system to pause the action and prompt me to verify so that large transfers can’t proceed without a fresh check and I can resume without losing my work."
Description

Introduce a middleware and UI modal that intercepts sensitive operations (payouts, policy edits, bulk refunds, bank account changes) and pauses execution until the user completes a fresh verification step. Preserve the action context and input so the user can resume seamlessly after verification without data loss. Enforce deny-by-default if verification fails, times out, or is dismissed. Ensure idempotency for bulk operations and provide a single verification for a batch where permissible. Implement a server-side "recently_verified" claim with a configurable freshness window to authorize completion. Provide responsive, accessible UI components for web/mobile, with clear timeouts, error states, and fallback paths.

Acceptance Criteria
Conditional Triggering Rules Applied to Sensitive Actions
Given a user initiates one of the sensitive operations (payout, policy edit, bulk refund, bank account change) When the system evaluates trigger conditions (new/untrusted device, action amount >= configured high_amount_threshold, admin-only screen) Then the step-up verification modal is required if any trigger condition is true And the step-up verification modal is not required if no trigger conditions are true and a valid recently_verified claim exists within freshness window And the decision (triggered or bypassed) is recorded with reason and timestamp And the high_amount_threshold and freshness window are configurable via server configuration
Payout Intercept and Seamless Resume Post-Verification
Given a user completes the payout form and clicks Submit When step-up verification is required Then middleware halts the payout request before any external transfer is created And the UI displays the verification modal without navigating away And all payout form inputs and validation state are preserved in memory/session When the user successfully verifies within the allowed time window Then the original payout request resumes automatically using the preserved payload And exactly one payout is created and confirmed (no duplicate attempts) And the user sees a success confirmation for the payout
Policy/Bank Details Edit Preservation and Resume
Given a user edits policy settings or bank account details and clicks Save When step-up verification is required Then the save operation is paused and no changes are persisted And the verification modal is shown and all user inputs remain intact When the user completes verification successfully Then the original Save operation proceeds with the preserved inputs And the updated settings are persisted and reflected on reload And audit metadata records the verification event associated with the change
Bulk Refunds Single Verification and Idempotency
Given a user selects a batch of N payments for refund and initiates Bulk Refund When step-up verification is required Then a single successful verification authorizes processing the entire batch within the same session and scope And the server assigns and requires a batch idempotency key so each refund is processed at most once And if the request is retried with the same idempotency key, no duplicate refunds are created And partial failures are reported per item with stable error codes, while successful items are not reprocessed on retry And if the freshness window expires before retrying failed items, a new step-up verification is required
Deny-by-Default on Verification Failure, Timeout, or Dismissal
Given the verification modal is displayed for a sensitive operation When the user dismisses the modal, fails verification, or the verification times out after the configured limit Then the pending operation is canceled and no state changes occur And the user is shown a clear error message indicating the reason (dismissed, failed, or timed out) And no external side effects are triggered (e.g., no payout, no refund, no setting change) And the UI returns to the pre-action state with inputs preserved for review or discard
Server-Side Recently Verified Claim with Configurable Freshness
Given a user completes step-up verification When the server issues a recently_verified claim Then the claim includes timestamp, device/session binding, and authorized scopes for action types And the claim is accepted for authorizing sensitive operations only while within the configured freshness window And upon expiration, device change, logout, or risk event, the claim is invalidated and sensitive actions require re-verification And APIs without a valid claim respond with a consistent error indicating recent verification required
Accessible, Responsive Verify Modal with Biometrics and Magic Link Fallback
Given the verification modal is displayed on web or mobile Then it meets WCAG 2.1 AA basics: keyboard focus trap, visible focus, screen-reader labels, and proper role semantics And the modal layout adapts to mobile and desktop viewports without clipping or overflow When biometric verification is available and selected Then successful biometric auth immediately satisfies verification without leaving the app And after two consecutive biometric failures, the user is prompted to switch methods When the user selects magic link verification Then a single-use magic link is sent via the configured channel and expires within the configured time window And opening the valid link completes verification and returns the user to the pending action to resume automatically And all error states (expired link, network error, unsupported method) are clearly messaged with a retry or alternative method option
Magic Link Step-Up Verification
"As an instructor performing a sensitive action on a new device, I want to confirm via a one-click link so that I can proceed securely without a full re-login."
Description

Generate one-time, short-lived, cryptographically signed magic links delivered via email or SMS to re-verify the user on demand. Bind the token to the session and device fingerprint where possible, enforce single use, and expire links after a tight TTL (e.g., 10 minutes). On successful click, elevate the session with a recent-verification claim scoped to the tenant and action. Provide cross-device handling (display a 6–8 digit confirmation code when opened on another device) and localized templates with tenant branding. Include rate limiting, bounce/undeliverable handling, and clear UX for expired/invalid links with a safe retry path.

Acceptance Criteria
Same-Device Magic Link Elevates Session for Sensitive Action
Given an authenticated user initiates a sensitive action requiring step-up verification When the system issues a cryptographically signed, single-use magic link with a TTL of 10 minutes, bound to the current session ID and device fingerprint And the user opens the link on the same device within the TTL Then the token signature validates, the bindings match, and the token is marked consumed And the user’s session is elevated with a recent_verification claim scoped to the tenant and the specific action, with max_age ≤ 10 minutes And the sensitive action proceeds without additional login
Cross-Device Confirmation Code Flow
Given a user initiates step-up verification and opens the magic link on a different device than the originating session When the link is opened within the TTL Then the link displays a randomly generated 6–8 digit confirmation code and the originating session shows a code entry prompt When the correct code is entered on the originating session within 2 minutes and ≤ 5 attempts Then the originating session is elevated with a recent_verification claim scoped to the tenant and action, and the link/token are marked consumed And further code submissions or link clicks fail with an invalid/used status and safe retry guidance
Single-Use and Expired/Invalid Link Handling
Given a magic link token is consumed once When the same link is used again Then the user sees an invalid/used message that does not disclose account details and a safe option to request a new link (subject to rate limits) Given a magic link is opened after its TTL or fails signature/tenant/session validation When the user attempts to proceed Then display an expired/invalid page with a clear retry action and help link, and do not elevate the session And all outcomes are audit logged with reason codes (expired, already_used, signature_invalid, tenant_mismatch, session_mismatch)
Rate Limiting and Abuse Controls for Send and Verify
Given any user/tenant/IP/device requests a step-up magic link When sends exceed 5 per 15 minutes per user, or 20 per hour per tenant, or 10 per 10 minutes per IP Then the service returns HTTP 429 with a Retry-After header and no link is sent Given a user is entering confirmation codes When attempts exceed 5 for a single verification Then the verification is locked for 15 minutes and a new flow is required And throttle events are logged and messages avoid user enumeration
Bounce and Undeliverable Handling with Channel Fallback
Given the provider reports an email hard bounce or SMS undeliverable for a step-up message When the event is received Then mark that channel as undeliverable, suppress further sends to it for 24 hours, and surface UI to update contact info or switch channels When the user selects an alternate verified channel Then issue a fresh token, invalidate prior tokens, and send using localized, branded templates And record bounce/undeliverable metadata in audit/support logs with PII redaction
Localized and Branded Magic Link Templates
Given a tenant has branding (logo, name, primary color) and a user has a preferred locale/timezone When sending step-up via email/SMS Then the message renders tenant branding, localizes content and times, and explicitly states the TTL (e.g., “Link expires in 10 minutes”) And includes clear cross-device code instructions and the requesting app/device hint And is available in at least English and one additional locale with proper fallback And passes accessibility checks (contrast ≥ 4.5:1, alt text present) and contains no sensitive PII
Token Security, Signing, Binding, and Auditing
Rule: Tokens are cryptographically signed (e.g., HMAC-SHA256 or EdDSA) with key rotation; unsigned or invalidly signed tokens are rejected Rule: Token contains jti, tenant_id, action_scope, session_id, issued_at, expires_at; no PII is embedded Rule: jti hashes are stored server-side to enforce single use; on consume, mark used with timestamp Rule: Enforce session/device binding; on mismatch, require the cross-device code flow rather than elevating directly Rule: TTL ≤ 10 minutes with ±60s clock skew tolerance Rule: All issuance, delivery outcomes, verifications, failures, and admin overrides are audit logged with correlation IDs
Risk-Based Trigger Rules Engine
"As a platform admin, I want step-up to trigger only under elevated risk so that users aren’t interrupted unnecessarily while high-risk actions stay protected."
Description

Implement a rules engine that evaluates risk signals—new device fingerprint, IP reputation, geo-distance from last login, time-of-day anomalies, role/permission, action category, amount thresholds, and velocity of sensitive actions—to decide when to require step-up verification. Provide sane defaults and allow per-tenant overrides with presets (e.g., always for payouts, amount > $X for refunds, always for policy edits). Offer a simulation mode to preview trigger rates, and expose metrics for tuning. Integrate with the existing authorization layer and event bus to minimize latency and ensure consistent enforcement across web and API clients.

Acceptance Criteria
Default Enforcement for Payouts
Given the tenant has not modified default risk rules When a user with permission to initiate payouts requests a payout via web or API Then the rules engine returns decision "require_step_up" with rule_id "payouts_always" And the authorization layer blocks the payout until step-up verification succeeds And the same request from web and API yields identical decisions for identical context And the rules engine adds decision latency of <= 150 ms p95 and <= 300 ms p99 And an event "risk.decision.made" containing tenant_id, user_id, action_type="payout_initiate", rule_id, matched_signals, decision, and correlation_id is published on the event bus before the HTTP response is sent
Amount Threshold for Refunds
Given the tenant uses refund_amount_threshold = 10000 cents by default When a refund is requested for amount > 10000 cents in the booking currency Then the rules engine returns decision "require_step_up" with rule_id "refunds_amount_threshold" And when amount <= 10000 cents and no other rules match, the decision is "allow" (no step-up) And the matched_signals include amount_cents and threshold_cents And the decision is deterministic for identical inputs
Admin Policy Edits Require Step-Up
Given a user with role "Admin" or "Owner" is editing policy settings When the user attempts to save any policy change Then the rules engine returns decision "require_step_up" with rule_id "policy_edits_always" And a non-admin user is rejected by the authorization layer before rule evaluation And the decision is enforced consistently across web and API endpoints for policy updates
New Device and Geo/Time Anomaly Triggers
Given a sensitive action attempt (payout, refund, policy_edit) And the device fingerprint has not been seen for this user in the last 90 days OR IP reputation score <= 20 OR geo_distance from last successful login > 500 km within 30 minutes OR the action timestamp is outside the user's 95% active time window (computed over the last 30 days) When the rules engine evaluates the request Then the decision is "require_step_up" with rule_id indicating the matched signal ("new_device", "low_ip_rep", "geo_jump", or "time_anomaly") And matched_signals include the specific signal values that triggered the rule And if none of these signals are present and no other rules match, this rule does not trigger step-up
Velocity-Based Trigger for Sensitive Actions
Given defaults N=3 sensitive actions in a rolling 10-minute window per user per tenant across categories [payout, refund, policy_edit] When the count of sensitive actions attempted by the same user within 10 minutes exceeds 3 Then the rules engine returns decision "require_step_up" with rule_id "velocity_sensitive_actions" And the windowing is rolling based on event timestamps from the event bus And counts include both successful and blocked attempts across web and API And when the rolling count decreases to <= 3 due to time elapsing, this rule no longer triggers
Per-Tenant Overrides and Presets
Given a tenant enables preset "Always for payouts", sets refund_amount_threshold=7500 cents, and disables the time_of_day_anomaly rule When the rules engine evaluates relevant actions Then payouts always return decision "require_step_up", refunds > 7500 cents return decision "require_step_up", and time-of-day anomalies do not trigger decisions And invalid override values are rejected with error code "RULES_VALIDATION_ERROR" and the last valid configuration remains active And any change creates an audit record and emits "risk.rules.updated" with previous and new config checksums on the event bus And configuration changes propagate and take effect within 60 seconds across all nodes
Simulation Mode and Metrics Exposure
Given simulation mode is run for a specified tenant, time range, and sample size against historical events When the simulation completes Then it produces a report including overall_trigger_rate, trigger_rate_by_rule_id, trigger_rate_by_action_type, and estimated added challenges per 1000 actions And simulation does not alter live enforcement or send user challenges And operational metrics are exposed: counters (risk_decisions_total, step_up_required_total), histograms (risk_decision_latency_ms), and gauges (rules_active_total) labeled by tenant_id, rule_id, action_type, and client_type And metrics are queryable via the existing monitoring stack and reflect updates within 60 seconds
Biometric Step-Up via WebAuthn/Passkeys
"As a studio admin, I want to confirm sensitive actions with my device biometrics so that I can verify quickly without checking my email."
Description

Enable on-device biometric verification using WebAuthn with platform authenticators (Face ID, Touch ID, Android Biometrics, Windows Hello). Provide a frontend SDK to invoke a challenge, handle availability detection, and fall back to magic link when unsupported. Store public keys securely per user and tenant, support multiple credentials, and require user verification in authenticator settings. Ensure compatibility with major browsers, passkey syncing across devices, and responsive, accessible UX. Validate assertions server-side and upgrade the session with a recent-verification claim upon success.

Acceptance Criteria
Step-up verification for high-value payout
Given an authenticated instructor initiates a payout above the configured step-up threshold and a user-verifying platform authenticator is available When the frontend SDK requests a WebAuthn assertion with userVerification=required using a fresh server-issued challenge Then the native biometric prompt is shown and the user successfully verifies within 60 seconds And the server validates RP ID, origin, challenge, signature, credential ownership (user and tenant), UV=true, and a non-decreasing signCount, and records an audit event And the payout action is authorized and the session is upgraded with recent_verification=true and verified_at set to now with a TTL of 10 minutes
Fallback to magic link when biometrics unsupported
Given the user initiates a sensitive action and a user-verifying platform authenticator is unavailable, unsupported, or returns NotAllowedError When the SDK detects unavailability and requests a magic link fallback via the user’s verified email or phone Then a one-time link is sent that expires in 10 minutes, is rate-limited to 3 sends per hour, and is bound to the session, user, tenant, and action via a nonce And following the link completes verification on the same or another device, upgrades the session with recent_verification=true, and redirects back to the pending action And the action remains blocked if the link is expired, revoked, or verification is not completed
Admin settings access requires recent verification
Given the user navigates to Payout Settings or Policy Edit screens When the session lacks recent_verification or verified_at is older than 10 minutes or the browser session is new on this device Then the SDK prompts WebAuthn step-up before allowing access And on success the screen loads; on failure or cancel the user remains blocked and sees an accessible error with retry and fallback options And within the TTL window, subsequent navigation to these screens does not re-prompt
Register and manage multiple platform credentials
Given a logged-in user opens Security settings to add a platform authenticator When they create a passkey with userVerification=required and attestation conveyance=none Then the server stores credentialId, publicKey, signCount, transports, and metadata scoped to user and tenant, encrypted at rest, and prevents duplicates And the user can register up to 5 credentials, list them with device labels, and remove any; removed credentials are immediately invalid for assertions And subsequent step-up attempts succeed with any remaining valid credential for that user in the same tenant
Cross-browser and mobile support
Given a supported environment (latest two versions of Chrome, Safari, Edge, and Firefox on macOS, Windows, iOS, and Android) with platform authenticators available When the SDK initiates a step-up Then the WebAuthn prompt appears and completes successfully on each environment without console errors, and the UI is responsive across 320–1920px viewports And in environments lacking platform authenticators, the SDK automatically degrades to magic link fallback without breaking the flow
Server-side assertion validation and security controls
Given the server receives a WebAuthn assertion for step-up When validating, it checks RP ID matches the configured RP ID, origin is in the allowlist, challenge equals the last issued nonce for the session and action within 120 seconds, signature verifies against the stored public key, user handle matches, UV=true, and signCount is higher or handled per authenticator policy Then on success it issues a recent_verification claim with a TTL of 10 minutes, logs the event with device info and a hashed credentialId, and enforces per-user rate limiting of 5 attempts per minute And on failure it returns 401 with specific error codes (invalid_challenge, bad_signature, uv_required, origin_mismatch, replay_detected), does not modify the session, and increments security metrics
Accessibility and localization for step-up flow
Given a user relying on screen readers or keyboard-only navigation and using any supported locale When the step-up dialog is presented Then all UI meets WCAG 2.2 AA (focus management, ARIA roles/labels, contrast ≥ 4.5:1), is fully operable via keyboard, and is localized for EN/ES/FR And biometric prompt instructions include cancel and fallback options that are reachable via keyboard and announced by screen readers
Security Audit Trail & Alerts
"As a compliance reviewer, I want a complete record of step-up checks on sensitive actions so that I can investigate disputes and demonstrate our controls."
Description

Record every step-up event with timestamp, user and tenant IDs, action type, parameters (e.g., amount), risk signals evaluated, verification method used, outcome (success/failure), device fingerprint, IP, and country. Store logs in an append-only, tamper-evident datastore with role-based access in an admin security report. Provide filtering, export (CSV/JSON), and retention policies. Emit webhooks and optional email/Slack alerts for high-risk denials or repeated failures. Redact or hash sensitive fields to protect privacy while preserving forensic value.

Acceptance Criteria
Append-Only, Tamper-Evident Event Logging
Given a step-up verification is initiated or completed When the system records the audit event Then a new entry is appended with fields: sequence_id, previous_entry_hash, content_hash, and required metadata And the previous_entry_hash matches the content_hash of the immediate prior entry And any attempt to update or delete an existing entry via API or console returns HTTP 403 and no storage mutation occurs And a full integrity verification recomputing hashes from first to last entry reports zero breaks for untampered logs And any detected integrity failure produces an "integrity_alert" audit entry with details
Complete Event Schema Capture
Given step-up events for payout, policy edit, and bulk refund actions (both success and failure) When each event is logged Then each entry contains non-null values for: timestamp (ISO 8601 UTC), user_id, tenant_id, action_type, parameters (including amount where applicable), risk_signals (names and scores), verification_method, outcome, device_fingerprint, ip_address, country And the timestamp differs from server wall clock by no more than 1 second And schema validation rejects writes missing any required field with HTTP 400 and no entry is persisted And each entry includes a unique event_id for correlation
Role-Based Access and Tenant Isolation for Security Report
Given an authenticated user with role SecurityAdmin or Owner within tenant T When they access the Security Audit report Then they can view, filter, and export only entries for tenant T And edit/delete controls are not present and mutation endpoints return HTTP 405 Given a user without SecurityAdmin or Owner role When they attempt to access the report Then the system returns HTTP 403 and logs an "access_denied" audit event
Filtering, Search, and Export (CSV/JSON)
Given audit logs exist across multiple dates, users, actions, outcomes, verification methods, countries, IPs, and risk scores When filters (date range, user_id, action_type, outcome, verification_method, country, ip_prefix, amount range, risk_score >= threshold, device_fingerprint) are applied singly or combined Then the result set equals the intersection of criteria with total_count and page_count returned And sorting by timestamp desc/asc is available and stable within a page And exporting the current result set to CSV and JSON yields identical record counts and field values And exports of up to 100,000 records finish within 30 seconds and are paginated for larger sets And exported files include a header with export_time (UTC) and filter summary
Retention Policies and Purge Logging
Given a default retention of 24 months and tenant-level overrides allowed between 3 and 60 months When retention for tenant T is set to N months Then entries older than N months are purged within 24 hours by a scheduled job And each purge operation creates a "retention_purge" audit event including counts purged and time window And queries and exports exclude purged records after purge completion And reducing retention is preceded by a confirmation and grace period of 72 hours unless forced by an account Owner
High-Risk Denials and Repeated Failures Alerts
Given alerting is configured for tenant T with webhook URL and optional email/Slack And high-risk is defined as (risk_score >= 0.80) OR (3 or more failed verifications by the same user_id or device_fingerprint within 10 minutes) When such a condition occurs Then a webhook POST is sent within 10 seconds including event_id, tenant_id, user_id (hashed if required), action_type, outcome, risk_score, count_window, ip_address (redacted), country, and signature headers And non-2xx webhook responses trigger retries with exponential backoff up to 5 attempts and a final "delivery_failed" audit entry if not delivered And configured email/Slack notifications are delivered within 60 seconds with deduplication preventing duplicate alerts for the same incident within 5 minutes And all alert deliveries and retries are logged with status
Privacy Redaction and Hashing in Logs and Exports
Given an audit event is recorded Then the following fields are never stored or displayed in plaintext in reports or exports: end-user email, phone, verification codes, access tokens, card/bank identifiers And IP addresses are stored as both full value encrypted at rest and a report-visible redacted form (IPv4 /24, IPv6 /64) with only the redacted form used in UI and exports And device_fingerprint and risk_signals.detail values are stored as deterministic HMAC-SHA256 hashes scoped per tenant key to allow correlation without revealing raw values And amounts are stored and displayed with currency; no full PAN or bank account numbers are logged And no endpoint or export returns the full IP or PII fields; attempts to request them return HTTP 403 and are logged as "sensitive_data_blocked"
Admin Controls & Policy Thresholds
"As a studio owner, I want to configure when extra verification is required so that I can balance security with my team’s productivity."
Description

Add a Security Settings UI that lets tenant owners configure when step-up is required: payout initiation/approval, policy edits, bank account changes, bulk refund thresholds by count and amount, and access to admin-only screens. Allow selection of allowed methods (magic link, biometrics), define verification freshness windows, and set role-based exceptions. Provide safe defaults, inline guidance, and an audit log of policy changes. Include a preview mode showing estimated trigger rates and a test mode to validate flows before enabling for all users.

Acceptance Criteria
Security Settings UI Access & Safe Defaults
Given a tenant owner is authenticated, When they navigate to Settings > Security, Then the "Step-Up Verification Policies" section is visible. Given a non-owner attempts to access Security Settings, When they navigate to Settings > Security, Then access is denied and settings are not editable or visible. Given a new tenant with no prior configuration, When the owner opens Security Settings, Then safe defaults are pre-populated: step-up required for payout initiation, payout approval, bank account changes, policy edits, and admin-only screens; at least one method (magic link) enabled; a default freshness window is set; no role-based exceptions. Given safe defaults are present, When the owner selects "Restore defaults" and confirms, Then defaults are reapplied and persisted. Given inline help icons are present, When the owner taps an info icon, Then contextual guidance appears and does not block saving. Given settings are changed, When the owner selects Save, Then changes persist tenant-wide and are immediately effective (unless in Test Mode).
Configure Step-Up for Payouts and Bank Account Changes
Given the owner enables "Require step-up for payout initiation", When a user initiates a payout, Then a step-up challenge is required unless exempt by role and within freshness window. Given the owner enables "Require step-up for payout approval", When a user approves a payout, Then a step-up challenge is required unless exempt by role and within freshness window. Given the owner sets a minimum payout trigger amount X, When a payout is initiated with amount >= X, Then a step-up challenge is required; When amount < X, Then no step-up is triggered by this rule. Given the owner enables "Require step-up for bank account changes", When a user adds, edits, or removes a bank account, Then a step-up challenge is required before changes are committed. Given settings are updated, When the owner clicks Save, Then subsequent actions reflect the new rules without requiring users to log out/in.
Policy Edits and Admin-Only Screens Gating
Given the owner enables "Require step-up for policy edits", When any user attempts to modify Security Settings or payout/refund policy configuration, Then a step-up challenge must be completed before saving. Given the owner enables "Gate admin-only screens", When a user navigates to designated admin-only areas (e.g., Security Settings, Payouts Settings), Then a step-up prompt is shown prior to viewing if the user is outside the freshness window. Given a user has completed step-up, When they navigate between multiple admin-only screens within the freshness window, Then no additional prompt is displayed. Given the owner configures role-based exceptions for admin-only screen access, When an exempt role user navigates to those screens, Then they are not prompted within policy bounds; non-exempt roles are prompted.
Bulk Refund Thresholds by Count and Amount
Given the owner sets bulk refund thresholds N (count) and A (amount), When a bulk refund is initiated with total count >= N OR total amount >= A, Then a step-up challenge is required before execution. Given the bulk refund operation is below both thresholds, When initiated, Then no step-up is triggered by the bulk refund rule. Given the owner enters invalid thresholds (negative, non-numeric, or exceeding allowed limits), When saving, Then validation errors are shown and save is blocked. Given thresholds are changed and saved, When a user reopens the bulk refund dialog, Then the current effective thresholds are displayed. Given step-up is required for a bulk refund, When the user cancels the step-up flow, Then the bulk refund is not executed and no partial refunds are applied.
Allowed Verification Methods and Fallbacks
Given the owner selects allowed methods (magic link, biometrics), When saved, Then step-up prompts offer only the selected methods. Given biometrics is allowed but the device does not support or has disabled biometrics, When a step-up is required, Then the flow automatically offers magic link if allowed; otherwise the action is blocked with guidance. Given both methods are allowed, When a step-up is required, Then the user can choose between biometrics and magic link. Given the owner attempts to save with no methods selected, When saving, Then validation prevents save and instructs to select at least one method. Given magic link is selected, When triggered, Then a one-time link is sent to the verified contact on file and the UI indicates where it was sent without revealing full contact details.
Verification Freshness Window Configuration
Given the owner sets a verification freshness window W minutes, When a user completes step-up, Then protected actions performed within W minutes do not re-prompt. Given W minutes have elapsed since the last successful step-up, When a protected action is attempted, Then a new step-up is required. Given the owner enters a value for W outside the allowed range or a non-numeric value, When saving, Then a validation error is displayed and the value is not saved. Given the owner updates W and saves, When reviewing current sessions, Then the new window applies to future verifications and does not retroactively extend already-issued verifications.
Preview Mode, Test Mode, and Audit Logging
Given Preview Mode is enabled, When the owner adjusts any step-up setting, Then an estimated trigger rate is displayed for each rule based on recent activity and includes a data timestamp. Given there is insufficient historical activity, When in Preview Mode, Then the UI explains that estimates are unavailable due to low data volume. Given Test Mode is enabled (owner-only), When protected actions are performed by the owner, Then step-up prompts occur per the draft settings while other tenant users are unaffected. Given Test Mode is disabled and settings are published, When protected actions are performed by tenant users, Then prompts occur per published settings. Given any policy change is saved, When viewing the audit log, Then an immutable record exists with actor, timestamp, tenant, IP/device fingerprint, and before/after values of changed fields, and entries are filterable by date and actor.

Omni-Share Cards

Auto-builds rich, channel-optimized invite cards after checkout—complete with friend-first copy, applied discount, class details, and a live countdown badge for the 2-seat hold. One tap to send via SMS, WhatsApp, IG DM, or copy link. Result: faster shares, higher tap-through, and on-brand invites without manual editing.

Requirements

Post-Checkout Card Generation Engine
"As a purchaser, I want a ready-made invite card right after I book so that I can share it with a friend in seconds without typing or editing."
Description

Automatically generates a share-ready invite card payload immediately after successful checkout for the booked session. Compiles class essentials (title, date and time with timezone, location or livestream link, instructor name), friend-first copy, a deep link with signed parameters, an optional discount token reference, and the hold expiration timestamp for countdown rendering. Produces variants for supported channels and exposes them via a lightweight API for the confirmation screen and receipts. Ensures idempotency and retry safety, handles time zone formatting, and falls back to a waitlist invite if capacity is unavailable. Designed to integrate with seat hold and discount services while remaining resilient to network or downstream errors.

Acceptance Criteria
Generate card payload and expose via API after successful checkout
Given a booking with bookingId B for session S is marked 'paid' When the engine processes the successful checkout event for B Then it creates exactly one share card payload within 1500ms (P95) And the payload includes: bookingId, sessionId, classTitle, startAtIso (UTC), timeZoneDisplay, locationType, locationDisplayOrUrl, instructorName, friendCopy, deepLink, discountTokenRef (nullable), holdExpiresAtIso (nullable), channels[], schemaVersion And GET /share-cards?bookingId=B returns 200 and the payload body matches the published schema And the payload's classTitle, date/time, location/livestream link, and instructorName match the booked session S values
Produce channel-optimized variants for SMS, WhatsApp, IG DM, and Copy Link
Given a generated base payload for booking B with an active 2-seat hold When channel variants are built Then variants include exactly sms, whatsapp, ig_dm, copy_link And each variant contains title (optional), body, and url; body is non-empty; url is a valid HTTPS URL And sms.body length <= 320 characters and includes the deep link (shortened if available) and class title And whatsapp.body includes friend-first copy, class date/time, and the deep link And ig_dm.body includes the deep link and friend-first copy trimmed to <= 500 characters; if imageUrl is present, it returns HTTP 200 And copy_link.url equals the deep link with utm_source=copy_link And every variant appends utm_source={channel} and utm_medium=invite to the deep link And countdown metadata for all variants references holdExpiresAtIso
Deep link signing, discount token reference, and parameter integrity
Given a discount token D was applied to booking B When the engine generates the deep link for channel 'sms' Then the deep link contains signed parameters: bookingId=B, sessionId=S, channel=sms, discountTokenRef=D (optional), ts=issuedAt And the signature validates server-side and rejects if any parameter is tampered And resolving the link loads the share landing with the discount applied and shows the correct class details within 600ms TTFB (P95) And if no discount token is present, discountTokenRef is null and no discount language appears in friendCopy
Idempotency and safe retry on duplicate events
Given the successful checkout event for bookingId B is delivered multiple times or retried concurrently When the engine processes these events Then exactly one payload is created and the same payloadId is returned for all attempts And processing is idempotent based on (bookingId, sessionId) And no duplicate calls are made to downstream services for the same booking And the API responds 200 for duplicates with the original payload
Accurate time zone formatting and DST-safe timestamps
Given a session S scheduled in IANA zone America/Los_Angeles at a DST boundary instant When the payload is generated Then startAtIso equals the correct UTC instant with the proper offset for that occurrence And timeZoneDisplay uses the correct local abbreviation for that instant (PDT or PST as applicable) And the formatted date/time string includes weekday, month, day, local time, and tz abbreviation (e.g., Tue, Oct 14 · 6:00 PM PDT) And for livestream classes, locationType=livestream and locationDisplayOrUrl contains a valid HTTPS join URL; for in_person, it contains a human-readable address
Capacity unavailable: fall back to waitlist invite
Given capacity for session S is unavailable at card generation time or the 2-seat hold has expired and no seats remain When generating the payload Then friendCopy switches to waitlist messaging and discount language is omitted And the deep link resolves to the waitlist enrollment page for session S And discountTokenRef is null and holdExpiresAtIso is null And channel variants exist for all supported channels and include the updated link and copy
Resilience to downstream service errors and graceful degradation
Given any downstream dependency is failing or slow (discount service, seat hold service, image renderer) When generating the payload Then the engine returns a payload within 2000ms (P95) without throwing an error to the client And it retries each failing dependency up to 3 times with exponential backoff starting at 200ms And if a dependency still fails: discountTokenRef is omitted and discount language removed; holdExpiresAtIso omitted and countdown metadata removed; imageUrl omitted if image render fails And all degradations are logged with correlationId and error codes, and a background reconciliation job is queued to patch the payload once the dependency recovers
Channel-Optimized Templates and One-Tap Share
"As a purchaser on my phone, I want one-tap share options for SMS, WhatsApp, IG DM, and copy link so that I can send the invite through whatever app my friend uses."
Description

Delivers per-channel invite templates tailored to SMS, WhatsApp, Instagram DM, and copy link, optimizing message length, line breaks, emojis, and preview text to increase tap-through. Implements one-tap sharing via native share sheets and channel-specific deep links, with graceful fallbacks to copy link when an app is unavailable. Applies short links and channel-tagged parameters for analytics while preserving readability. Supports iOS, Android, and mobile web with locale-aware date and time formatting, and ensures reliability with offline queueing and retry on send failures.

Acceptance Criteria
SMS and WhatsApp One-Tap Share Templates
Given a user completes checkout and taps Share for SMS or WhatsApp When the selected channel is SMS on iOS or Android Then the native SMS composer opens prefilled with: friend-first copy, class title, locale-formatted date/time, applied discount label, and a short link And the SMS body is <= 300 characters excluding the short link, uses <= 2 line breaks, and <= 2 emojis And the short link contains channel= sms and platform parameters and is placed on the last line When the selected channel is WhatsApp and the app is installed Then WhatsApp opens with a prefilled message where the first line summary is <= 80 characters, total lines <= 4, and includes the short link with channel= wa And tapping Send in either channel successfully transmits the prefilled content without truncation of the short link
Instagram DM Share with Clipboard Fallback
Given a user completes checkout and taps Share via Instagram DM When Instagram is installed and the OS supports sharing to Instagram Then the system share sheet presents Instagram as a target and, upon selection, Instagram DM opens with the link attached And if text prefill is not supported by Instagram, the full message (text + short link) is copied to clipboard and a toast "Message copied—paste in Instagram" is shown within 1 second And the short link includes channel= ig and resolves correctly when tapped from within Instagram
Mobile Web Share Sheet and Clipboard Fallback
Given a user is on mobile web and views the Omni-Share Card after checkout When the device/browser supports the Web Share API (Level 2) Then invoking Share opens the native share sheet with prefilled title and text and includes the short link When the Web Share API is not available Then a Copy Link modal is shown with the composed message and a primary Copy action And tapping Copy places the full message on the clipboard within 300 ms and shows a success toast within 1 second
App Unavailable or Share Failure Fallback to Copy Link
Given a user taps a specific channel button (SMS, WhatsApp, Instagram) When the corresponding app is not installed or the deep link/share invocation fails Then the product falls back to the Copy Link modal without app crash or freeze And the modal displays the same channel-optimized message and short link And the event is logged with channel= link and reason= fallback And the user can successfully copy the message and continue sharing
Short Links with Channel Analytics Tags
Given any channel share is initiated When the short link is generated Then the visible URL is a branded short domain <= 24 characters and contains opaque path only (no visible UTM in message) And the underlying target URL includes analytics tags for channel, platform, locale, and campaign identifiers When a recipient opens the short link Then redirection to the booking page completes within 500 ms at p95 and analytics are recorded for the correct channel
Locale-Aware Date and Time Formatting in Templates
Given the device locale and timezone are known When composing the per-channel message Then date and time are formatted according to the device locale (e.g., 24h vs 12h, month/day order) and include a timezone abbreviation (e.g., 7:30 PM PT) And numerals and punctuation render correctly for RTL locales And if the locale is unsupported, the format defaults to en-US without placeholders showing
Offline Queue and Retry on Send Failures
Given the device is offline or the short-link service is temporarily unavailable When the user taps any share option Then the share payload (channel, message text, class metadata) is queued locally and the user sees "Will send when back online" within 1 second And short-link generation is retried automatically up to 3 times or upon connectivity restoration within 24 hours And once the short link is created, an in-app banner "Ready to share to [Channel]" appears; tapping it opens the native composer with the prefilled message And if retries are exhausted, the user is prompted with a Copy Link option and the outcome is logged
Live Hold Countdown and Seat Locking
"As an instructor, I want the invite to hold seats and show a live countdown so that invites drive fast decisions without overbooking my class."
Description

Creates a time-bound seat hold for two seats on checkout, tied to the referrer booking and class session, reserving inventory without overbooking. Exposes a synchronized expiration timestamp used to render a real-time countdown badge on the invite card and on the landing page, with periodic clock drift correction. On expiration, automatically releases seats and switches the landing experience to a waitlist or alternative session suggestion. Handles edge cases including low inventory, duplicate links, and concurrent opens, and records hold lifecycle events for audit and analytics.

Acceptance Criteria
Create 2-Seat Hold on Successful Checkout
Given a referrer completes checkout for class session S and available seats >= 2 When the booking B is confirmed Then a hold H for exactly 2 seats is created linked to booking_id B and session_id S And H.status = "active" and H.seats_held = 2 And H.expires_at = H.created_at + configured hold_ttl_minutes And session S available inventory is decremented by 2 immediately And hold creation and inventory decrement are atomic so total seats held across concurrent checkouts never exceed available inventory And the API response includes hold_id, seats_held=2, expires_at, and status="active"
Expose Synchronized Expiration Timestamp
Given an active hold H exists When requesting the Share Card payload or Landing Page Prefetch API for booking B Then the response includes hold_id, hold_expires_at (ISO 8601 UTC), and server_now (ISO 8601 UTC) And hold_expires_at remains constant across repeated requests for the same hold unless the hold state changes And hold_expires_at equals H.created_at + TTL within 1 second And the payload includes session_id and seats_held for verification
Countdown Badge Rendering with Drift Correction
Given the client receives server_now and hold_expires_at for hold H When rendering the countdown badge Then the remaining time is displayed in mm:ss and updates at 1 Hz until it reaches 00:00 And the client recalibrates time drift at least every 30 seconds and on visibility changes And if measured drift exceeds 1 second, the countdown is corrected without page reload Given the hold reaches expiration while the page is open When remaining time hits zero Then the UI switches to the expired state within 1 second based on the hold status endpoint
Hold Expiration: Release Inventory and Landing Fallbacks
Given an active hold H for 2 seats exists and fewer than 2 seats have been redeemed When H.expires_at is reached Then all unredeemed seats are released back to session inventory within 2 seconds And H.status transitions to "expired" idempotently Given a recipient opens the landing page with an expired hold and session inventory == 0 When the page loads Then the waitlist opt-in flow is shown as the primary path And no countdown or "2-seat hold" badge is displayed Given alternative upcoming sessions for the same class exist within the next 14 days When the landing page loads with an expired hold Then at least 3 alternative sessions (or all if fewer) are shown sorted by soonest start time with price, start time, and a Book CTA
Low Inventory at Hold Creation
Given available seats == 1 at the moment the referrer booking is confirmed When attempting to create a 2-seat hold Then no hold is created and no inventory is decremented for a hold And the Share Card is generated without a countdown badge and uses waitlist/alternative-session copy And an event "hold_not_created" is logged with reason="insufficient_inventory" and available_seats=1 Given available seats == 0 at confirmation When attempting to create a 2-seat hold Then behavior is identical with reason="sold_out"
Duplicate Links and Concurrent Opens
Given a single invite link token T references hold H When multiple recipients open T concurrently within the TTL Then all recipients see the same hold_id and synchronized countdown And at most two redemptions can succeed; subsequent redemption attempts return error code "hold_exhausted" and are routed to waitlist/alternatives Given the same token T is opened in multiple tabs by the same user When a redemption completes in any tab Then other tabs reflect the updated remaining seat count within 2 seconds or on next poll, whichever occurs first
Lifecycle Event Logging for Audit and Analytics
Given hold lifecycle transitions (created, extended, partially_redeemed, fully_redeemed, expired, released, not_created) When any such event occurs Then a durable event record is written with fields: event_type, hold_id, booking_id, session_id, account_id, timestamp (UTC ISO 8601), actor, seats_held, seats_redeemed, reason (nullable), and request_id And events are idempotent on (event_type, hold_id, request_id) And events are queryable via analytics within 5 minutes of occurrence
Auto-Applied Guest Discount Linkage
"As an invitee, I want the discount to apply automatically from the invite link so that checkout is quick and I’m sure I’m getting the promised deal."
Description

Generates a single-use guest discount that is automatically applied when the invitee arrives via the shared link, without manual codes. Binds the discount to the class or eligible catalog, the referrer booking, and an expiration aligned to the seat hold. Validates and applies the discount during checkout, displays strikethrough pricing and savings, and provides clear messaging if the discount has expired or is ineligible. Enforces fraud controls by signing and scoping tokens, limiting use to one redemption, and preventing referrer self-use, while remaining compatible with existing taxes, fees, and payment flows.

Acceptance Criteria
Auto-Apply Discount Within Hold Window
Given an invitee opens the shared link containing a valid, signed token bound to a specific class or eligible catalog and the referrer booking And the current server time is before the token’s expiry (aligned to the 2-seat hold) When the booking page loads Then the discount is automatically applied without manual code entry And the UI displays strikethrough original price, discounted price, and the savings amount in currency And a live countdown shows remaining time matching the token expiry to the second And the discount persists through checkout and payment confirmation if payment completes before expiry
Expired Discount Handling
Given an invitee opens the shared link after the token’s expiry time OR the countdown expires before payment confirmation When the booking page or checkout recalculates pricing Then the discount is not applied and all prices reflect standard rates And a visible message states the guest offer has expired and the user may proceed at standard price And the countdown badge is removed or set to 00:00 And an audit log records reason=expired with link token id and timestamp
Eligibility Scope Enforcement
Given the shared link token is scoped to a specific class or to defined eligible catalog tags When the invitee views an ineligible class/item Then no discount is applied and no strikethrough pricing is shown And a message indicates the offer applies only to the eligible class/catalog When the invitee navigates to an eligible class within the same session and token remains unexpired Then the discount auto-applies there with correct pricing display
Single-Use Token Consumption
Given the shared link token has not yet been redeemed When an invitee completes a successful payment using the auto-applied discount Then the token is marked redeemed server-side atomically with the transaction And subsequent visits using the same link do not apply a discount And the UI shows a message that the offer has already been used, allowing checkout at standard price And the redemption record links the redeemed guest order to the referrer booking id
Referrer Self-Use Block
Given the referrer opens the shared link while authenticated as the referrer account OR enters the same email/phone used on the referrer booking during checkout When eligibility is evaluated on page load and before payment authorization Then the discount is not applied or is removed prior to payment And a message indicates the offer is for guests only and the referrer cannot redeem their own invite And the referrer may continue to book at standard price
Token Signature and Tamper Protection
Given the shared link includes a signed token with scope, referrer booking id, and expiry When the token signature is invalid, the payload has been altered, the referenced booking is not found/voided, or the token is otherwise malformed Then the discount is not applied And the UI shows a generic invalid offer message without revealing technical details And a security/audit event is recorded with reason=invalid_signature|tampered|booking_not_found And rate limits prevent repeated invalid token validations from the same IP/device within a short window
Pricing, Tax, and Payment Flow Compatibility
Given the discount applies to the eligible item’s subtotal per business rules When the invitee checks out using any supported payment method (saved card, Apple Pay, Google Pay) Then taxes and fees are computed on the discounted amount according to jurisdictional rules And the order summary, receipt, and confirmation reflect original price, discount line, taxes/fees, and final total with no double-discounting And the payment authorization and capture succeed with the discounted total And declines or retries preserve the discount if still within expiry; otherwise revert to standard pricing with clear messaging
Brand Theming and Asset Injection
"As a studio owner, I want invite cards to match my brand and tone so that shares look professional and build trust."
Description

Applies studio or instructor branding to all invite cards and landing previews, including logo, color palette, fonts, and tone-of-voice copy templates. Provides an admin editor to customize default friend-first copy per channel with a live preview, and auto-generates Open Graph visuals for link previews using branded assets and class details. Ensures accessibility with contrast and font size best practices, supports dark mode variants, and serves assets via CDN with cache busting and safe fallbacks to ClassNest defaults when brand elements are missing.

Acceptance Criteria
Branded Styling Applied to Share Cards and Landing Previews
- Given an instructor account with saved logo, primary/secondary colors, and font selections, When a share card is generated post-checkout for any channel, Then the card renders the logo in the header, applies the primary color to CTAs and accents, uses the selected fonts for headings and body, and matches the brand palette on both mobile and desktop previews. - Given branded theming is enabled, When the landing preview loads from the share link, Then the page uses the same logo, colors, and fonts consistently and passes a visual regression check with a tolerance ≤ 0.5% pixel diff against the approved snapshot. - Given fonts are self-hosted or via provider, When fonts fail to load within 1 second, Then the system falls back to safe web fonts per brand mapping without layout shift > 100 ms or CLS > 0.05.
Channel-Specific Friend-First Copy Editor with Live Preview
- Given an admin opens the Copy Templates editor, When selecting SMS/WhatsApp/IG DM/Link channel, Then the editor shows the channel-specific default copy with allowed tokens and a character counter; saving updates the template in the datastore and increments the template version. - Given a template containing tokens {class_title}, {start_time_local}, {discount_name}, {discount_amount}, {hold_minutes}, {share_url}, When preview is refreshed, Then tokens resolve using the selected sample class and locale, and the preview matches channel constraints (e.g., SMS ≤ 160 characters without truncation of the link). - Given an invalid template (unsupported token or exceeding max length per channel), When saving, Then the editor blocks save, highlights the invalid segments, and displays actionable error messages. - Given a template is updated and saved, When a new share card is generated, Then the new copy is used for new shares while previously sent shares remain unchanged (non-retroactive).
Branded Open Graph Image Generation for Link Previews
- Given a share link is requested by a social crawler, When the HTML is served, Then it includes og:title, og:description, og:image, og:url, and twitter:card=summary_large_image with values derived from class details and brand templates. - Given branded assets exist, When the OG image is generated, Then the image is 1200x630 in PNG or JPEG ≤ 300 KB, includes logo, brand colors, class title, date/time, and discount badge, and responds with HTTP 200 within 400 ms p95. - Given brand assets are missing or invalid, When generating the OG image, Then the system uses ClassNest default theme and a generic layout, and all required metadata remains present and valid. - Given cache busting is enabled, When the template or assets change, Then the og:image URL changes via a content hash and social debuggers retrieve the updated image successfully.
Accessible Theming Meets WCAG AA
- Given any branded color palette, When applied to text and backgrounds, Then contrast ratios meet WCAG 2.1 AA (≥ 4.5:1 normal text, ≥ 3:1 large text) and automated checks via axe-core report zero color-contrast violations on share cards and landing previews. - Given brand font selections, When rendered, Then base body text is ≥ 16 px, supports user zoom to 200% without loss of content or functionality, and focus indicators maintain ≥ 3:1 contrast. - Given share card and landing preview components, When navigated using keyboard only, Then all interactive elements are reachable in logical order, operable, and announced to screen readers with meaningful labels (logo alt text, CTA purpose, countdown). - Given insufficient contrast is detected during theme build, When adjustments are required, Then adaptive shade/tint adjustments are applied automatically to meet AA and the admin is informed that contrast adjustments were applied.
Dark Mode Theme Variant
- Given a device or browser prefers-color-scheme: dark, When a share card or landing preview is loaded, Then a dark variant is applied using brand palette transformations while preserving WCAG AA contrast compliance. - Given the user switches between light and dark modes, When toggling is simulated, Then the theme transitions without flash of unstyled content (FOUC) and maintains an appropriate logo variant (inverted or alternate) if provided. - Given no dark-specific assets exist, When rendering dark mode, Then the system auto-adjusts colors (e.g., tint/invert) and swaps the logo to a safe monochrome fallback to maintain legibility.
CDN Delivery, Cache Busting, and Safe Fallbacks
- Given branded assets (logo, font files, OG images), When requested by clients or crawlers, Then they are served from the configured CDN domain over HTTPS with cache-control: immutable and max-age ≥ 7 days, using versioned URLs that contain a content hash. - Given an asset is updated in the admin, When saved, Then a new hashed URL is generated, a CDN purge is triggered for the prior path, and subsequent requests use the new URL within 2 minutes. - Given an asset URL fails (404/5xx or timeout > 500 ms), When rendering share cards or previews, Then the system falls back to ClassNest default assets without user-facing errors and logs the incident with severity=warning including request ID and asset key. - Given global traffic, When assets are requested from target regions, Then CDN edge hit ratio is ≥ 95% and p95 asset load time is ≤ 250 ms.
Share Attribution and Conversion Analytics
"As a studio owner, I want to see which channels and invites convert so that I can optimize promotions and grow bookings."
Description

Tags every invite with a share identifier, referrer booking, and channel, and captures key events across the funnel including card generated, share tapped, landing viewed, checkout started, purchase succeeded, hold expired, and waitlist joined. Attributes revenue and conversions to the original purchaser and channel, presents channel performance and cohort trends in the dashboard, and supports exports and webhooks for external analytics. Complies with privacy requirements by avoiding PII in URLs, honoring consent, deduplicating sessions, and applying basic anti-fraud heuristics.

Acceptance Criteria
Unique Share ID and Channel Tagging on Invite Cards
Given a purchaser completes checkout for a class When the Omni-Share Card is generated Then a unique, opaque share_id is created and persisted server-side and is linked to the referrer_booking_id without exposing PII And the generated share URL includes share_id and channel parameters and contains no PII (e.g., no names, emails, or phone numbers) And selecting a specific channel button (SMS, WhatsApp, IG DM, Copy Link) sets the correct channel parameter value in the URL And share_id remains the same across all channel buttons for the same card and booking And all generated URLs pass validation against the defined URL schema and signing (if enabled)
End-to-End Funnel Event Capture
Given a share card exists with a valid share_id When a recipient taps the invite link Then an event share_tapped is recorded with timestamp, share_id, channel, and user-agent metadata When the landing page renders client-visible content Then an event landing_viewed is recorded with session_id and consent status When the recipient initiates checkout from the landing Then an event checkout_started is recorded with share_id, channel, class_id, and session_id When payment completes successfully Then an event purchase_succeeded is recorded with order_id, gross_amount, net_amount, currency, share_id, channel, and referrer_booking_id When the 2-seat hold timer expires without purchase Then an event hold_expired is recorded with share_id and class_id When the recipient joins the waitlist from the landing Then an event waitlist_joined is recorded with share_id, channel, class_id, and session_id And all events are emitted at-most-once per session via idempotency keys and delivered with 99% within 5 seconds, with retries on transient failures
Attribution, Deduplication, and Conversion Window
Given a recipient taps an invite link containing share_id and channel And subsequently purchases within the configurable attribution window (default 7 days) When the purchase succeeds Then the conversion and revenue are attributed to the original purchaser (referrer) associated with the share_id and to the last tapped channel within the window And self-referrals (same customer account or same authenticated user as the referrer) are excluded from attribution And multiple purchases initiated from the same share_id are each attributed independently And multiple taps within the same session are deduplicated so only one landing_viewed and one share_tapped are counted per session And cross-device conversions using the same share link still attribute via share_id even if session_id differs And purchases without a share_id (organic) are not attributed to any referrer or channel
Dashboard Channel Performance and Cohort Trends
Given tracked events exist within a selected date range When the instructor views the Analytics dashboard Then the Channel Performance table shows, per channel, counts for cards_generated, shares_tapped, landings_viewed, checkouts_started, purchases_succeeded, revenue, waitlist_joined, and hold_expired, plus conversion rates from tap->purchase and landing->purchase And filters for date range, class, and channel update all charts/tables consistently within 5 seconds And a Cohort Trends view groups by referrer purchase week and month and reports invite-to-purchase rate, average revenue per referrer, and average time-to-conversion And all displayed totals reconcile to exports within 1% for counts and within 0.5% for revenue And all time series respect the account’s timezone setting
Exports and Webhooks for External Analytics
Given the user requests an export from the dashboard with applied filters When the CSV is generated Then it includes one row per event with columns: timestamp_iso, event_type, share_id, channel, referrer_booking_id, class_id, session_id, order_id (nullable), gross_amount, net_amount, currency, attributed (boolean), and consent_status And the export respects the selected date range, class filters, and timezone And files over 50MB are delivered via asynchronous download link with expiration and email notification Given webhooks are configured with a signing secret and endpoint URL When any tracked event occurs Then a JSON payload with event body and HMAC-SHA256 signature header is POSTed within 5 seconds And non-2xx responses are retried with exponential backoff for up to 24 hours with idempotency keys And webhook delivery logs are viewable with status, attempts, and last error
Privacy, Consent, and Anti-Fraud Protections
Given a share link is generated Then the URL contains no PII (no names, emails, phone numbers, or raw customer IDs) and uses opaque identifiers only Given a recipient lands on the page with consent not yet granted for analytics When the page loads Then only essential events needed for service operation are recorded with minimal metadata, and non-essential analytics events are deferred until consent is granted And if consent is denied, non-essential tracking events are not recorded and are excluded from attribution and reporting And Do Not Track signals are honored by disabling non-essential tracking And basic anti-fraud heuristics exclude from attribution: self-referrals, excessive taps (>20/min) from the same IP/device-userAgent pair, and purchases detected from the referrer’s authenticated session And excluded events are flagged and visible in admin audit logs
Latency, Reliability, and Data Consistency
Given normal operating conditions When events are produced Then 95th percentile ingestion latency is under 2 seconds and 99th percentile under 5 seconds And the analytics dashboard reflects new events within 5 minutes (end-to-end freshness SLO) And the event pipeline guarantees at-least-once delivery with idempotent processing to yield exactly-once effects in storage And the system maintains 99.9% monthly uptime for event collection endpoints And in case of outage, backfill replays all queued events without duplication, with completeness verified by counts per event_type matching source logs within 0.5%

Invite Tracker

Shows real-time status for each buddy invite (Delivered, Viewed, Tapped, Booked, Expired) right on the confirmation screen and roster. Offers one-tap actions—Nudge, Resend, Swap Friend, or Extend Hold (if enabled)—so you can salvage invites without chasing messages. Eliminates guesswork, boosts conversions, and saves time.

Requirements

Real-time Invite Status Pipeline
"As an instructor, I want to see each buddy invite’s live status as it changes so that I can decide when to intervene and recover the booking before the hold expires."
Description

Implements end-to-end event capture and a state machine to show each invite's live status (Delivered, Viewed, Tapped, Booked, Expired) on confirmation screens and rosters. Ingests delivery/open/click/book events from SMS/email providers and ClassNest booking flows via webhooks and internal events, deduplicates idempotently, and persists timestamps per status. Pushes incremental updates to clients via WebSocket/SSE for instant UI refresh. Handles expirations with scheduled jobs, retries on provider failures, and backfills status after outages. Supports multi-channel invites and multiple invites per booking with clear attribution to inviter and invitee. Exposes a performant read model optimized for roster/confirmation queries.

Acceptance Criteria
Real-time status updates to confirmation and roster
Given an invite exists and a user is viewing the related confirmation screen or roster with an active WebSocket or SSE connection When a Delivered, Viewed, Tapped, Booked, or Expired event for that invite is ingested and persisted Then the confirmation screen and roster display the new status label and its timestamp within 2 seconds (p95) and 5 seconds (p99) without page refresh And exactly one incremental update message is delivered per status change per client session And the update includes the correct channel badge (SMS/Email) and invitee identifier matching the invite record
Idempotent, deduplicated event ingestion
Given a delivery/open/click/book event with the same provider event id or dedupe key is received 0–10 times over 24 hours When the events are processed Then only the first occurrence applies a single state transition and updates the relevant status timestamp once And subsequent duplicates are acknowledged without any additional state change, side effect, or duplicate audit record And the handler returns a 2xx response on duplicates to prevent further retries
Out-of-order and late event reconciliation
Given events for an invite can arrive out of order (e.g., click before delivery) or late (up to 7 days after occurrence) When such events are ingested Then per-status timestamps are stored using the event occurrence time from the payload And the current status shown is the most advanced by precedence Booked > Tapped > Viewed > Delivered > None And the current status never regresses due to a less advanced late event And earlier-state timestamps are preserved if previously recorded
Expiration scheduling and hold extension
Given an invite has an expiration time and hold extension is enabled for the class When the expiration time is reached and no booking has occurred Then the status transitions to Expired within 60 seconds (p95) of the scheduled time and the held seat is released atomically And connected clients receive the Expired update within 2 seconds (p95) And if Extend Hold is triggered before expiration, the expiration time is updated and no Expired transition occurs And an audit entry records the expiration or extension with actor, timestamp, and prior values
Provider failure handling and retry/backoff
Given the provider webhook delivers events that intermittently fail due to transient errors (HTTP 5xx/429) or timeouts When processing such events Then the system behaves idempotently and signals retry by returning non-2xx only when processing truly fails, and 2xx when safely deduped/applied And dependency calls (e.g., provider lookups when needed) use exponential backoff with jitter for up to 30 minutes or 5 attempts (whichever first) And no data loss or duplicate state transitions occur during a simulated retry storm of 100 duplicate deliveries
Outage backfill and reconciliation
Given a provider webhook outage causes a 1-hour gap of missed events When the backfill job runs for the affected time window and invite cohort Then missing delivery/open/click/book events are fetched via provider APIs, reconciled by dedupe keys, and applied idempotently And after backfill completion, the read model’s per-invite current status and per-status timestamps match the provider’s event ledger for the window And operational metrics report zero unreconciled invites and the job duration is under 15 minutes for up to 10k events
Roster/confirmation read model performance and attribution
Given a class with up to 200 invites across SMS and Email and multiple invites per booking When the roster and confirmation pages query the read model by classId or bookingId Then results include one record per invite with inviterId, invitee identifier, channel, current status, and per-status timestamps And queries return in ≤120 ms p95 and ≤250 ms p99, support pagination and sorting by latest status change, and return only fields required by the UI And attribution remains correct when multiple invites exist for the same booking and statuses change independently
One-Tap Recovery Actions
"As a studio owner, I want one-tap actions to recover stalled invites so that I can convert more seats without chasing messages manually."
Description

Adds context-aware action buttons—Nudge, Resend, Swap Friend, and Extend Hold—next to each invite on confirmation screens and rosters. Enforces business rules (cooldowns, daily limits, channel eligibility, and permission checks), and logs all actions for auditability. Nudge sends a reminder using the original channel with updated copy; Resend issues a fresh invite link and revokes the prior link; Swap Friend replaces the invitee while preserving the seat hold and attribution; Extend Hold increases the expiration time when enabled by studio policy and seat availability. Provides optimistic UI states with eventual consistency, clear error/success toasts, and undo where safe. All actions are idempotent, rate-limited, and localized.

Acceptance Criteria
Nudge: Original Channel, Cooldowns, Updated Copy
Given an invite in Delivered, Viewed, or Tapped status with a valid originating channel and the configured cooldown has elapsed When the host taps Nudge Then a reminder is sent via the original channel with updated copy that reflects current time-to-expiry And the Nudge button enters a Sending state within 300ms and resolves within 5s on success And an audit log entry is recorded with actor, inviteId, channel, templateId, and timestamp And if cooldown has not elapsed or the channel is missing, the action is blocked, a descriptive error toast is shown, and no message is sent
Resend: Fresh Link Issued, Prior Link Revoked
Given an active invite that is not Booked or Expired When the host taps Resend Then a new unique invite token is generated and delivered via the original channel And all previously issued tokens for this invite are revoked within 5s And attempts to use a revoked token return an "Invite expired or replaced" response and cannot book And an audit log records token revocation and new token issuance And success or error is surfaced via toast; the UI does not duplicate actions on refresh or retry
Swap Friend: Replace Invitee, Preserve Hold & Attribution
Given a held-seat invite for Invitee A and a replacement contact B selected by the host When the host taps Swap Friend Then Invitee A’s invite is revoked and B is issued a new invite bound to the same seat hold with the remaining hold time preserved And booking attribution (referrer, campaign, class, host) is preserved on the new invite And an audit log captures fromInviteeId, toInviteeId, seatId, previousExpiry, newInviteId, and actor And if A has already booked or B already has a booking for the same class, the swap is blocked with an explanatory error toast and no changes are persisted
Extend Hold: Policy, Availability & Limits
Given studio policy enables hold extension and the invite has not expired When the host taps Extend Hold Then the invite expiration increases by the configured increment without exceeding the studio’s maximum extension limit And if the maximum is reached or policy is disabled, the action is blocked and a clear error is shown And the new expiration time is reflected in the UI within 5s and logged with previous and new timestamps And the action is only available when seat status allows maintaining the hold
Permission & Channel Eligibility Gating
Given a host’s role permissions and the invite’s available contact channels When the confirmation screen or roster renders Then only actions the host is permitted to perform are visible and enabled And Nudge/Resend are shown only when a valid originating channel exists; Extend Hold is shown only when policy allows; Swap Friend requires access to the client list And the server enforces the same checks; unauthorized attempts return 403 and are audit-logged without side effects
Resilience: Idempotency, Rate Limits & Daily Limits
Given intermittent network or double-taps on any action When the action request is retried or the button is tapped multiple times within a short window Then exactly one side-effectful operation is performed; subsequent duplicates return a no-op response and the UI remains consistent And per-invite cooldowns and studio-configured daily limits are enforced; attempts beyond limits are blocked with an informative toast and logged And metrics counters increment once per successful action execution
UX Feedback: Optimistic State, Toasts & Undo (Localized)
Given any one-tap action is initiated When the request is sent Then an optimistic state appears within 300ms and is reconciled with the server response; on failure, the UI reverts to the pre-action state And a localized success or error toast is displayed; text and button labels honor the user or studio locale with English fallback And an Undo option is presented for safe actions (Extend Hold, Swap Friend) for 10s, and is disabled if a booking or conflicting change occurs during the window
Unique Deep-Link Invites & Tracking
"As an invited friend, I want a single link that takes me directly to my held spot so that booking is fast and error-free on my device."
Description

Generates signed, expiring deep links per invite that attribute clicks and bookings to the inviter and class, with channel-specific parameters for SMS and email. Clicks route through a tracking redirect that records Tapped status, handles device detection, and deep-links into the app or responsive web booking flow with prefilled class/seat/price and invite token. Email invites include open tracking for Viewed, with privacy-safe fallbacks. Links can be revoked/rotated on Resend, and expire automatically or on cancellation. Implements anti-abuse (token scoping, short TTL, rate limiting) and complies with consent and unsubscribe rules. Captures UTM/source data for analytics.

Acceptance Criteria
Signed, Expiring Deep Link Generation and Validation
Given an instructor creates an invite for a specific class, seat, and price via SMS or email When the system generates the invite URL Then the URL contains a unique signed token scoped to the invite_id, class_id, seat_id (or seat hold), inviter_id, channel, and price And the token includes an expiry timestamp within the configured TTL And server-side verification rejects any tampered or unsigned token with HTTP 400 and logs the event And a valid, unexpired token resolves successfully and exposes no PII outside of ids and non-sensitive metadata
Tracking Redirect Records Tapped and Routes by Device
Given a recipient taps a valid invite link on any device When the tracking redirect processes the request Then it records a Tapped event with invite_id, channel, timestamp, and user agent And it performs device detection and routes to the native app deep link if installed, otherwise to the responsive web booking page And the destination view is prefilled with class, seat (or held seat), price, and invite token And UTM/source parameters are persisted through to the destination and data layer for analytics And duplicate taps from the same device within 10 minutes are counted once for analytics while retaining raw click logs
Email Viewed Tracking with Privacy-Safe Fallbacks
Given an email invite is sent with an open-tracking pixel and a unique invite link When the email is opened in a client that permits pixel loads Then the system records Viewed once for that invite And when Mail Privacy Protection or similar blocks pixel loads Then the system records Viewed upon the first click-through of the invite link from the email channel without double-counting subsequent clicks And taps from non-email channels do not set Viewed
Booking Attribution and Prefill Integrity via Deep Link
Given an invitee completes a booking after arriving through a valid invite link When checkout is submitted Then the booking is attributed to the inviter_id and class_id from the token And the applied price matches the price encoded in the token (or is rejected if mismatched) And the seat selection respects the token’s seat/hold scope (or is rejected if unavailable/out of scope) And an InviteBooked analytics event is emitted with invite_id, channel, and UTM parameters
Link Expiry and Cancellation Enforcement
Given an invite link has reached its TTL or the invite/class has been canceled When the link is accessed Then the system returns an Expired state (HTTP 410 or equivalent) and prevents booking And the invite’s status updates to Expired in the tracker And an InviteExpired analytics event is logged with reason (ttl|canceled)
Resend Rotates Token and Revokes Prior Links
Given the inviter uses the Resend action for an existing invite When a new link is generated Then a new signed token is issued with a fresh expiry and unique URL And all prior tokens for that invite are revoked and return HTTP 410 Revoked if accessed And the tracker history shows the resend event with timestamp and new URL And resends are rate-limited per invite (e.g., max 3 per 10 minutes), returning a clear error when exceeded
Consent, Unsubscribe, and Anti-Abuse Controls
Given an instructor attempts to send invites via SMS or email When the recipient has not provided required consent or has unsubscribed/opted out Then the system blocks sending and surfaces a clear reason with remediation guidance And all outbound SMS include STOP instructions and all emails include functional unsubscribe links And STOP/unsubscribe events immediately prevent future sends to that recipient And invite link/token creation and taps are rate-limited per inviter and per invitee to mitigate abuse, returning HTTP 429 on excess And tokens use short TTL per policy and are scoped to prevent reuse across classes or accounts
Roster and Confirmation UI with Status Chips
"As an instructor, I want a clear, real-time view of invite statuses and quick actions on the roster so that I can manage my class efficiently from my phone."
Description

Enhances the confirmation screen and class roster with status chips (Delivered, Viewed, Tapped, Booked, Expired), timestamps, and per-invite action menus. Supports real-time updates via WebSocket/SSE, bulk filters (e.g., Show stalled invites), and search by invitee name/phone/email. Provides accessible, mobile-first designs with clear color semantics and tooltips explaining each state. Includes empty, loading, and error states; skeletons for fast perceived performance; and localization for copy. Integrates with existing booking detail modals and message history views for context.

Acceptance Criteria
Real-time Status Chips on Confirmation and Roster
- Given invites exist, when the confirmation screen or roster loads, then each invite row shows exactly one status chip from {Delivered, Viewed, Tapped, Booked, Expired} with a relative timestamp (e.g., "3m ago"). - Given a chip is focused/hovered/long-pressed, when the tooltip appears, then it explains the state and shows an absolute timestamp localized to the user’s timezone. - Given color semantics, when chips render, then Booked=green, Viewed/Tapped=amber, Delivered=gray, Expired=red, all with contrast >= 4.5:1 and non-color indicators (icon/text). - Given an invite row, when the info icon or chip is tapped/clicked, then the booking detail modal opens prefiltered to the invitee with a link to open the message history view for context.
Per-Invite Action Menu
- Given an invite row, when the overflow/menu button is activated, then the menu shows actions: Nudge, Resend, Swap Friend, and Extend Hold (only if holds are enabled and invite not expired). - Given state constraints, when an action is not applicable (e.g., Expired cannot Extend Hold), then that action is hidden or disabled with an accessible tooltip explaining why. - Given an action is confirmed, when it completes, then the UI shows a success toast, the status/timestamp update accordingly, and the action is logged in message history with actor, time, and outcome. - Given an action fails, when the server returns an error, then an error alert with retry is shown and no duplicate events are recorded. - Performance: menu opens within 150ms on mid-tier mobile devices; actions respond (success/failure) within 2s or show a non-blocking progress indicator.
Live Updates via WebSocket/SSE
- Given an active WebSocket/SSE connection, when an invite status changes on the server, then the corresponding chip and timestamp update in the UI within 2 seconds without a page refresh on both confirmation and roster screens. - Given transient network loss, when the connection drops, then the client retries with exponential backoff and falls back to 15s polling after 3 failed attempts, showing a non-intrusive connectivity banner. - Given duplicate or out-of-order events, when updates arrive, then idempotency by inviteId+version ensures the latest serverTimestamp wins and no flicker occurs. - Given reconnection, when back online, then the UI reconciles missed updates by fetching the latest state and clearing the connectivity banner.
Findability: Filters and Search
- Given the filter control, when opened, then options include: All, Delivered, Viewed, Tapped, Booked, Expired, and Show stalled invites. - Given Show stalled invites, when applied, then the list shows invites with no status change for >= 60 minutes and not in Booked or Expired states. - Given the search input, when typing, then results debounce at 300ms and match on name (case/diacritics-insensitive), phone (digits-only match), and email (case-insensitive), combined with the current filter (AND logic). - Given a clear action, when search is cleared, then the full filtered list returns and any result count badges update accordingly. - Performance: search/filter results render within 500ms for up to 500 invites.
Accessibility and Keyboard Navigation
- Keyboard: all interactive elements (chips, tooltips, filter, search, menus, actions) are reachable in a logical order and operable via Tab/Shift+Tab/Enter/Space/Arrow keys; Escape closes menus/tooltips/modals. - Screen readers: chips, actions, and tooltips have meaningful labels/roles; status changes are announced via aria-live="polite" with concise messages; tooltips are focusable and dismissible. - Touch: tap targets are >= 44x44px; long-press or info icon reveals tooltips on mobile; hover-only interactions have touch equivalents. - Contrast: text/icons meet WCAG 2.1 AA (>= 4.5:1); color is not the sole conveyor of state. - No motion traps: progress indicators and skeletons do not cause motion sickness and respect reduced motion preferences.
Empty, Loading, and Error States with Skeletons
- Loading: skeleton rows render within 100ms of screen load and remain until data resolves or an error occurs; skeletons approximate chip and row layout. - Empty: when no invites match (initial or after filter/search), show an empty state with explanatory copy and a primary action (e.g., Create invite) and a secondary action to clear filters. - Error: transport/server errors show an inline error with a Retry button; retry restores prior context (filters/search) and attempts reload; errors include a correlation ID for support. - Partial failures (e.g., search endpoint error) display scoped inline errors without clearing existing data or selections.
Localization and Timezone Handling
- Strings: all user-facing copy (chip labels, tooltips, menu items, empty/loading/error text, toasts) is sourced from i18n catalogs with English fallback; no hard-coded user-visible strings remain. - Time: relative timestamps are localized with correct pluralization; absolute timestamps use the user’s locale and timezone; switching timezone updates displayed times consistently. - RTL: layouts, icons, and menus render correctly in right-to-left languages; mirroring and text alignment are correct. - Runtime switch: changing language at runtime updates all strings in-place without full page reload.
Capacity, Holds, and Waitlist Coordination
"As an operations manager, I want invite actions to respect capacity and waitlist rules so that we maximize fill rate without overbooking."
Description

Orchestrates seat holds for invites so status-driven actions don't oversell classes. Extends or releases holds based on invite lifecycle and actions, and synchronizes with Smart Waitlist to auto-offer openings when invites expire or are swapped. Implements strict concurrency controls and idempotent operations to avoid double allocation, with eventual consistency safeguards and compensating actions on failure. Enforces studio policies for max holds per class, extension limits, and swap cutoffs relative to start time. Emits domain events for downstream systems (notifications, analytics).

Acceptance Criteria
Hold Placement on Invite Send (Capacity and Max Holds Policy)
Given a class with capacity C, booked seats B, existing active holds E, and studio policy maxHoldsPerClass = M and holdTTL = H And the host sends K buddy invites from the confirmation screen or roster (with a unique idempotencyKey per request) When the system processes the invite(s) Then it creates exactly min(K, M - E, C - B - E) new holds and never allocates more than (C - B) total remaining seats And each new hold is status=Held with expiration = min(now + H, classStart - cutoff) And public availability decreases by the number of new holds within 1 second And duplicate/retried requests with the same idempotencyKey do not create additional holds And under >=10 concurrent invite sends, final (bookings + active holds) <= C with no oversell
Hold Extension Within Policy Limits
Given an active hold with remainingExtensions r, studio policy maxExtensionsPerHold = L, extensionIncrement = X minutes, and extensionCutoff = T_cutoff before class start When the instructor taps Extend Hold Then if extendHoldEnabled = true AND r < L AND now < (classStart - T_cutoff), the hold expiration is extended by X minutes (capped at classStart - T_cutoff) And extensionCount increments by 1 and is persisted atomically And Invite Tracker reflects the new expiration within 1 second And a hold.extended event is emitted with an idempotencyKey; retries do not extend twice And if any precondition fails, the action is rejected with a clear error and no change to inventory
Auto-Release and Smart Waitlist Offer on Expire/Decline/Swap
Given an invite hold reaches expiration, is explicitly declined, or is swapped away When the trigger occurs Then the hold transitions to Released and the seat returns to inventory within 2 seconds And if Smart Waitlist is non-empty, the next eligible customer receives an auto-offer within 5 seconds And no more than one active offer exists per released seat and per customer at a time And confirmation screen and roster reflect Released/Offered statuses within 5 seconds And events hold.released and waitlist.offer.created are published at-least-once with idempotency keys; consumers can deduplicate
Idempotent Nudge/Resend Without Duplicate Holds
Given an invite with an active hold When the instructor taps Nudge or Resend multiple times (including network retries) within 10 minutes Then no new holds are created and capacity consumption does not increase And notification sends are at-most-once per idempotencyKey and rate-limited per policy And Invite Tracker shows a single consolidated activity entry for the action And the API returns 200 OK for safe retries without side effects
Swap Friend Transfer of Hold Without Oversell
Given an active invite hold assigned to Invitee A and policy swapCutoff = T_cutoff When the instructor selects Swap Friend before (classStart - T_cutoff) Then Invitee A’s link is revoked and Invitee B receives a new invite bound to the same hold (holdId unchanged) And no seat is released to public inventory or waitlist during the swap And all prior booking tokens for A are invalidated; only B can book against the hold And if attempted at or after (classStart - T_cutoff), the action is rejected with reason=swapCutoffReached and no state change And a hold.swapped event is emitted; consumers can process idempotently
Failure Recovery with Compensating Actions (Eventual Consistency)
Given a flow that releases a hold and creates a waitlist offer, and a downstream dependency fails mid-transaction When the failure is detected Then the system completes inventory-critical steps or rolls them back via compensating actions so that (bookings + active holds + active offers) <= capacity C at all times And retries are scheduled with exponential backoff; duplicate side effects are prevented via idempotency keys And background reconciliation corrects any divergence within 60 seconds And errors are logged with correlationIds and surfaced in the ops dashboard
Domain Events Emitted for Key Transitions
Given transitions: hold.created, hold.extended, hold.released, hold.swapped, waitlist.offer.created, booking.confirmed When a transition occurs Then a domain event is published within 3 seconds including: eventId, occurredAt, classId, seatId/holdId, inviteId, actor, previousStatus, newStatus, idempotencyKey, correlationId And delivery is at-least-once and per-invite ordering is preserved using a partition key And consumers can deduplicate using eventId; no out-of-order events are observed for the same invite
Invite Funnel Analytics and Export
"As a studio owner, I want to analyze invite conversion and action effectiveness so that I can improve messaging and fill more classes."
Description

Provides reporting for invite conversion by stage (Delivered → Viewed → Tapped → Booked), broken down by instructor, class, channel, and time range. Surfaces top drop-off points and the impact of actions (e.g., Nudge conversion rate). Includes dashboards, CSV export, and API endpoints for BI tools. Ensures data consistency with the status pipeline, uses backfilled historical events, and anonymizes PII where required. Supports cohorting by copy variant and campaign tags to enable experimentation.

Acceptance Criteria
Funnel Conversion Dashboard by Instructor/Class/Channel/Time Range
Given invite events exist across stages and dimensions, When a user applies filters for date range, instructor(s), class(es), channel(s), and timezone, Then the dashboard displays counts for Delivered, Viewed, Tapped, and Booked with stage-to-stage conversion percentages. Given a time range selection (Last 7, 30, 90 days, or custom), When applied, Then metrics recalculate to include only events within the range using the selected timezone. Given a granularity selection (daily/weekly/monthly), When viewed, Then trend charts render per bucket with non-overlapping aggregates and include zero-value buckets where applicable. Given filter changes, When applied, Then dashboard responses return within ≤2s for cached ranges and ≤5s for uncached ranges for datasets up to 100k invites. Given no matching data for selected filters, When viewing the dashboard, Then zero states are shown with an empty-state message and no errors.
Top Drop-off Points Identification
Given an invite funnel for the selected filters, When viewing the drop-off module, Then the stage with the highest attrition is highlighted and shows absolute drop and percentage drop. Given segmentation by instructor/class/channel, When switching segments, Then the highlighted drop-off stage and values update accurately per segment. Given small sample sizes (<100 invites in range), When calculating rankings, Then a "low sample size" indicator is shown and stages are not ranked. Given the drop-off formula, Then percentage drop is computed as (upstream stage − downstream stage) / upstream stage and matches CSV/API outputs for the same filters.
Action Impact Analytics (Nudge, Resend, Swap Friend, Extend Hold)
Given tracked action events with timestamps and invite IDs, When filtering by an action type, Then the dashboard shows post-action conversion-to-booked within 3, 7, and 14 day windows and the baseline for invites without that action. Given multiple actions on the same invite, When attributing conversions, Then last-touch attribution is applied by default within the selected window and users can switch to first-touch. Given small action sample sizes (<50 actions), When rendering impact metrics, Then the UI shows a warning and 95% confidence intervals. Given parity across surfaces, Then action impact rates match those from the API and CSV exports for identical filters within ±0.1%.
CSV Export of Funnel and Action Metrics
Given a user initiates CSV export with current filters and selected fields, When the export completes, Then the CSV contains a single header row, UTF-8 encoding, comma delimiter, and all selected columns with correct data types. Given up to 1,000,000 invites in range, When exporting aggregated metrics, Then the export completes within 2 minutes and a downloadable link and email notification are provided. Given PII handling rules, When exporting, Then emails/phones are anonymized (masked or salted hash per region) unless the user has "PII Export" permission; instructor and class names remain visible. Given a selected timezone and date range, When generating the file, Then timestamps in the CSV are ISO 8601 and in the selected timezone. Given dashboard totals for the same filters, When validating the CSV, Then row counts and metric totals match within ±0.1%.
Analytics API for BI Tools
Given an API key with analytics.read scope, When calling GET /analytics/invites/funnel with filters (date_from, date_to, timezone, granularity, instructor_id[], class_id[], channel[], copy_variant[], campaign_tag[]), Then the API responds 200 with JSON containing stage counts and conversion rates per time bucket. Given detailed event access, When calling GET /analytics/invites/events with pagination, Then the API returns deterministic cursor-based pagination ordered by event_time ascending and includes next_cursor when more data exists. Given rate limits of 60 requests per minute per key, When limits are exceeded, Then the API returns 429 with a Retry-After header. Given versioning, When the Accept-Version header is absent, Then the latest version is returned; when Accept-Version: v1 is provided, Then responses adhere to the v1 contract. Given authentication or validation failures, When the request has an invalid/expired key or invalid filters, Then the API returns 401/403/400 with machine-readable error codes and messages.
Data Consistency, Backfill, Cohorting, and PII Compliance
Given the status pipeline (Delivered → Viewed → Tapped → Booked), Then stage counts are monotonically non-increasing down-funnel for any filter set and time bucket. Given historical backfill jobs, When reprocessing completes, Then dashboard, CSV, and API metrics reflect updated counts within 15 minutes and are idempotent (no double counting of events). Given cohort filters for copy_variant and campaign_tag, When applied, Then all metrics restrict to invites matching those attributes and support side-by-side comparison in the dashboard. Given regional privacy requirements (e.g., GDPR), Then personally identifiable data is anonymized or excluded across all analytics surfaces unless explicit permission is granted, and access is logged with user, time, and scope. Given data freshness SLAs, Then new events appear in analytics within 5 minutes at P95 and within 15 minutes worst-case, and a freshness indicator shows the last processed timestamp.
Admin Settings and Policy Controls
"As an account admin, I want to set invite policies and templates so that Invite Tracker aligns with our operations and compliance needs."
Description

Adds a settings panel to configure Invite Tracker policies: enable/disable feature, default invite hold duration, max extensions, Nudge/Resend cooldowns and daily caps, allowed channels, swap permissions, and copy templates per locale. Includes role-based access control, audit logs of changes, and feature flags for staged rollout. Validates configurations to prevent unsafe combinations and exposes safe defaults. Changes propagate in real time to the status pipeline and actions services.

Acceptance Criteria
Feature Toggle and Real-Time Propagation
Given Invite Tracker is enabled for a studio When an instructor opens a class confirmation screen or roster Then Invite Tracker status indicators and actions (Nudge, Resend, Swap Friend, Extend Hold) are visible and functional Given the same studio When an admin disables Invite Tracker in Settings and saves Then within 5 seconds the UI hides Invite Tracker elements and the actions API responds 403 INVITE_TRACKER_DISABLED for related endpoints And the status pipeline and actions services read the disabled state within 5 seconds for that studio
Hold Duration and Max Extensions Enforcement
Rule: Default hold duration must be between 5 and 1440 minutes; Max extensions must be between 0 and 10 Given default hold duration = 60 minutes and max extensions = 2 When a new invite hold is created Then the hold expiry is created_at + 60 minutes Given an invite with remaining_extensions > 0 and not expired When the instructor taps Extend Hold Then the expiry increases by 60 minutes and remaining_extensions decreases by 1 Given remaining_extensions = 0 or the invite is expired When the instructor taps Extend Hold Then the control is disabled and the API responds 409 EXTENSION_LIMIT_REACHED or 410 HOLD_EXPIRED Given existing pending invites When an admin updates hold duration to 45 minutes and max extensions to 1 and saves Then within 5 seconds new invites and subsequent extensions use the updated values And existing hold expiries already set are not shortened Given an admin enters values outside valid ranges When saving Then the form blocks save with inline validation messages and no configuration is persisted
Nudge/Resend Cooldowns and Daily Caps Enforcement
Rule: Cooldown must be 1–1440 minutes; Daily cap must be 0–10 per recipient per channel; Day boundary uses studio timezone Given Nudge cooldown = 30 minutes and daily cap = 3 When an instructor sends a Nudge via SMS to recipient R Then further Nudge attempts via SMS to R within 30 minutes are blocked with 429 COOLDOWN_ACTIVE and a countdown indicator shows remaining time Given 3 Nudges via SMS have been sent to R today When the instructor attempts another Nudge via SMS to R the same day Then the request is blocked with 429 DAILY_CAP_REACHED and the UI shows the reset time (local midnight) Given Resend cooldown = 10 minutes and daily cap = 5 When sending Resend actions Then the same cooldown and cap rules apply and are counted separately from Nudges Given an admin changes cooldowns or caps and saves When the instructor immediately attempts a Nudge/Resend Then enforcement reflects the new values within 5 seconds in both UI and API Given an admin sets cooldown = 0 or cap > 10 When saving Then the form blocks save with validation explaining allowed ranges
Allowed Channels Policy Enforcement (UI and API)
Given allowed channels are set to ["SMS","Email"] When an instructor opens Invite Tracker actions Then only SMS and Email options are shown; other channels are hidden Given a client calls the actions API with channel = "WhatsApp" (disallowed) When the request is processed Then the API responds 403 CHANNEL_NOT_ALLOWED with a descriptive error payload Given a channel is disabled by an admin and saved When an instructor attempts to use that channel within 5 seconds Then the option is not available in the UI and the API rejects direct calls with 403 CHANNEL_NOT_ALLOWED
Locale Templates Selection and Fallback
Given templates exist for en-US and es-ES and default locale is en-US When sending a Nudge via Email to a recipient with locale es-ES Then the es-ES template is selected and all placeholders resolve from the invite context without errors Given a recipient locale fr-FR and no fr-FR template exists When sending a Resend via SMS Then the en-US default template is used Given neither a matching locale template nor a default template exists for the channel When attempting to send Then the operation is blocked with 422 TEMPLATE_MISSING and the UI prompts to add a template before retrying Given an admin previews a template When placeholders are incomplete or invalid Then the preview highlights missing variables and saving the template is blocked until resolved
Role-Based Access Control and Audit Logging
Rule: Owner/Admin can view and edit; Instructor can view only; others receive 403 for write operations Given a user with Owner or Admin role When visiting Settings > Invite Tracker Policies Then all fields are visible and editable and Save is enabled Given a user with Instructor role When visiting the same page Then settings are visible but Save is disabled and write APIs return 403 FORBIDDEN Given an unauthorized user When calling read or write endpoints Then read returns 401/403 as appropriate and write returns 403 FORBIDDEN Given any settings change is saved When the operation completes Then an audit log entry records actor id, role, IP, timestamp (ISO8601), changed fields with old/new values, and feature flag context, and is retrievable via the audit log UI/API
Feature Flags for Staged Rollout
Given a feature flag controls Invite Tracker Policies by studio cohort When the flag is set to 50% rollout Then only users in flagged studios see the settings panel and services enforce the policies for those studios; non-flagged studios see no panel and policies are not enforced Given the flag is toggled on or off for a specific studio When the change is saved Then within 30 seconds the UI, status pipeline, and actions services reflect the new state for that studio Given a global kill switch is activated When instructors access Invite Tracker features Then UI elements are hidden and the actions API responds 403 FEATURE_FLAG_DISABLED across all studios

Pair Waitlist

Lets attendees choose a linked, two-seat waitlist when classes are full or the hold lapses. Only offers openings when two seats free up, or smartly switches to single-seat offers if time is tight (with clear opt-in). Keeps friends together, fills late cancellations instantly, and preserves goodwill.

Requirements

Paired Waitlist Opt-In UI
"As an attendee planning with a friend, I want to join the waitlist as a pair so that we are either both confirmed or neither of us loses out."
Description

Provide a mobile-first flow on the booking page that lets an attendee choose “Join as Pair” and link two seats to a single waitlist entry. Capture companion details (name, email, phone), show clear explanations of pair-only offers vs. optional single-seat fallback, and collect explicit consent to fallback. Validate inputs, prevent duplicate pairings, and surface real-time capacity/waitlist position. Ensure accessibility (WCAG 2.1 AA), localization, and graceful error states. Integrate with existing class detail and waitlist components without increasing friction for single-seat users.

Acceptance Criteria
Mobile Pair Join Flow Visibility & CTA
Given a class is full or the seat hold has lapsed, When the attendee opens the booking page on a mobile viewport (≤768px), Then a "Join as Pair" option is displayed above the individual waitlist option, and "Join as Individual" remains the default selection. Given the attendee toggles "Join as Pair", When the UI updates, Then the companion detail fields and fallback consent controls are revealed and the primary CTA label changes to "Join Waitlist as Pair". Given the attendee joins as an individual, When they proceed without toggling, Then they can join the waitlist in ≤2 taps from arrival and with ≤1 required form field, preserving current single-seat friction levels. Given any viewport size, When the layout reflows, Then all controls remain visible without horizontal scrolling on widths ≥320px.
Companion Details Capture, Validation, and Duplicate Prevention
Given "Join as Pair" is selected, When the form renders, Then companion full name, email, and phone fields are required and validated client-side and server-side. Given the companion email is entered, When the format is invalid, Then inline error text describes the issue and submission is blocked until corrected. Given a phone number is entered, When it is not in E.164 or recognized local format, Then the field shows an inline error and offers the expected format. Given the companion email or phone matches an existing active booking or waitlist entry (individual or pair) for this class occurrence, When the attendee submits, Then submission is rejected with a non-blocking message explaining the duplicate and suggesting the companion use their own account. Given all validations pass, When the attendee submits, Then the paired waitlist entry is created with both attendee and companion details stored and masked appropriately in the UI.
Explicit Fallback Consent for Single-Seat Offers
Given "Join as Pair" is selected, When the fallback consent control is shown, Then two mutually exclusive choices are presented: "Only offer when two seats open" and "Also offer a single seat if time is tight", with neither preselected. Given neither choice is selected, When the attendee attempts to submit, Then submission is blocked and an inline error requests a selection. Given a choice is selected, When the attendee submits, Then the selected fallback preference and a consent timestamp are persisted with the waitlist entry and reflected in the confirmation UI. Given the attendee selects "Also offer a single seat", When they review the summary, Then the UI clearly indicates single-seat offers may be sent and provides a link to change this preference later.
Real-Time Capacity and Pair Waitlist Position
Given the booking page is open, When the class capacity or pair waitlist position changes, Then the displayed position and capacity indicators update within 3 seconds via web socket, or within 15 seconds via polling if the socket is unavailable. Given the app is offline, When updates cannot be received, Then the UI shows the last known position with a "no connection" notice and disables submission until connectivity returns. Given a pair joins the queue, When the position is calculated, Then position numbering is 1-based and reflects pair-eligible ordering only. Given capacity reaches at least two open seats, When the attendee is next in the pair queue, Then the UI shows a "high chance" indicator with ARIA live announcement.
Accessibility Compliance (WCAG 2.1 AA) for Pair Waitlist UI
Given a keyboard-only user, When navigating the form, Then all interactive elements are reachable in logical order, have visible focus states, and are operable without timeouts. Given a screen reader user, When the form loads and dynamic updates occur, Then all fields have programmatic labels, error messages are associated and announced, and position updates are announced via aria-live polite without interrupting input. Given any user, When viewing the UI, Then text and icon contrast ratios meet or exceed 4.5:1 for normal text and 3:1 for large text, and touch targets are at least 44x44 px. Given focus-triggering actions (open/close explanations, errors), When they occur, Then focus is moved to the new context and returned on dismiss without trapping.
Localization, Formatting, and RTL Support
Given the site locale is not English, When the pair UI loads, Then all static and dynamic copy, validation messages, and CTA labels are localized with no mixed-language strings. Given the locale uses RTL script, When the page renders, Then layout, icons with directionality, and field alignment mirror correctly and remain usable on 320px width devices without horizontal scroll. Given locale-specific formats, When the user enters phone numbers, dates, or times, Then input masks and helper text adapt to the locale and values are normalized for storage. Given the user switches language, When a different locale is selected, Then all entered form data persists and revalidates against the new locale rules without clearing.
Integration with Existing Components and Single-Seat Friction Guardrails
Given the existing class detail and waitlist components, When the pair UI is enabled, Then the integration does not break existing analytics, tracking, or navigation, and reuses existing styles and APIs. Given a single-seat user, When they join the waitlist, Then the number of required interactions is ≤2 taps and page load time does not increase by more than 100 ms p95 compared to baseline. Given bundle delivery, When the pair UI is loaded, Then additional gzipped JS/CSS size is ≤25 KB and does not block first input delay beyond 50 ms p95. Given site errors, When backend APIs fail or time out, Then the UI shows descriptive, localized errors with retry and preserves all entered data on retry.
Two-Seat Matching Engine
"As a studio owner, I want the system to auto-offer only when two seats free up for a pair so that friends stay together without manual intervention."
Description

Implement backend logic that monitors inventory changes and offers openings to paired waitlist entries only when two seats become available for the same class instance. Respect queue order, prevent race conditions with concurrent holds or late cancellations, and avoid partial offers to pairs that have not opted into fallback. Coordinate with existing hold/offer services and class capacity rules. Provide idempotent operations, retries, and telemetry to trace match outcomes and drop-offs.

Acceptance Criteria
Trigger Pair Offer Only When Two Seats Available
Given a class instance is at capacity and at least one paired waitlist entry exists And the paired entry has not opted into single-seat fallback When exactly two seats become available for the same class instance Then the engine issues one paired offer reserving two seats via the hold service using the configured hold duration And no offers are sent to single-seat waitlist entries ahead of the paired offer while two seats are available And an OfferCreated event is published including classInstanceId, waitlistEntryId(s), seatsOffered=2, offerType=pair
Queue Order and Eligibility for Paired vs Single Waitlist Entries
Given a unified waitlist queue containing paired and single entries with enqueue timestamps When seats become available Then eligibility is determined such that a pair is eligible only when ≥2 seats are available unless single-seat fallback is enabled And the earliest eligible entry by enqueue timestamp receives the offer (FIFO) And earlier ineligible pairs are not skipped in favor of later pairs; they retain position until eligibility is met And accepting an offer updates capacity and queue atomically
Concurrency Safety and Atomic Seat Holds for Pair Offers
Given concurrent cancellations, expiries, and offer generations for the same class instance When creating a paired offer Then seat holds are applied atomically so total held + confirmed never exceeds class capacity And duplicate or overlapping holds for the same seat(s) are prevented And if a race condition is detected, the operation aborts cleanly and retries per policy without partial holds or duplicate offers
Single-Seat Fallback for Opted-In Pairs Near Start Time
Given a paired waitlist entry has explicitly opted into single-seat fallback with a configured fallback threshold before class start And only one seat is available When time to class start is within the configured fallback threshold Then the engine issues a single-seat fallback offer associated to the paired entry, labeled offerType=singleSeatFallback And only one seat is held; the paired entry retains queue position for a second seat if one later becomes available And if the pair is not opted in, no single-seat fallback offer is issued under the same conditions
Idempotent Operations and Safe Retries for Offer Creation
Given offer creation requests carry an idempotency key And a transient failure occurs after creating the hold but before responding When the request is retried with the same idempotency key within the retry window Then the original offer is returned and no additional holds or duplicate offers are created And emitted events/metrics are de-duplicated by the idempotency key (at-most-once externally visible effects)
Telemetry and Traceability of Match Outcomes
Given any match attempt for a paired waitlist entry When an offer is created, accepted, declined, expired, or times out Then telemetry events include correlationId, classInstanceId, waitlistEntryId(s), offerType, seatsOffered, queuePositionAtOffer, outcome, and timestamps And events are queryable in the analytics store within the configured ingestion SLA And distributed tracing spans link matching, hold, and offer services for end-to-end traceability
Capacity Rules and Hold Coordination Compliance
Given class capacity rules, existing holds, and late cancellations When capacity changes due to cancellation or hold expiry Then the matching engine re-evaluates eligibility and issues offers without exceeding capacity or violating provider rules And holds are created and released through the existing hold service; offers are dispatched via the offer service And no offers are issued when capacity is restricted by rules such as instructor blocks or blackout constraints
Smart Fallback to Single-Seat Offers (Consent-Based)
"As a paired attendee, I want the option to receive a single-seat offer close to start time if only one seat opens so that I can still attend when plans change."
Description

Enable time-based logic that, within a configurable pre-class window (e.g., 6 hours), can send single-seat offers to each member of a pair who has explicitly opted in. Clearly communicate trade-offs, track consent per user, and ensure fairness by preserving the pair’s queue position until fallback is triggered. Manage separate timers for individual offers, avoid over-offering beyond remaining capacity, and log decisions for support/audit. Comply with regional consent requirements for messaging and offer handling.

Acceptance Criteria
Configurable Fallback Window Enforcement
Given a class with a configured fallback window of N hours before start When the current time is earlier than start time minus N hours Then the system must not generate or schedule single-seat fallback offers for any pair Given the current time is between start time minus N hours and the class start time When evaluating waitlisted pairs Then only pair members with recorded single-seat fallback consent = true are eligible for single-seat offers Given the class start time is reached When there are any pending single-seat fallback offers Then all such offers are auto-expired and no new offers are issued
Per-User Explicit Consent Capture and Storage
Given a user opens the single-seat fallback opt-in UI When the UI is displayed Then it shows purpose, trade-offs, channels to be used, and region-specific compliance copy with the opt-in control defaulted to off Given the user opts in When the action is submitted Then the system stores a consent record with: user_id, purpose = "single-seat fallback offers", channels, region, legal_basis, language, timestamp, source, and policy_copy_version Given the user opts out When the action is submitted Then eligibility is immediately revoked and any pending fallback offers for that user are canceled with a notification Given a support/admin user requests a consent record by user_id When the system retrieves the record Then the record is returned within 2 seconds (p95) for the past 12 months
Queue Fairness and Position Preservation
Given a pair on the waitlist outside the fallback window When seats become available Then they are only considered for two-seat offers in their original queue order regardless of single-seat opt-in status Given the fallback window has begun When evaluating offers Then the pair retains their two-seat queue position for any two-seat openings; single-seat evaluation does not advance them ahead of earlier pairs for two-seat openings Given any member of the pair accepts a single-seat offer When the acceptance is confirmed Then the remaining member stays on the waitlist for a single seat with the original timestamp carried over; the pair’s two-seat position is forfeited and this transition is logged
Capacity-Safe Offer Generation
Given remaining capacity C for a class When issuing single-seat fallback offers Then the number of concurrently active offers across all users must be less than or equal to C at all times Given exactly one seat is available and both members of the same pair are eligible When selecting a recipient Then the system issues the offer to only one member using a deterministic and fair selector; the selector decision and rationale are logged Given capacity changes due to acceptance, cancellation, or new releases When recalculating offers Then new offers may be issued up to the new capacity and any excess pending offers are canceled in reverse priority order with notifications
Independent Offer Timers and Conflict Handling
Given a single-seat fallback offer is sent When the offer is created Then it is assigned an independent acceptance timer equal to the class-configured duration (default 15 minutes) and the timer value is included in the message content Given a user accepts within the timer and a seat remains When the system confirms the seat Then all other pending or scheduled offers are re-evaluated against updated capacity; conflicting offers are canceled and recipients are informed that the seat is no longer available Given a user attempts to accept after the timer expires or when capacity is zero When the system processes the acceptance Then the acceptance is rejected with a clear message and the user remains (or returns) to the waitlist with their priority unchanged
Decision, Messaging, and Compliance Audit Logging
Given a fallback decision is evaluated or a message is sent When the event occurs Then an audit log entry is stored with: event_type, class_id, user_id, pair_id, timestamp, capacity_before/after, selector_reason, consent_check_result, region, channel, template_id, outcome, and correlation_id Given a support/admin user filters logs by class_id or user_id for the last 30 days When requesting results Then the system returns matching entries within 3 seconds (p95) and supports CSV and JSON export Given the user’s region prohibits messaging on a channel without explicit consent When consent for that channel is missing or revoked Then the system does not send on that channel, records a blocked attempt with reason, and uses only permitted channels if available
Coordinated Offer and Hold Windows
"As an instructor, I want synchronized hold windows for pair offers so that seats aren’t locked unnecessarily and late cancellations refill quickly."
Description

For pair offers, reserve both seats simultaneously for a configurable hold duration (e.g., 10–20 minutes) and display synchronized countdowns. Support a shared confirmation link where either member (if designated) can confirm for both seats, or dual confirmations if required by policy. On expiration, automatically release seats and advance to the next eligible pair or fallback logic. Prevent overlapping offers, handle declined/canceled responses, and reconcile holds with existing global hold mechanisms to avoid inventory deadlocks.

Acceptance Criteria
Simultaneous Two-Seat Hold with Synchronized Countdown
Given a class has at least 2 available seats and an eligible pair is next in queue When the system generates a pair offer Then both seats are placed on hold at the same timestamp for the configured duration (10–20 minutes) and no other bookings can consume them during the hold Given the hold is active When either attendee views the offer page Then a synchronized countdown displays identical remaining time for both seats (±1 second) and updates at least once per second Given an admin updates the pair hold duration within 10–20 minutes When new offers are generated after the change Then new offers use the updated duration and in-flight holds retain their original duration
Shared Confirmation Link Confirming Both Seats
Given policy is set to "shared confirmation allowed" When the designated member opens the shared confirmation link and confirms Then both seats are confirmed atomically, payment is captured for both, and the hold ends Given both seats are confirmed via the shared link When confirmation completes Then confirmations are sent via email/SMS to both attendees within 60 seconds and inventory reflects 0 seats remaining for those two seats Given both members attempt to confirm near-simultaneously via the shared link When the system processes the requests Then only one booking is created and no duplicate charges occur (idempotent processing)
Dual Confirmations Required by Policy
Given policy is set to "dual confirmations required" When the first member confirms Then both seats remain on hold until expiry and the UI shows "1 of 2 confirmed" Given policy is set to "dual confirmations required" When the second member confirms before the hold expires Then both seats are confirmed and payment is captured for both members Given policy is set to "dual confirmations required" When the second member has not confirmed by hold expiry Then both seats are released, no payment is captured, and the first member receives an expiration notification within 60 seconds
Expiration Release and Next Offer Advancement
Given a pair offer hold is active When the hold timer reaches zero without satisfying confirmation policy Then both seats are released to inventory within 5 seconds and the system advances to the next eligible pair in FIFO order Given a next eligible pair exists When the prior offer expires Then a new pair offer is sent within 30 seconds and the previous offer link is deactivated Given no eligible pair exists When an offer expires Then the seats become immediately available for general booking
Prevent Overlapping Offers and Reconcile with Global Holds
Given a pair offer hold is active on two specific seats When any other offer or hold (single or pair) attempts to reserve either seat Then the system rejects the overlap and records an audit log entry with timestamp and conflicting offer IDs Given a global checkout hold exists on a seat When a pair offer is initiated that would include that seat Then the seat is excluded from the pair offer, or the pair offer is not created, ensuring no seat is in more than one active hold Given high concurrency (>=100 concurrent offers across classes) When offers are created and updated Then no seat appears in more than one active hold at any time as verified by audit logs and a periodic consistency check job
Handling Declined or Canceled Responses
Given a pair offer is active When either member explicitly declines the offer Then both seats are immediately released and the system advances to the next eligible pair Given both members have confirmed When both cancel within the allowed cancellation window Then both seats are released per policy and the next eligible pair is offered if seats become available before class start Given a pair declines an offer When decline processing completes Then that pair is not re-offered for the same class time unless they re-opt in to the waitlist
Smart Fallback to Single-Seat Offers with Opt-In
Given time-to-class-start is within the admin-configured threshold (e.g., 60 minutes) and only one seat becomes available When both members have opted in to single-seat fallback Then the system sends separate single-seat offers to each member with clear labeling and separate payment flows Given single-seat fallback is enabled When one member accepts and the other does not within their respective holds Then a single-seat booking is created for the accepting member and the pair remains eligible for future pair offers for any subsequent seat Given single-seat fallback is disabled or no opt-in is recorded When only one seat is available Then no single-seat offer is sent and the system either waits for two seats or advances to the next pair per policy
Linked Checkout and Payment Capture
"As a pair, I want to pay together or separately while securing both seats at once so that we avoid mismatches and failed bookings."
Description

Provide checkout that can capture payment for both seats atomically in one transaction or via split payment invites, ensuring no seat is confirmed unless both payments succeed or an authorized payer covers both. Support passes/memberships, promo codes per attendee, taxes, and fees. Integrate with existing payment provider using payment intents, handle partial failures and rollbacks cleanly, and issue automatic refunds or release holds when needed. Optimize for mobile UX, PCI compliance, and clear receipts for each attendee.

Acceptance Criteria
Atomic Two-Seat Single-Payer Checkout
Given a pair waitlist offer for 2 seats with a 10-minute hold When the payer completes a single checkout using a valid payment method and any required SCA within the hold Then both seats are confirmed in one operation, the payment intent is captured once, the available seats decrease by 2, and the confirmation page shows both attendee names. Given the same offer When payment fails, SCA is abandoned, or the hold expires before capture Then neither seat is confirmed, any authorization is voided or expires, both seats return to inventory, and the user sees a clear failure message. Given a possible double-submit or browser refresh When the payer retries within 60 seconds Then an idempotency key prevents duplicate charges and only one booking is created.
Split Payment with Invites and Auto-Cover Option
Given a pair waitlist offer with split payment selected When attendee A pays and attendee B receives SMS/email invite Then both seats remain on hold for 10 minutes, the invite link is unique and single-use, and B’s checkout reflects A’s payment status. Given A has paid but B has not completed within the hold When A opts to “Cover both seats” before expiry Then a second capture for the remaining balance succeeds and both seats confirm; otherwise A’s payment is automatically refunded within 15 minutes and both seats are released. Given B’s payment fails after A paid When the system notifies A with a one-click “Cover both seats” option Then if A declines or takes no action within 2 minutes, A is refunded automatically and both seats are released. Given the invite was sent When B opens the link after expiry Then the link shows “Offer expired” and no payment is accepted.
Per-Attendee Passes, Memberships, Promo Codes, Taxes, and Fees
Given two attendees When each applies their own pass, membership, and/or promo code Then entitlements are validated in real time, discounts apply per attendee, and the order summary shows per-attendee lines with base price, discount, fees, and taxes. Given one attendee uses a pass covering full price and the other pays When the transaction completes via single payer or split payment Then only the paying attendee is charged cash, the pass is decremented once, and both seats are confirmed atomically. Given invalid or exhausted promo codes When applied Then the system rejects them with a clear message and prevents checkout until resolved. Given a taxable class When totals are computed Then the grand total equals the sum of per-attendee lines with proper rounding to currency minor units and matches the captured amount.
Partial Failure Compensation, Rollbacks, and Refunds
Given a split payment where A’s payment succeeded and B’s failed or timed out When no cover action is taken Then A is automatically refunded to the original method within 15 minutes, both seats are released, and an audit log entry is recorded. Given a single-payer two-seat checkout where capture succeeds for one seat but fails for the second due to provider error When the error is received Then the entire transaction is rolled back (no seats confirmed), the payment intent is canceled/voided, and the user is informed to retry. Given any automatic refund is issued When the provider webhook confirms refund status Then the booking shows “Canceled,” inventory is restored if not already, and the user receives a refund notification.
Payment Intents, Webhooks, and Idempotency Gating Seat State
Given checkout initialization When creating payment intents Then the system sets amount, currency, capture_method=automatic, and attaches metadata for booking_id and attendee_ids, storing the payment_intent_id. Given SCA is required When the user authenticates Then the flow handles requires_action and only transitions to seat confirmation after payment_intent status is succeeded. Given network retry or duplicate request When the same idempotency key is used Then only one payment intent is created and used for confirmation. Given provider webhooks (payment_intent.succeeded, payment_intent.canceled, payment_intent.payment_failed) When events are received Then seat confirmation occurs only after succeeded, and any failed/canceled event triggers release/rollback within 2 minutes.
Per-Attendee Receipts and Notifications
Given a successful paired booking When confirmation occurs Then separate receipts are sent to each attendee via email (and SMS link) within 2 minutes, each showing attendee name, class, date/time, price, discounts, taxes, fees, total, last4 or “Pass used,” and transaction ID. Given split payment with attendee A covering both When confirmation occurs Then A’s receipt shows both line items and total charge; B’s receipt shows $0 or “Covered by A,” with pass or discount details as applicable. Given a refund due to failure or expiry When processed Then refund notifications are sent with amount, method, and expected settlement time.
Mobile-First PCI/SCA-Compliant Checkout UX
Given a mobile device (≥360x640 viewport) When loading checkout over simulated 4G Then Time to Interactive is under 2.5 seconds and all touch targets are at least 44x44px. Given the card entry form When entering card details Then inputs are tokenized via provider elements, no raw PAN hits ClassNest servers (SAQ A scope), and 3DS/SCA flows complete without full-page reloads. Given a hold countdown is active When displayed Then it updates every second in mm:ss, and the Pay/Confirm button disables on expiry with a clear message and option to rejoin the waitlist. Given accessibility requirements When evaluated Then the checkout meets WCAG 2.1 AA basics for contrast, focus order, labels, and screen reader announcements for errors and countdown.
Multi-Channel Notifications and Reminders
"As a busy attendee, I want clear, timely alerts about pair offers and expirations so that I can act quickly and not miss my chance."
Description

Send timely SMS/email notifications for waitlist joins, pair offers, countdown reminders, expirations, declines, and consent-based single-seat fallback offers. Use localized, brandable templates with deep links to offer and checkout screens. Enforce opt-in/opt-out preferences, quiet hours, and rate limits; include deliverability tracking and retries. Ensure messages reflect pair vs. single-seat context unambiguously and update in-app status in real time via webhooks or push.

Acceptance Criteria
Pair Waitlist Join Confirmation
Given an attendee pair joins a full class waitlist and at least one member has opted in to SMS and/or email When the join action completes Then a confirmation is sent via each opted-in channel within 60 seconds, the content explicitly states it is a pair waitlist requiring two seats, includes class details, and contains a deep link to the waitlist status And no message is sent on any opted-out channel Given a confirmation is generated during configured quiet hours When dispatch is evaluated Then the message is queued and not sent until quiet hours end, and then dispatched within the configured post-quiet-hours dispatch SLA while respecting per-user per-channel rate limits
Pair Offer Notification (Two Seats Available)
Given two seats become available for a class and an eligible pair is first in line When the offer is created Then notifications are sent to all pair members who are opted in on each channel within 60 seconds, the content clearly states the offer is for two seats, includes class details and price summary, shows the exact expiration timestamp in the recipient's timezone, and contains a deep link to two-seat checkout And no notification is sent on any opted-out channel And the offer appears in-app for the pair with a matching expiration timer
Countdown Reminders, Declines, and Offer Expiry
Given an offer is active and countdown reminders are enabled with a configured schedule When a reminder threshold is reached Then a reminder is sent via each opted-in channel, includes time remaining and a deep link to checkout, and respects quiet hours and configured rate limits Given a recipient declines an active offer from a deep link or in-app action When the decline is confirmed Then the offer is immediately canceled for that pair, any pending reminders are canceled, seats are released to the next eligible candidate, and a decline confirmation is sent via opted-in channels Given an offer reaches its expiration timestamp without acceptance When expiration occurs Then an expiration notification is sent via opted-in channels, the offer status is set to expired, and seats are released to the next eligible candidate
Consent-Based Single-Seat Fallback Offer
Given only one seat is available within the configured fallback window and the top pair has pre-opted into single-seat fallback When fallback is triggered Then a single-seat offer is sent via opted-in channels within 60 seconds, the content clearly states it is for one seat only (not two), and includes deep links to single-seat checkout and to manage fallback preferences Given only one seat is available and the top pair has not pre-opted into single-seat fallback When fallback is triggered Then a consent request is sent via opted-in channels, no single-seat offer is sent until explicit consent is recorded from the pair, and the consent decision is stored with timestamp and actor Given a recipient accepts a single-seat fallback offer and completes checkout When payment succeeds Then the pair’s waitlist state updates to reflect one seat claimed and the remaining seat status per configured rules, and all subsequent messages for this class instance reflect the single-seat context unambiguously
Localization, Branding, and Deep Links
Given a notification is generated for a recipient with a locale and timezone When the template is rendered Then localized copy is selected with fallback to default, all placeholders are resolved with correct values, and all dates/times display in the recipient’s timezone Given brand theming is configured for the instructor or studio When an email is rendered Then logo, colors, sender name, and reply-to match the configured brand theme; for SMS, the sender ID matches configuration where supported Given a recipient taps a deep link in a message When the link opens Then it routes to the correct screen (offer details or checkout) for the specific class and seat context (pair vs single), pre-populates the correct number of seats, and tracking parameters do not break routing
Preferences, Quiet Hours, and Rate Limits Enforcement
Given a recipient has opted out of a channel When any notification is generated for that recipient Then that channel is not used and the event is logged as suppressed due to opt-out Given quiet hours are configured for a recipient or tenant When a notification is generated during quiet hours Then the notification is queued according to policy and not dispatched until quiet hours end, with dispatch time adhering to the configured post-quiet-hours SLA Given per-user and per-tenant rate limits are configured by channel When multiple notifications would exceed limits within a window Then the system enforces the limits, queueing or suppressing messages per policy, and records the reason as rate-limited Given a recipient has no opted-in channels When an event occurs that would send a message Then no outbound message is sent and the in-app feed records the event
Deliverability Tracking, Retries, and Real-Time Status Updates
Given a message is dispatched to a provider When provider delivery callbacks are received Then the system records delivery states (queued, sent, delivered, failed, bounced) with provider message ID and timestamps, and exposes them in logs/analytics Given a transient delivery failure occurs (e.g., timeout or 5xx) When retry policy is enabled Then the system retries up to the configured maximum attempts with exponential backoff, without breaching rate limits, and marks the message failed after the final attempt Given a permanent failure or hard bounce is detected When processing delivery status Then the system does not retry, records the failure, and flags the channel as undeliverable for that recipient without changing their opt-in preference Given any message state change or user action (accept, decline) occurs When the event is processed Then the in-app waitlist/offer status updates within 10 seconds via webhook or push, and the UI reflects the correct pair vs single-seat context
Admin Controls, Analytics, and Guardrails
"As a studio owner, I want configurable controls and insights for pair waitlists so that I can tune policies and measure their impact on bookings and goodwill."
Description

Add studio-level and class-level settings to enable Pair Waitlist, configure fallback windows, define who can confirm (one vs. both), set pair capacity limits, and reorder or pause the pair queue. Provide analytics on fill rate, time-to-fill, conversions from pair vs. fallback, decline reasons, and no-show impact. Include guardrails against abuse (e.g., repeated declines, duplicate pairings), audit logs for offer decisions, GDPR/CCPA-compliant data retention, and manual override tools to convert pairs to single or force an offer when appropriate.

Acceptance Criteria
Studio/Class Settings: Enable Pair Waitlist and Fallback Window
Given I am a studio admin with edit permissions, When I open Studio Settings > Waitlist, Then I can toggle Pair Waitlist on/off and save successfully. Given a class has its own settings, When I open Class > Settings > Waitlist, Then I can override studio defaults, enable/disable Pair Waitlist, and set a fallback window between 0 and 120 minutes with inline validation and error messaging for out-of-range values. Given fallback window > 0 and the pair has opted in to single-seat fallback, When fewer than 2 seats are available within the window, Then single-seat offers are sent per policy; When window = 0 or pair did not opt in, Then no single-seat offers are sent. Given I save changes, Then the settings apply only to offers created after the save timestamp; existing active offers retain their original policy and a notice indicates this behavior. Given settings are saved, When I refresh the page or fetch via public API, Then values persist and match what was saved. Given settings are updated, Then an audit log entry is recorded with admin, timestamp, and change details.
Confirmation Policy: One vs Both Must Confirm
Given class-level policy is set to "Both must confirm", When an offer is sent to a pair, Then the booking is finalized only after both members confirm and pay within the hold window; If either declines or times out, Then both seats return to inventory and the pair remains in queue per rules. Given class-level policy is set to "Either may confirm", When one member confirms and pays, Then both seats are held and the partner is prompted to confirm within the remaining hold time; If the partner does not confirm by expiry, Then the unconfirmed seat returns to inventory while the confirmed seat remains booked. Given an offer is sent, Then the offer message clearly states the confirmation policy and time remaining. Given the policy is changed, When new offers are generated, Then they use the new policy while existing offers keep the original policy. Given offers are accepted or declined, Then analytics attribute conversions and declines to the active confirmation policy for reporting.
Pair Capacity, Queue Reorder, and Pause Controls
Given I am a class admin, When I set pair waitlist capacity to N (0–500) and save, Then the system enforces max active pair entries = N; When N = 0, Then pair waitlist entry is disabled with a clear message to users. Given there are pairs in the queue, When I drag-and-drop a pair to a new position and save, Then the new order is immediately used for next-offer selection and visible to admins; original join timestamps are preserved in audit logs. Given the queue is paused, When users attempt to join or the system would send new offers, Then joining is blocked and no new offers are sent; existing active holds continue; an admin banner indicates paused state. Given I unpause the queue, Then normal operations resume without data loss. Given two admins edit the queue concurrently, Then optimistic concurrency prevents silent overwrites and surfaces a conflict message requiring refresh.
Analytics Dashboards: Fill Rate, Time-to-Fill, Conversions, Decline Reasons, No-Show Impact
Given I open Analytics > Waitlist, When I select a date range, class, and instructor, Then I see: (a) fill rate for openings eligible for Pair Waitlist, (b) median and p90 time-to-fill from opening to confirmation, (c) conversion rates split by pair vs fallback, (d) decline reasons distribution, and (e) no-show rates for pair-originated bookings vs non-waitlist baseline. Given I click Export CSV, Then a CSV downloads containing displayed metrics and underlying event rows sufficient to recompute them. Given I compare dashboard metrics to underlying event logs for the same filters, Then values match within 1% or 1 booking (whichever is greater). Given new booking/waitlist events occur, Then dashboards update within 60 minutes and show a "Last updated" timestamp. Given I hover or tap metric info icons, Then definitions and inclusion/exclusion rules are displayed consistently across UI and CSV.
Guardrails: Abuse Prevention (Repeated Declines, Duplicate Pairings)
Given a user or pair declines 3 offers for the same class within 30 days, Then they enter a 7-day cooldown for that class; they receive a notification; admins can remove the cooldown from an admin panel. Given a user attempts to create multiple pair entries for the same class occurrence, Then the duplicate is blocked with a clear message; if the user is already paired for an overlapping class start within 60 minutes, Then the new entry is blocked with guidance. Given a user declines >5 offers across ≥5 classes within 24 hours, Then the account is flagged for review in an Admin > Guardrails queue. Given any guardrail triggers, Then an audit log entry is created with reason code, scope (class/global), and expiry; affected UI surfaces show explanatory banners to the user. Given an admin whitelists a user, Then guardrails are bypassed for that user for 30 days (scope configurable) and the whitelist is logged with actor and reason.
Audit Logs and Data Retention Compliance (GDPR/CCPA)
Given any offer lifecycle event occurs (created, sent, accepted, declined, expired, auto-fallback, override), Then an immutable audit log entry is recorded with timestamp (UTC), class/session ID, pair/member IDs, policy snapshot, actor (system/user/admin), channel, reason code, and outcome. Given I open Admin > Audit Log, When I filter by date, class, event type, or user, Then results load within 2 seconds for up to 10,000 records and can be exported to CSV. Given studio retention is set to 365 days, When an entry ages past 365 days, Then PII fields are anonymized and aggregate counts are retained; changes to retention are applied prospectively and logged. Given a GDPR/CCPA erasure request is submitted for a user, When processed, Then their PII is deleted/anonymized across waitlist records, analytics, and logs within 30 days and a downloadable processing receipt is available. Given role-based access control, Then only users with the "View Logs" permission can access PII in audit logs; others see redacted values.
Manual Overrides: Convert Pair to Single and Force Offer
Given I am a class admin, When I select a pair and choose "Convert to single", Then I can split into one or two single waitlist entries, users are notified (including any required opt-in to single offers), and the action is recorded with reason and actor. Given there is an opening, When I click "Force offer" for a selected pair, Then an offer is sent immediately regardless of queue position (provided seats are available) and the bypass is flagged in audit logs with justification. Given a hold is active, When I adjust the hold timer to a value between 5 and 120 minutes, Then both users see the updated expiry and receive an updated reminder. Given I cancel an active hold via override, Then seats return to inventory immediately and both users receive a cancellation notice with the stated reason. Given any override occurs, Then guardrails are bypassed only for that action and a visible admin banner warns that an override is in effect.

Split Perk

Configurable incentives for Buddy Boost: Friend-Only Discount, Split Discount (both get savings), or Friend Discount + Inviter Credit. Clear microcopy at share and checkout sets expectations and highlights the deal. Aligns rewards to your goals—new-client growth, retention, or higher AOV—while maximizing take-up.

Requirements

Configurable Split Perks
"As a studio owner, I want to configure who gets the perk and how it’s applied so that I can align incentives with my goals for growth, retention, or higher order value."
Description

Provide an admin/UI and API to create and manage Buddy Boost incentive types: Friend-Only Discount, Split Discount (both parties receive savings), and Friend Discount + Inviter Credit. Support percent or fixed-amount discounts, min spend thresholds, per-order caps, eligibility rules (new clients only, returning, or all), class/event/category scoping, schedule windows, expirations, one-per-user/booking limits, and stacking rules with existing promos. Include validation, preview, and versioning, plus A/B variants per configuration. Enforce tax/fee handling preferences (pre/post-tax). Persist configurations with audit history and allow safe rollback. Expose read endpoints for checkout and share surfaces, and write endpoints secured by role-based access controls. Integrate with catalog, pricing engine, and policy services.

Acceptance Criteria
Admin creates Friend-Only Discount (percent) with validation and preview
Given an authenticated admin with role=Owner When they submit a new Buddy Boost configuration of type=Friend-Only Discount with percent_discount=15, fixed_amount=null, min_spend=40.00, per_order_cap=20.00, scope.category_ids=["Yoga"], schedule_window.start=today, schedule_window.end=today+30d, expiration_after_claim_days=14, one_per_user=true, one_per_booking=true, stacking=Disallow, tax_application=Pre-Tax Then the API responds 201 with config_id, version=1, status=Published, and normalized values And the response includes a calculation_preview for a $60.00 Yoga class showing discount=9.00, subtotal_before_discount=60.00, subtotal_after_discount=51.00 And validation errors are returned (422) if percent_discount and fixed_amount are both set or both null, if percent_discount>100, if per_order_cap<0, if schedule_window.end<start, or if scope references unknown categories And an audit entry is persisted with actor_id, timestamp, action=Create, and field diffs And the configuration is retrievable via GET /perks/{config_id} and matches saved values
Split Discount applied to friend checkout and inviter's next eligible booking
Given a published Split Discount config with percent_discount=10, per_order_cap=15.00, min_spend=0.00, scope.class_ids=[123], eligibility=All, tax_application=Pre-Tax When a friend F checks out class 123 using a valid share token linked to inviter U and the order subtotal is $120.00 pre-tax Then F sees a $12.00 discount applied and pays based on subtotal_after_discount=$108.00 plus tax And the system issues a one-time discount entitlement for inviter U scoped to class_ids=[123], amount_type=Percent(10), per_order_cap=15.00, expiration_days=30, and marks it reserved to U When inviter U books class 123 within 30 days with a pre-tax subtotal of $50.00 Then a $5.00 discount is auto-applied at checkout and the entitlement is consumed And if inviter U does not redeem within 30 days, the entitlement expires and cannot be applied And retrying or failing payments by F is idempotent: the Split Discount is applied at most once to F's successful order And both applications are recorded in a discount_ledger with references to config_id, order_id, user_id, and amounts
Friend Discount + Inviter Credit issuance, expiry, and redemption
Given a published Friend Discount + Inviter Credit config with fixed_amount_friend=10.00, fixed_amount_credit=10.00, min_spend=30.00, per_order_cap=10.00, eligibility=New Clients Only, scope.category_ids=["Pilates"], credit_expiration_days=60, tax_application=Post-Tax When a new client F checks out a $45.00 Pilates class via a valid share link Then F receives $10.00 off applied post-tax and pays tax computed on $45.00 with discount applied per policy And upon F's successful payment, inviter U is issued an account_credit of $10.00 with expiry 60 days from issuance and scoped to Pilates When U attempts to redeem the credit on an eligible $25.00 order Then $10.00 is applied up to the per_order_cap and the credit status becomes Consumed And if U attempts to apply the credit on an ineligible category or after expiry, the credit is not applied and the API returns 409 with reason=ineligible_or_expired And if F's order is fully refunded within the refund window, the inviter's credit is revoked if unused or reversed if already consumed; ledger reflects reversal entries
Eligibility rules enforced for new, returning, and all clients
Given a Buddy Boost config with eligibility=New Clients Only When a user with no completed bookings in the studio applies the perk Then the perk is eligible and may be applied When a user with at least one completed booking applies the perk Then the perk is rejected with code=INELIGIBLE_EXISTING_CLIENT and no discount or credit is issued Given eligibility=Returning Only When a user with at least one completed booking applies Then the perk is eligible When a user with no booking history applies Then the perk is rejected with code=INELIGIBLE_NEW_CLIENT Given eligibility=All Then both new and returning users are eligible And identity resolution uses unique user_id within studio, deduplicated by verified email/phone; audit logs capture the eligibility decision inputs and outcome
Stacking rules, one-per-user, and one-per-booking limits
Given stacking=Disallow When a cart already has any other promo/discount applied Then the Buddy Boost perk is not applied and the API returns applied=false with reason=NON_STACKABLE Given stacking=Allow When multiple discounts are present Then the Buddy Boost discount is applied after merchant promo in configured priority order and the combined discount never reduces subtotal below $0.00 Given one_per_user=true When the same user attempts to use the same config again Then the system prevents application with code=USER_USAGE_LIMIT_REACHED Given one_per_booking=true When two perks attempt to apply to the same booking concurrently Then only the first is accepted; subsequent attempts are rejected and optimistic locking prevents double application
Tax and fee handling: pre-tax vs post-tax calculations
Given tax_application=Pre-Tax and an order with subtotal=100.00, tax_rate=10%, processing_fee=3.00, percent_discount=20% with per_order_cap=50.00 Then discount_base=100.00, discount=20.00, taxable_amount=80.00, tax=8.00, total=88.00+3.00=91.00 Given tax_application=Post-Tax with the same order and discount Then discount_base=110.00 (subtotal+tax), discount=22.00, tax=10.00, total=(subtotal+tax-discount)=88.00; processing_fee remains 3.00 and is not discounted Given fixed_amount=15.00 with Pre-Tax Then discount=min(15.00, per_order_cap, subtotal) and calculations follow the same order of operations And all monetary calculations round to the currency minor unit using banker’s rounding; line-item totals reconcile to the order total
Versioning, A/B variants, RBAC, read endpoints, and rollback
Given a config v1 is Published When an editor creates v2 Draft with changes and defines A/B variants (A:50%, B:50%) Then users are randomly but deterministically assigned to a variant by user_id hash and the assignment is sticky for 30 days When GET /perks/offer is called with context (class/category/time) Then only Published configs scoped to the request are returned with variant-specific fields, display_text for share and checkout, and an ETag for caching; secrets are excluded Given POST/PUT/DELETE endpoints When a caller without role in {Owner, Admin, Marketer} invokes them Then the API returns 403 and an audit record with action=Denied is written When rolling back v2 to v1 Then v1 becomes the active Published version, v2 remains stored, audit history captures actor, reason, and diffs, and read endpoints serve v1 within <=60s And all endpoints are idempotent and return 429 with Retry-After on rate limit; dependencies (catalog, pricing, policy) failures cause the feature to fail closed
Shareable Invite Links & Attribution
"As an instructor, I want a single link I can share that reliably credits me for invitees so that I can grow my client base without manual tracking."
Description

Generate unique, fraud-resistant invite links and codes tied to the inviter and a specific perk configuration. Support deep links to mobile/web booking pages, UTM tagging, and campaign identifiers. Implement cross-device attribution using first-party storage, link tokens, and optional code entry fallback at checkout. Track lifecycle events (link created, clicked, account created, booking completed) and tie them to both parties. Support expiration, revocation, and rate limits. Provide admin tools to view/refetch links, revoke misuse, and merge attributions. Integrate with analytics pipeline and respect privacy/consent settings.

Acceptance Criteria
Invite Link Generation with Attribution and Deep Linking
Given an authenticated inviter selects a Split Perk configuration and target (class, collection, or storefront), When they generate an invite, Then the system creates a unique, non-guessable URL token (>=128-bit entropy) and a human-readable code (6–10 chars) both tied to inviter_id, perk_id, and target_id. And the link deep-links to the specified booking page and, when a class instance is targeted, preserves class_id and occurrence datetime. And the URL includes utm_source=buddy_boost, utm_medium=referral, utm_campaign=<campaign_slug>, and campaign_id=<uuid> exactly once each with correct values. And repeated generation with the same inputs within 24 hours returns the existing link (idempotent) and does not create duplicates. And the link record stores created_at, expires_at (nullable), and usage_counters initialized to zero.
Cross-Device Attribution with First-Party Storage and Fallback Code
Given a recipient clicks the invite on Device A, When they later open the booking flow on Device B within the 30-day attribution window, Then attribution succeeds via either authenticated session matching or validated invite code entry, creating exactly one attribution record. And on Device A, a first-party cookie/localStorage record invite_token=<token> with expiry=30 days is set when consent permits, and the server associates it to sessions started within that window. And if storage is unavailable or consent is denied, Then checkout prominently prompts for the invite code; valid codes are accepted and applied within 300ms p95. And multiple clicks or code entries by the same recipient within the window deduplicate to the earliest click as source; subsequent attempts do not create additional attributions.
Lifecycle Event Tracking and Analytics Integration
Given link_creation, link_click, account_creation, and booking_completed milestones, When each event occurs, Then the system emits a versioned analytics payload within 5 seconds p95 containing: event_type, event_id (UUIDv4), occurred_at (ISO-8601), link_id, inviter_id, recipient_id (nullable), perk_id, campaign_id (nullable). And the pipeline deduplicates using (event_id) and (event_type + link_id + subject_id) keys; duplicates are dropped with counters visible in monitoring. And booking_completed includes monetary fields: amount_gross, discount_amount, inviter_credit_awarded, currency (ISO-4217), and booking_id. And both inviter and recipient timelines reflect these events within 30 seconds; counts and states match analytics-derived aggregates.
Expiration, Revocation, and Abuse Rate Limiting
Given a link with expires_at set, When expires_at passes, Then link landing renders an expired state and the code is rejected at checkout with error_code=INVITE_EXPIRED. And when a link is revoked by an admin, Then API GET returns 410 Gone and the UI shows revoked messaging; checkout rejects with error_code=INVITE_REVOKED. And invite creation is rate-limited per inviter to 10/hour and 100/day by default (configurable); excess requests return HTTP 429 with Retry-After header. And redemptions per link are capped at the configured maximum (default 1 unless perk allows multi-use); anomalous spikes trigger an abuse flag and temporarily disable the link; legitimate single redemption is not blocked and validation latency remains <300ms p50.
Admin Controls for Viewing, Revoking, and Merging Attributions
Given an admin with role=owner or role=manager has access, When they search by inviter_id, link_code, recipient contact, or campaign_id with filters (status, date range, perk type), Then results return within 2 seconds p95 with pagination. And admins can copy/refetch a link, revoke/restore with reason codes {misuse, user_request, error}, and all actions are audit-logged with actor_id, action, target_id, timestamp, and before/after snapshots. And admins can merge attributions by selecting source and target; all events and credits are re-parented to target; source is marked superseded and hidden from standard views; operation is irreversible and logged. And all admin endpoints are permission-gated and return 403 for insufficient roles; changes propagate to analytics within 60 seconds.
Perk Application and Enforcement at Checkout
Given a link tied to a Split Perk configuration, When the recipient reaches checkout via link or valid code, Then the correct perk is auto-applied: Friend-Only Discount applies only to recipient; Split Discount applies to both parties; Friend Discount + Inviter Credit applies discount to recipient and defers credit to inviter post-successful booking. And the checkout displays clear microcopy including discount/credit amounts, eligibility constraints (e.g., new-client-only), and how inviter credit is earned; order totals and taxes reflect the perk accurately before payment. And if recipient is ineligible (e.g., not a new client where required), Then the perk is not applied and a non-PII message explains why; payment is still possible at full price. And inviter credit, when applicable, is issued only after payment succeeded (per configuration) and appears in the inviter wallet within 1 minute; the issuance is recorded in analytics.
Privacy, Consent, and Data Minimization Compliance
Given regional and user consent settings, When processing invites, Then only first-party cookies/localStorage are used and only after consent for non-essential tracking is granted; without consent, minimal functional tokens are used solely to apply the perk. And URLs and events exclude PII (no email/phone); IDs are pseudonymous (hashed or surrogate keys); retention for invite-related identifiers is capped at 13 months by default (configurable). And users can opt out or request deletion; subsequent attribution is disabled; upon deletion requests, invite-related personal data is removed or anonymized within 30 days and mirrored in user data exports.
Real-time Perk Application at Checkout
"As a new client, I want my friend discount applied automatically at checkout so that I feel confident I’m receiving the promised deal without extra steps."
Description

Detect and evaluate eligibility at checkout using attribution tokens and user state. Automatically apply the correct perk per configuration: show friend discount as a distinct line item; for Split Discount, apply inviters’ portion when both are in the same transaction, otherwise generate a pending credit to be issued on invitee completion; for Friend Discount + Inviter Credit, apply invitee discount now and queue inviter credit issuance. Enforce stacking rules, min spend, and one-per-order limits. Present clear price breakdowns and error states, and gracefully handle edge cases (multiple invites, expired links, currency differences). Expose idempotent APIs to recalculate totals and support retries. Integrate with taxes/fees, payment authorization, refunds, and chargeback flows.

Acceptance Criteria
Friend-Only Discount Applied with Valid Token
Given a checkout session has a valid, unexpired Friend-Only attribution token and the invitee is eligible and the cart meets the configured minimum spend When totals are calculated at checkout Then a single "Friend Discount" line item is applied to the invitee’s order per configuration, taxes and fees are recalculated accordingly, and the order total reflects the discount And the price breakdown displays subtotal, discount, taxes, fees, and final total with the correct currency code And the perk is applied only once per order and persists correctly across page refreshes and recalculation calls
Split Discount Applied When Inviter and Invitee Checkout Together
Given inviter and invitee are in the same checkout session linked by a valid Split Discount token and the cart meets the configured minimum spend When totals are calculated Then two distinct line items are applied: "Invitee Split Discount" and "Inviter Split Discount", each for their configured share, with taxes and fees recalculated And the combined discount equals the configured total benefit and is applied exactly once per order And the checkout summary clearly displays each party’s discount prior to payment authorization
Split Discount Pending Credit When Invitee Purchases Separately
Given an invitee checks out with a valid Split Discount token and the inviter is not present in the same transaction When the invitee’s payment is successfully authorized and captured Then the invitee’s discount is applied at checkout as a line item and an inviter credit record is created in "pending" status And the inviter credit is auto-issued (status "issued") only after the invitee’s order reaches a completed state per business rules and is not refunded within the capture window And duplicate recalculation or retry calls do not create duplicate pending or issued credits (idempotent on token plus order)
Friend Discount + Inviter Credit Flow
Given configuration is Friend Discount + Inviter Credit and a valid token is present for an eligible invitee meeting minimum spend When totals are calculated and payment is authorized Then the invitee discount is applied as a line item at checkout And an inviter credit record is queued for issuance and is auto-issued upon invitee order completion (not refunded), with an audit trail including token, order IDs, amounts, and timestamps And the checkout UI displays microcopy indicating the inviter will receive a credit after completion
Perk Stacking, Min Spend, and One-Per-Order Enforcement
Given multiple perk tokens or promo sources are present in a session When recalculating totals Then only one perk is applied per order according to configuration priority and highest net invitee benefit, and others are suppressed with a specific inline message And if the cart subtotal is below the configured minimum spend, the perk is not applied and the UI indicates the remaining amount needed to qualify And the system prevents stacking with other coupons or credits and logs the suppression reason And repeated recalculation or page refresh does not allow the perk to be applied more than once per order
Edge Case Handling for Multiple Invites, Expired Links, and Currency Mismatch
Given a user arrives with multiple attribution tokens When totals are calculated Then the system deterministically selects the token that yields the highest invitee discount; if tied, selects the most recent last-click token, and records the selection in the audit log And if the selected token is expired or revoked, no perk is applied and the UI shows "Perk expired" without blocking checkout And if the cart currency differs from the token’s currency, the perk is not applied and a message explains the mismatch; no currency conversion is performed And error states are returned via API with specific error codes and user-friendly messages without exposing internal details
Idempotent Totals API and Payment/Refund/Chargeback Integration
Given a client calls POST /checkout/recalculate with an idempotency key, cart contents, pricing context, and attribution tokens When the same request is retried with the same idempotency key within the idempotency window Then the API returns an identical totals breakdown (subtotal, discounts by line, taxes, fees, total, currency) and no duplicate credits or perks are created And the totals snapshot used for payment authorization matches the recalculation response used to authorize the payment And upon full or partial refund, associated issued inviter credits are proportionally reversed via negative credit adjustments; pending credits are canceled And upon chargeback, any related issued credits are clawed back via negative adjustments and the order is marked to block re-issuance on retries
Inviter Credit Wallet & Redemption
"As a returning client who invited a friend, I want my earned credit to appear automatically and be easy to use at checkout so that I clearly benefit from inviting friends."
Description

Implement a ledger-backed wallet for inviter credits with issuance, balance tracking, expiration, and redemption at checkout as a tender or discount line. Trigger credit issuance upon invitee booking success, with safeguards for cancellations, refunds, and chargebacks (auto-reversal or hold periods). Support partial redemptions, multi-currency rules, minimum spend, and per-user caps. Provide user-facing balance views, history, and notifications hooks. Include admin adjustments, exports, and audit logs. Integrate with payments, refunds, anti-abuse (velocity, self-invite detection), and taxation reporting as applicable.

Acceptance Criteria
Credit Issuance on Invitee Booking Success and Hold Period
Given an inviter has an active Split Perk with Inviter Credit configured and an invitee uses their referral link, When the invitee’s booking payment is successfully captured (status = Paid), Then create a pending credit ledger entry with amount per rule, booking currency, source_booking_id, inviter_user_id, status = pending, hold_until = now + configured_hold_days, expires_at = now + configured_expiry_days, and an idempotency key tied to booking and inviter. Given hold_until elapses and the source booking has not been canceled, refunded, or charged back, When the hold job runs, Then transition the ledger entry to status = available and fire a credit_available notification hook. Given the source booking is fully refunded, canceled, or charged back before hold_until, When the reversal event is received, Then change the pending ledger entry to status = canceled and fire a credit_revoked hook. Given a partial refund occurs before hold_until, When the event is received, Then proportionally reduce the pending credit amount or cancel it if below the configured threshold; update the ledger accordingly. Given a duplicate booking event is received for the same inviter and booking, When issuance is attempted, Then do not create a second credit (idempotency enforced).
Wallet Balance Display and Transaction History
Given a logged-in user with wallet activity, When they open the Wallet view, Then display available and pending balances per currency computed from ledger entries with latency ≤ 2 seconds. Given the user views transaction history, When the history loads, Then show a paginated, filterable list of entries (issued, redeemed, expired, reversed, adjusted) with amount, currency, status, source_booking_id, created_at, updated_at, and description, and totals reconcile with balances. Given the user has no credits, When they view the Wallet, Then show a zero-state and no errors. Given notification hooks are enabled, When a credit is issued, becomes available, is redeemed, or is T days before expiration, Then fire the respective webhooks with documented payloads.
Checkout Redemption as Tender or Discount with Partial Redemptions
Given a user has available credits in the checkout currency and the order meets the minimum spend, When at checkout, Then present an Apply Credit control showing available amount and the configured mode (tender or discount). Given the user applies an amount ≤ available balance and ≤ order total, When they confirm, Then subtract that amount from the payable total if tender mode or add a discount line if discount mode; ensure tax is computed accordingly (tender: tax base unchanged; discount: tax base reduced per jurisdiction flag). Given multiple credit entries exist, When applying credit, Then consume credits by earliest expires_at then oldest created_at, and record a redemption ledger item per source credit. Given the user modifies or removes the applied credit, When they update the checkout, Then recompute totals and restore the wallet balance immediately. Given available credit is less than the order total, When applying max, Then accept partial redemption and require another payment method for the remainder. Given currency rounding rules, When applying credit, Then round to minor units and never produce a negative payable amount.
Refunds, Cancellations, and Chargebacks Auto-Reversal
Given a booking that generated inviter credit is refunded, canceled, or charged back after the credit became available, When the reversal event is received, Then create a negative adjustment removing the credit; if already redeemed, Then create a negative balance if needed and flag the account; fire a credit_reversed hook. Given a booking paid using inviter credit is refunded, When the refund is processed, Then return the credit portion to the user’s wallet as available and return the cash portion to the original payment method, preserving correct tax treatment. Given a partial refund of a booking paid with credit occurs, When processed, Then restore credits proportionally up to the redeemed amount. Given concurrent or duplicate refund events, When processed, Then handle idempotently using idempotency keys so no duplicate reversals are created.
Multi-Currency, Minimum Spend, Expiration, and Per-User Caps
Given credits are stored per currency, When a user checks out in a different currency, Then disallow redemption unless FX conversion is enabled; if enabled, Then convert at the configured rate provider at application time, show the effective rate, and round to minor units. Given a merchant sets a minimum spend for redemption, When the order subtotal is below the threshold, Then disable Apply Credit with an explanatory message. Given per-user issuance and balance caps are configured, When a new credit would exceed caps, Then block issuance and log a reason; fire an issuance_blocked_cap hook. Given credits have an expires_at timestamp, When the expiration job runs daily at 00:05 UTC, Then move expired credits to status = expired, remove from available balance, and fire a credit_expired hook; send a reminder notification T days before expiration. Given multiple credits exist, When redeeming, Then always consume those closest to expiry first.
Anti-Abuse Controls (Self-Invite Detection and Velocity Limits)
Given an invitee books using an inviter link, When the system detects self-invite signals (same user account, same payment card fingerprint, same device fingerprint, or same email), Then block credit issuance and label the event rejected_abuse with an audit trail. Given an inviter reaches N credits issued in a rolling 24-hour window (configurable), When the (N+1)th issuance occurs, Then set the new credit to status = review or reject per configuration and do not make it available. Given repeated abuse signals from an account, When thresholds are exceeded, Then automatically suspend further credit issuance for that inviter and notify admins via webhook. Given a blocked issuance, When the inviter views their wallet history, Then show an entry with status = rejected and a reason code; do not include it in balances.
Admin Adjustments, Exports, Audit Logging, and Tax Reporting
Given an authorized admin opens the wallet admin panel, When they apply a manual credit or debit adjustment with a reason code, Then create an immutable ledger entry with actor_id, reason, note, and amount; update balances accordingly; hard deletes are not permitted. Given an auditor requests a ledger export for a date range, When the export is generated, Then produce a CSV with entry_id, user_id, type (issue/redeem/expire/reverse/adjust), mode (tender/discount), amount, currency, status, source_booking_id, created_at, updated_at, actor_id, reason, and a checksum; totals reconcile to the ledger. Given taxation reporting is enabled, When a redemption is applied, Then tag the entry with tax_treatment = tender or discount and include it in a monthly tax report (available by T+3 days) aggregating discount-mode as promotional discounts and tender-mode as non-taxable tender. Given any wallet mutation via API or web, When it occurs, Then write an audit log with actor (user/admin/service), IP, user-agent, timestamp, and before/after balance snapshots.
Contextual Perk Microcopy & Templates
"As a customer, I want clear, concise explanations of the perk when I share and when I pay so that I understand exactly what I’ll receive and any conditions."
Description

Deliver template-driven, locale-aware microcopy for share surfaces, invites, and checkout (banners, line items, terms) that clearly sets expectations: who gets what, how much, when it applies, and any limits. Provide variables for dynamic values (discount amount, expiry, min spend, class name) and conditionals per perk type. Include previews in admin, inline linting for compliance and clarity, and channel variants for SMS/email/web. Ensure accessibility and mobile-first rendering. Allow A/B copy variants tied to configurations and capture impression/click metrics.

Acceptance Criteria
Locale-Aware Template Rendering Across Surfaces
Given a perk configuration and a detected user locale (with merchant default as fallback) When microcopy is rendered for share, invite, and checkout surfaces Then numbers, dates, times, and currencies are formatted per CLDR for the target locale and currency code And pluralization and grammatical gender (where applicable) are correct for the locale And locale fallback order is user -> merchant default -> en-US And right-to-left locales render with correct text direction and punctuation mirroring And all copy includes explicit statements of who benefits, how much, when it applies, and any limits And time zones used in copy reflect the merchant/venue time zone for event-specific messaging And no hardcoded English appears when a translation is provided for the locale
Dynamic Variables and Safe Defaults
Given a template containing variables {discount_amount}, {expiry_date}, {min_spend}, {class_name}, {perk_type}, and {credit_amount} When rendering the template Then 100% of present variables are substituted with formatted values (no unresolved {tokens}) And currency values adhere to minor units (e.g., USD cents) and rounding rules And conditional blocks include/exclude clauses based on variable presence (e.g., omit min spend clause if {min_spend} is null) And if {expiry_date} is missing, a localized default phrase (e.g., “until further notice”) is used And percentage vs fixed-amount discounts are correctly detected and labeled And snapshot tests verify no dangling punctuation or double spaces after conditional omission And an error is logged and rendering is aborted (without publishing) if any required variable is missing
Perk-Type Conditional Logic (Friend-Only, Split, Friend+Inviter Credit)
Given a selected perk type of Friend-Only Discount, Split Discount, or Friend Discount + Inviter Credit When generating share, invite, and checkout microcopy Then exactly one incentive set is described, matching the selected perk type And Friend-Only Discount states the friend’s discount and omits any inviter benefit And Split Discount clearly states both parties receive the same discount And Friend Discount + Inviter Credit states the friend’s discount and the inviter’s credit amount, accrual timing, and redemption constraints And any configured min spend, class eligibility, and usage limits are included where applicable And checkout line items/terms match the promise in share/invite copy And automated tests assert no contradictory benefits appear for any perk type
Admin Previews with Real Values and Mobile-First
Given an admin is configuring a perk and selects a channel (SMS, Email, Web) and locale When updating configuration values or switching perk type/locale Then the preview updates within 300 ms in the UI And realistic sample data populates variables to show complete sentences And device preview defaults to mobile viewport with a toggle for desktop (web/email) And SMS preview enforces character counters and segment estimation And checkout banner and line-item previews render without truncation at common mobile widths (e.g., 320–414 px) And the admin can cycle through share, invite, and checkout surfaces for the same configuration And previews can be copied/exported for stakeholder review
Inline Linting for Compliance and Clarity
Given an admin edits a template When content is saved or pauses for 500 ms Then linting runs and returns results within 200 ms And errors are raised if required elements (who, what, how much, when, limits) are missing And errors are raised for prohibited terms (e.g., “free” unless discount_amount == 100%) and misleading claims And warnings are raised if Flesch-Kincaid grade level exceeds 8 or if passive voice exceeds 20% And channel-specific limits are enforced: SMS <= 160 GSM-7 chars or <= 70 UCS-2 chars; Email subject <= 78 chars; Web banners avoid overflow at 320 px And publish is blocked on any errors but allowed with warnings (with a visible acknowledgment) And each lint message highlights the offending text and provides a suggested fix
Channel Variant Rendering, Limits, and Accessibility
Given SMS, Email, and Web channel variants are configured When rendering each variant Then each channel uses its respective template and fallbacks to the default channel only if its template is empty And SMS includes a short link (< 23 chars) and total length stays within the enforced segment limits And Email includes localized subject and preheader; links include UTM parameters with {variant_id} And Web banners/terms meet WCAG 2.1 AA for contrast (>= 4.5:1), keyboard focus visibility, and screen-reader readable copy And all images/icons used in web/email have descriptive alt text or aria-labels And language attributes (lang) match the rendered locale for assistive technologies And no critical content is conveyed by color alone
A/B Copy Variants and Metrics Capture
Given a perk configuration allows multiple copy variants When A/B testing is enabled Then up to 5 variants per channel can be created and assigned traffic weights that sum to 100% And users are randomly assigned a variant with stable bucketing for 30 days per config And all impressions and clicks are logged with timestamp, channel, locale, perk_type, variant_id, and session_id And metrics are queryable in the dashboard within 15 minutes of the event And variant-level CTR and exposure counts are accurate within ±2% And disabling a variant immediately routes its traffic to remaining variants proportionally And exporting raw events as NDJSON/CSV is available for audits
Perk Analytics & Goal Recommendations
"As a studio owner, I want to see which perk settings drive my chosen goal so that I can refine incentives and maximize return."
Description

Expose dashboards and exports that attribute invites to outcomes: invites sent, acceptance rate, conversion to paid, incremental bookings/revenue, new vs. returning client mix, AOV lift, cohort retention, and ROI by perk type and configuration. Visualize budget burn, caps, and breakage. Support A/B comparisons and time slicing. Provide goal-oriented recommendations (optimize for new-client growth, retention, or AOV) using observed performance and suggest parameter tweaks (discount level, min spend, eligibility). Feed insights back into configuration drafts via one-click apply proposals. Respect privacy and allow data deletion requests to cascade.

Acceptance Criteria
KPI Attribution Dashboard Accuracy
Given a selected time range, timezone, and a perk type/configuration filter When the analytics dashboard loads Then the metrics invites sent, acceptance rate, conversion to paid, incremental bookings, incremental revenue, new-vs-returning mix, AOV lift, 30/60/90-day cohort retention, and ROI are displayed And each metric matches the server aggregation API for the same filters within 0.1% for counts and 0.01 absolute for rates And all timestamps reflect the selected timezone and all currency values reflect the account currency
Analytics Export Completeness and Fidelity
Given selected filters and chosen columns When the user exports data as CSV and JSON Then the file contains one row per invite and one row per conversion in their respective export modes with the selected columns populated And the row count matches the dashboard totals for the same filters And timestamps are ISO 8601 with timezone offset; currency fields include ISO currency code and two-decimal precision And exports exclude personal identifiers unless the requester has PII_EXPORT permission And deletion-suppressed subjects are absent from the export And an export of up to 100,000 rows completes within 30 seconds
A/B Comparison and Time Slicing
Given two perk configurations (A and B) and a common time window When the user enables A/B Comparison for a selected goal metric Then the dashboard shows sample sizes, metric values for A and B, absolute and percent lift, and a 95% confidence interval And if minimum sample size of 200 per arm is not met, the comparison is labeled Insufficient Data and no significance badge is shown And time slicing by day/week/month preserves the same lift and significance calculations per slice
Budget Burn, Caps, and Breakage Visualization
Given an active perk with a defined budget and cap settings When the user views the Budget panel Then budget consumed, remaining budget, projected run-out date, cap utilization percent, and breakage percent are displayed and align with server calculations within 0.1% And when a cap is reached, a Cap Reached warning is shown and projections stop at the cap date And breakage excludes pending offers older than the configured expiration window
Goal-Oriented Recommendations Generation
Given a selected optimization goal (New Clients, Retention, or AOV) and a lookback window When the user requests recommendations Then at least three parameter proposals are generated specifying discount level, minimum spend, and eligibility rules And each proposal includes a predicted impact range for the primary goal metric and an ROI estimate with a confidence score and lookback period used And recommendations are produced within 10 seconds or an informative retry message is shown
One-Click Apply of Recommendation to Draft
Given a selected recommendation and the user has CONFIG_EDIT permission When the user clicks Apply as Draft Then a new draft configuration is created with the recommended parameters and tagged with the source recommendation ID And validation is enforced against configured bounds (discount within allowed limits, minimum spend within allowed limits); failures block save with inline errors And the draft appears in the Configurations list with status Draft and a diff view is available versus the current live configuration
Privacy and Deletion Cascade in Analytics
Given a verified deletion request for a data subject When the deletion job completes Then all analytics aggregations and exports exclude that subject’s events within 24 hours, and any stored identifiers are pseudonymized or removed per policy And affected KPIs are recomputed, and dashboards/exports reflect updated values on next refresh And an auditable log entry records the deletion cascade completion without exposing personal data

Boost Guard

Built-in fraud controls block self-referrals, duplicate devices, and serial cancellations, with caps per user and session. Transparent on-page rules and friendly error copy reduce confusion while protecting margins. Keeps the program fair and sustainable without manual audits.

Requirements

Device Fingerprint & Duplicate Booking Block
"As a studio owner, I want duplicate devices automatically blocked from creating multiple accounts or bookings so that referral abuse and slot hoarding are prevented without manual audits."
Description

Implement a privacy-compliant device fingerprint to detect and block multiple accounts or bookings originating from the same device within configurable windows. Combine server-side heuristics (IP, user agent, screen/device characteristics), hashed payment instrument metadata, and account identifiers to create a durable, rotating risk identifier. Apply checks at critical flows (account creation, booking, checkout, referral redemption, and waitlist auto-offer acceptance) to prevent duplicate device abuse. Provide soft blocks with step-up verification (e.g., SMS OTP) as a fallback and hard blocks for confirmed abuse. Log decisions and signals for review, expose aggregate metrics in analytics, and ensure minimal latency for mobile-first pages. Integrate with Smart Waitlist to avoid offering openings to flagged devices or require prepayment before confirming.

Acceptance Criteria
Block Duplicate Account Creation From Same Device Within 30 Days
- Given a device risk identifier R linked to ≥1 active accounts in the past 30 days, When a user attempts to create a new account from a device producing R, Then require SMS OTP step-up verification before account creation proceeds. - Given the step-up verification is completed successfully within 5 minutes and ≤3 attempts, When validation passes, Then allow account creation and tag the decision as "Soft Allow" with reason code OTP_PASS. - Given the device risk identifier R is linked to >K (default 2) accounts in the past 30 days or OTP fails/expires, When the user attempts account creation, Then hard block creation with error code ACCT_DUP_DEVICE and display friendly copy including rule summary and support link. - Given any block or allow decision, When the action is taken, Then log risk signals, decision, rule_ids, correlation_id, and timestamp for audit with 180-day retention.
Block Multiple Bookings From Same Device Within Configurable Window
- Given a confirmed booking exists for session S from any account associated with device risk identifier R within window W (default 24 hours) prior to start time, When another account on a device producing R attempts to book S, Then block the booking with error code BOOK_DUP_DEVICE and show friendly guidance. - Given config allow_step_up_for_duplicate_booking=true and R is linked to ≤K bookings for S, When SMS OTP step-up passes, Then allow the booking only if payment is captured immediately (no pay-later) and mark as PREPAID_RISK. - Given step-up fails or R exceeds caps, When booking is attempted, Then hard block and log reason_code DUP_DEVICE_BOOKING_CONFIRMED. - Given any decision, When processed, Then emit analytics event booking_duplicate_checked with decision and increment per-session duplicate counters.
Step-Up Verification on Suspicious Referral Redemption
- Given a referral redemption is initiated from a device risk identifier R that has redeemed ≥1 referral in the past 30 days or matches a payment instrument hash used for prior redemptions, When the user attempts to redeem, Then require SMS OTP verification. - Given OTP passes and R has not exceeded per-device referral cap C (default 1 per 30 days), When redemption continues, Then allow a single redemption and tag as REFERRAL_VERIFIED for 24 hours. - Given OTP fails or cap exceeded, When redemption is attempted, Then hard block with error code REF_DUP_DEVICE and present friendly copy with next steps. - Given any decision, When completed, Then log rule_ids, reason_codes, and link redemption to risk_id and payment_fingerprint.
Hard Block for Confirmed Abuse at Checkout
- Given the payment instrument fingerprint P (hashed) has been used by >M distinct accounts (default 2) in the past 24 hours or device risk identifier R has a 30-day cancellation rate >X% (default 50%) across ≥N bookings (default 4), When the user attempts checkout, Then hard block with error code CHK_ABUSE and display friendly explanation and support link. - Given a hard block occurs, When the user retries within 24 hours, Then continue to block unless a manual_override flag is set on the account. - Given only a soft risk threshold is met (score ≥T but not abusive), When checkout is attempted, Then require full prepayment (no reservation hold) and disallow promo/referral stacking; on success, mark order PREPAID_RISK.
Waitlist Auto-Offer Excludes Flagged Devices or Requires Prepayment
- Given a waitlist auto-offer is being assigned for session S, When the candidate account’s latest device risk identifier R is flagged High Risk in the last 7 days, Then exclude the account from the offer selection pool. - Given a waitlist offer is opened on a device producing risk identifier R flagged Medium Risk, When the user attempts to accept the offer, Then require full prepayment before confirming the spot. - Given prepayment succeeds, When confirmation is issued, Then mark the reservation PREPAID_RISK and notify the user; otherwise, expire the offer and return the spot to the queue. - Given any exclusion or enforcement, When processed, Then emit analytics event waitlist_risk_action with action_type and reason_code.
Risk Identifier Construction, Rotation, and Privacy Compliance
- Given device/session signals (IP subnet, user agent, screen/device characteristics) and hashed payment instrument metadata are available, When generating a risk identifier, Then create R using salted hashing without storing raw PII and without persisting full IPs beyond /24 subnet granularity. - Given rotation interval is configured (default 7 days), When rotation occurs, Then generate a new R' linkable to prior R via privacy-safe linkage with measured collision rate <1% and re-identification risk controls documented. - Given a data deletion request (GDPR/CCPA), When processed, Then delete or pseudonymize all identifiers and related logs for the subject within 30 days and exclude erased data from future decisions.
Logging, Analytics, and Latency SLOs for Risk Decisions
- Given any risk decision at account creation, booking, checkout, referral redemption, or waitlist acceptance, When finalized, Then log fields: request_id, account_id (if available), risk_id, decision (allow/soft_allow/block), rule_ids, reason_codes, signal snapshot, and server processing_time_ms with 180-day retention. - Given production traffic, When measuring server-side risk check time, Then meet p95 ≤120 ms and p99 ≤200 ms per flow over a rolling 24-hour window. - Given the risk check runs on mobile-first pages, When measuring page load impact, Then add ≤50 ms to TTFB p95 and never block the client main thread >10 ms. - Given logs are ingested, When dashboards refresh, Then expose metrics: blocked attempts per flow, step-up rate, step-up pass rate, prepayment enforcement rate, and processing latency with data latency ≤15 minutes and alerting on SLO burn. - Given a monitoring threshold is breached, When block rates exceed configured limits for 15 minutes, Then trigger an on-call alert with flow, rule_ids, and recent trend.
Self‑Referral & Coupon Abuse Prevention
"As a program manager, I want self-referrals and coupon abuse to be automatically blocked so that incentives reward genuine new customers and margins are protected."
Description

Prevent users from redeeming referral or promo incentives on themselves or through collusive loops by validating relationships between referrer and redeemer in real time. Cross-check email and phone normalization, payment method fingerprints, device fingerprints, IP/CIDR proximity, and matching profile attributes to block self-referrals and rapid circular referrals. Enforce configurable rules (e.g., one referral credit per unique new payer, exclude same card/device/household within X days) with transparent, friendly error copy and a link to policy. Provide overrides and allowlists for support, log all attempts with reason codes, and surface impact metrics on saved costs. Ensure compatibility with existing checkout, payments, and referral issuance flows without adding friction for legitimate users.

Acceptance Criteria
Real-Time Identity Match Block at Checkout
Given a user enters a referral or promo code during checkout When the redeemer’s normalized email OR phone matches the referrer’s normalized email OR phone Then deny the redemption and display: "This code can’t be used on your own account" with a visible link to /policies/referrals And do not apply any discount or issue any referral credit And allow the user to complete checkout without the promo And log an event with reason_code "SELF_MATCH" including hashed identifiers, tenant_id, campaign_id, timestamp And the decision latency is ≤ 300 ms at p95 And legitimate redemptions with non-matching identities proceed with no additional steps
Payment, Device, and IP Proximity Rule Enforcement
Given a user applies a referral or promo code When the redeemer shares a payment fingerprint OR device fingerprint OR is within the configured IP/CIDR proximity of the referrer within the last X days (default 7, configurable 1–30) Then block the redemption with friendly copy referencing recent use of the same payment method or device and include a link to /policies/referrals And do not apply any discount or issue any referral credit And log an event with reason_code "FINGERPRINT_MATCH" including matched_dimensions and age_days And respect allowlisted users/devices/cards so that the redemption proceeds while logging reason_code "OVERRIDE_BYPASS" And enforce decision latency ≤ 300 ms at p95
Circular Referral Loop Detection and Prevention
Given user B has redeemed user A’s referral When user A (or an account in the same household cluster) attempts to redeem user B’s referral within Y days (default 30, configurable 7–60) Then deny the redemption and show friendly copy explaining circular referrals aren’t allowed with a link to /policies/referrals And revoke any pending referral credits in the loop that have not yet been used And log an event with reason_code "CIRCULAR_LOOP" including chain_ids and window_days And ensure unrelated, unique new payers are not blocked by this rule And maintain decision latency ≤ 400 ms at p95 to accommodate historical lookups
Configurable Caps and Exclusions per Tenant
Given tenant rules specify "one referral credit per unique new payer" and "exclude same household/device/card within X days" When multiple bookings from the same new payer would award credits to the same referrer Then issue credit only for the first qualifying booking and block subsequent attempts with copy indicating the limit has been reached and a link to /policies/referrals And when an admin updates X days or toggles the caps, the next validation enforces the new values without a code deploy And persist all configuration changes with audit logs (who, what, when, old→new) and validation of ranges (days within 1–60) And unit and integration tests verify enforcement for each configurable rule
On-Page Rule Transparency and Friendly Error Messaging
Given the checkout page displays a promo/referral entry When the page loads Then show a concise rules summary (e.g., one per unique new payer; no same device/card/household within X days) with a visible link to /policies/referrals And when a redemption is blocked by any rule Then display user-friendly, localized error copy that explains the issue without exposing internal codes and includes the policy link And ensure WCAG AA contrast and that the message is focusable and screen-reader readable And ensure the message persists until dismissed and does not clear the cart or reset form fields
Support Overrides and Allowlists
Given a support agent with role "Support Admin" accesses the Boost Guard console When they add a user_id, card_fingerprint, device_fingerprint, or IP to an allowlist with a TTL (minutes to days) and reason text (min 10 chars) Then subsequent validations bypass the matching rule while logging an event with reason_code "OVERRIDE" referencing the original rule and agent_id And allowlist changes propagate and take effect within 2 minutes And agents can revoke or shorten TTL, with all actions audit logged (who, what, when) And non-admin users cannot create or edit overrides
Comprehensive Logging and Savings Impact Metrics
Given any referral/promo validation attempt occurs When the decision is made Then write an immutable event including tenant_id, user/account ids, decision (allow/block), reason_code, matched_signals, estimated_savings, and latency_ms with PII hashed using SHA-256 with tenant-specific salt And surface aggregated metrics in a Fraud Impact dashboard: total blocked attempts, block rate, estimated savings, top reason_codes, filters by date range, campaign, tenant And ensure data freshness ≤ 15 minutes at p95 and CSV export supports up to 100k rows And trigger an alert if ingestion error rate > 1% for 5 consecutive minutes
Serial Cancellation Limits & Consequences
"As an instructor, I want limits on repeat cancellations with clear consequences so that my classes stay full and reliable without constant manual policing."
Description

Track cancellations per user over rolling time windows and enforce configurable thresholds to deter habitual no-shows and late cancels. When limits are exceeded, apply proportionate consequences such as temporary booking locks, forfeiture policies, prepayment requirements, or deprioritization on the Smart Waitlist. Capture standardized cancellation reasons, notify users with clear, empathetic messaging, and provide a path to appeal or resolve errors. Integrate with payments for automated fee handling where allowed, respect studio-specific policies, and expose analytics to quantify reduction in no-shows and churn risk.

Acceptance Criteria
Rolling Cancellation Tracking & Threshold Enforcement Per Studio
- Given studio S configures {rollingWindowDays: 30, countTypes: ["late_cancel","no_show"], timezone: S.timezone}, When user U late-cancels or no-shows a class at S, Then U’s rolling count increments by 1 and each event expires exactly 30 days after its eventTimestamp in S.timezone. - Given the same configuration, When U cancels on-time or the class is canceled by the studio/instructor/system, Then the event does not increment the rolling count. - Given U has activity across multiple studios, When counts are calculated, Then counts are isolated per studio and do not aggregate across studios. - Given deduplication rules, When multiple actions reference the same bookingId, Then only one qualifying cancellation is counted. - Given real-time evaluation, When U attempts to book or join a waitlist, Then the current rolling count is calculated at request time and used to determine eligibility and consequence triggers.
Consequence Tier Mapping, Deprioritization & Auto-Expiry
- Given studio S defines tier rules (e.g., at count=2 => warning; count=3 => prepayment_required 14d; count=4 => booking_lock 7d; count=5 => waitlist_deprioritize 30d), When U’s rolling count crosses a tier threshold, Then the configured consequence is applied exactly once, recorded with start/end timestamps, and visible on U’s account. - Given multiple tiers are eligible simultaneously, When applying consequences, Then only the most severe consequence is active and lesser ones are suppressed. - Given a consequence with duration D, When the end time is reached, Then the consequence auto-expires without manual intervention and U’s state is restored. - Given waitlist_deprioritize is active with a configured weight penalty, When Smart Waitlist ranks candidates, Then U’s rank weight reflects the penalty and U is placed below non-deprioritized users at equal base score. - Given admin override, When an override is applied, Then the targeted consequence is paused, cannot auto-reapply during the override window, and the action is audit logged with actor, timestamp, and reason.
Temporary Booking Lock Enforcement
- Given U has an active booking_lock consequence, When U attempts to book or join a waitlist via web, mobile, or API, Then the action is blocked, no reservation is created, and U sees friendly error copy stating the policy and the unlock date/time. - Given a booking_lock, When U manages existing reservations, Then U can cancel existing bookings but cannot modify them into new time slots. - Given admin privileges, When an authorized admin books on behalf of U, Then the booking is allowed and the override is captured in an immutable audit log. - Given lock expiry, When the lock end time is reached, Then bookings are permitted immediately (no cache delay > 60 seconds).
Payments Integration: Prepayment Requirement & Automated Fees
- Given U has prepayment_required active, When U proceeds to checkout, Then only immediate payment methods are shown, “pay at door” options are hidden, and the booking is created only after successful authorization/capture. - Given studio S configures late_cancel_fee and/or no_show_fee and charging is allowed in the user’s locale, When U late-cancels or no-shows, Then the configured fee is automatically charged to U’s default payment method within 15 minutes and a receipt is sent. - Given a fee charge fails, When retries are executed, Then the system follows the dunning schedule (min 3 attempts over 7 days), notifies U after each failure, and restricts new bookings after final failure until the balance is paid. - Given an approved appeal reversing a fee or prepayment requirement, When the reversal is processed, Then collected fees are refunded within 3 business days and the prepayment flag is cleared across web, mobile, and API surfaces.
Analytics & Reporting on No-Shows, Fees, and Churn Risk
- Given studio S has active Boost Guard, When admins open Analytics > Cancellations, Then time-series metrics for cancellation rate, late-cancel/no-show counts, users under consequences, and fees charged are displayed and filterable by date range, class, and cohort. - Given data export requirements, When admins export CSV or call the reporting API, Then each record includes userId, studioId, classId, eventType, consequenceState, feeCharged, appealOutcome, and ISO-8601 timestamps. - Given baselining is configured, When viewing the dashboard, Then the system displays lift metrics (e.g., reduction in no-shows vs prior period) and shows the comparison window and methodology. - Given churn risk scoring is enabled, When segmenting users, Then users with repeated consequences are flagged with a churnRisk band and segment counts are visible and exportable.
Standardized Cancellation Reasons & Counting Rules
- Given U initiates a cancellation, When the cancellation dialog is shown, Then U must select a reason from a studio-defined list or choose “Other” and enter at least 10 characters before confirming. - Given a reason is submitted, When the event is saved, Then the system stores reasonCode, freeText (if provided), cancellationType (on-time/late/no-show), source (user/admin/system), and timestamps. - Given exempt rules, When the cancellation is studio-initiated or matches an exempt reason (e.g., weather emergency) configured by S, Then the event is excluded from rolling count and automated fees. - Given localization and accessibility, When the reason list is rendered, Then it is localized to the user’s language and is fully operable via screen readers and keyboard navigation.
User Notifications & Appeal Workflow
- Given U crosses a threshold and a consequence is applied, When the state change occurs, Then an in-app banner appears immediately and email/SMS are sent within 60 seconds including policy summary, consequence duration, unlock date/time (studio timezone), and an appeal link. - Given U submits an appeal via the provided link, When required fields are completed (reason ≥ 20 characters, optional attachments), Then a case is created with status=Open, U receives confirmation, and studio admins are notified. - Given an admin resolves an appeal, When the outcome is set to Approved or Rejected, Then U is notified, counters and consequences are adjusted accordingly, applicable fees are refunded on approval, and all actions are audit logged. - Given message template customization, When a studio edits copy within allowed variables, Then previews are available and messages must still include mandatory elements: policy link, duration, next steps, and support contact.
Per‑User and Per‑Session Booking Caps
"As a studio owner, I want caps on how many times a user can book or redeem within a period so that spots are distributed fairly and discounts aren’t exploited."
Description

Enforce configurable caps on actions such as bookings per day/week, free trial uses, referral redemptions, and checkout attempts per session to prevent hoarding and burst abuse. Implement server- and client-side rate limiting with idempotency keys to avoid double charges while blocking rapid repeats. Provide studio-level defaults and per-offer overrides, ensure caps are evaluated before payment authorization to reduce reversals, and render concise pre-checkout notices so users understand limits. Integrate with Smart Waitlist to skip or throttle offers to capped users.

Acceptance Criteria
Pre‑Auth Per‑User Daily/Weekly Booking Caps
Given studio booking caps are configured as daily=2 and weekly=5 in the studio’s timezone And the user has 2 confirmed bookings today and 4 confirmed bookings this calendar week When the user initiates checkout for another booking Then the system evaluates caps before any payment authorization And the booking is blocked with HTTP 429 and error_code="cap_exceeded" And the response includes remaining_allocation={"daily":0,"weekly":1} and reset_at timestamps in ISO 8601 And no payment authorization or capture is created
Offer‑Level Overrides Take Precedence Over Studio Defaults
Given the studio default free_trial_uses_per_user=1 and an offer override sets free_trial_uses_per_user=2 for Offer A And the user has redeemed 1 free trial on Offer A When the user attempts another free trial redemption on Offer A Then the redemption is allowed with HTTP 200 and usage recorded as 2/2 for Offer A And a third attempt within the same cap window is blocked with HTTP 429 and error_code="cap_exceeded" And the override does not affect caps on other offers
Referral Redemption Caps with Self‑Referral and Device Duplicate Checks
Given referral_redemptions_per_user=1 and device fingerprinting is enabled And the redeemer shares either the same account, payment instrument, or device fingerprint as the referrer When a referral code is submitted at checkout Then the redemption is blocked with HTTP 403 and error_code="self_referral_blocked" And the response contains a friendly_message explaining the rule and steps to proceed without the code And the attempt does not decrement the user’s legitimate redemption allowance And no payment authorization or booking is created
Per‑Session Checkout Attempt Cap and Burst Rate Limiting
Given checkout_attempts_per_session=3 per 10‑minute sliding window and client SDK rate limiting is enabled And the user submits 4 checkout requests within 2 minutes using the same session_id When requests arrive concurrently or in rapid succession Then the server returns HTTP 429 with error_code="rate_limited" for attempts exceeding the cap and includes retry_after (seconds) And the client SDK disables the checkout action and displays a friendly_message until retry_after elapses And blocked attempts do not create payment authorizations or bookings
Idempotency Protects Against Double Charge on Retries
Given the client sends a charge request with Idempotency‑Key=K And the initial request times out after a payment authorization is created but before booking confirmation is returned When the client retries with the same Idempotency‑Key K within 24 hours Then the API returns the original result without creating an additional authorization or capture And a retry with a different Idempotency‑Key is treated as a new attempt and allowed or blocked solely by applicable caps And the audit log links all requests sharing Idempotency‑Key K
Pre‑Checkout Cap Notices on Booking Page
Given the user’s remaining_allocation is {"daily":0,"weekly":1} based on configured caps When the user opens the booking page for an eligible class Then the UI renders a concise notice showing current limits and remaining_allocation with localized copy and a learn_more link And the notice updates within 2 seconds after any booking or cancellation that affects caps And if the daily cap is 0, the primary call‑to‑action is disabled with an explanation until reset_at
Smart Waitlist Skips or Throttles Offers to Capped Users
Given a waitlist queue contains users where some have reached booking caps When a spot becomes available Then the system skips users at or above cap and offers the spot to the next eligible user within 5 seconds And optionally sends a throttled notification to capped users explaining the cap and reset_at without holding the spot And all decisions are logged with reason codes (e.g., cap_exceeded, eligible) for audit
On‑Page Rule Disclosure & Friendly Error Messaging
"As a customer, I want clear, friendly explanations when an action is blocked so that I understand what happened and how to proceed without contacting support."
Description

Present concise, context-aware explanations of Boost Guard rules at the point of action (e.g., checkout, referral entry, waitlist offer) to set expectations and reduce confusion. Use plain language microcopy, localized strings, and accessible components to explain why an action is blocked and what the user can do next (e.g., try a different payment method, wait X hours, contact support). Ensure consistent styling across mobile-first booking pages, include links to policy details, and log which messages are shown to optimize clarity and reduce support tickets. Avoid leaking sensitive signals while still being transparent about general rules.

Acceptance Criteria
Checkout: Per-User Booking Cap Block
Given a signed-in user has reached the per-user booking cap for the selected session When the user taps Pay on the checkout page Then the payment attempt is prevented and an inline error banner appears above the payment form within 300 ms And the banner’s first sentence is ≤140 characters and states the booking limit has been reached And the banner suggests next steps: “try again in X hours” (rounded down to the nearest hour) and “view other sessions” CTA And the message contains none of the following terms: fingerprint, device ID, blacklist, fraud score And the message includes a link labeled “Program rules” opening in a new tab And the copy is rendered in the active locale; if a translation key is missing, English is shown with “(EN)” suffix And an analytics event boost_guard_message_shown is recorded with {message_key, rule_group:"cap", surface:"checkout", locale} and no PII
Referral Entry: Self-Referral Block
Given a user enters a referral code that maps to their own account When the code is submitted on the referral input Then the field is marked invalid and an inline helper error appears beneath it within 200 ms And the error explains self-referrals aren’t allowed and offers to remove the code or proceed without it And a one-click “Remove code” control clears the input and restores the apply button And the message text does not include: fingerprint, device, fraud, velocity And a “Referral policy” link is present and focusable And the message string is localized to the user’s locale with fallback to English if missing And an event boost_guard_message_shown is logged with {message_key, rule_group:"self_referral", surface:"referral_input", locale}
Waitlist Offer: Cooldown After Serial Cancellations
Given a user has hit the serial-cancellation threshold and is in cooldown When they attempt to accept a waitlist offer Then a modal blocks the action and explains they must wait until the cooldown ends And a relative countdown (e.g., “12h 30m”) is displayed and updates at least every minute And primary CTA is “Okay” and secondary CTA is “See other classes”; no payment is attempted And modal uses plain language and avoids: blacklist, fraud, fingerprint, device ID And content is localized; numerals and time formats follow locale rules And an event boost_guard_message_shown is logged with {message_key, rule_group:"cooldown", surface:"waitlist_offer", locale}
Localization & Fallback for Rule Messages
Given the user’s language preference is supported (en, es, fr, de, pt-BR) or unsupported When any Boost Guard rule message is rendered Then the message appears in the preferred language if supported And for unsupported languages the message falls back to English with an accessible note “English shown” hidden to sighted users And right-to-left languages render with correct directionality and alignment And all dynamic values (times, dates, numbers, currency) are formatted per locale And missing keys are reported to telemetry with {message_key, locale, fallback_used:true}
Accessibility: Error Announcement and Focus Management
Given a Boost Guard message is triggered on any booking surface When the message appears Then focus moves to the message container without scrolling the page to an unexpected position And the container has role="alert" and aria-live="assertive" so screen readers announce it within 1 second And keyboard users can tab to all CTAs within the message and return to the prior control And contrast ratio of text and interactive elements is ≥ 4.5:1 against background And the message is dismissible where appropriate and dismissal returns focus to the originating control
Styling Consistency on Mobile-First Pages
Given the booking surfaces (checkout, referral input, waitlist offer) are viewed on mobile (320–428 px wide) and tablet When a Boost Guard message is displayed Then typography, spacing, and colors use the design tokens {font-size, spacing, color.error, color.link} from the shared theme And the error banner/modal edges align to the 4px grid with ≥16px padding And the message does not cause layout shift > 0.1 CLS on first render And the same component variant is used across surfaces (banner inline, modal for blocking actions) And images/icons are optional; if present they are decorative with aria-hidden="true"
Message Impression Logging and Privacy
Given any Boost Guard message is shown or its CTA is clicked When analytics are sent Then events boost_guard_message_shown and boost_guard_message_cta_clicked are emitted within 2 seconds And payload includes only {message_key, rule_group, surface, locale, cta_id?, timestamp} and excludes PII (name, email, phone, card PAN, device identifiers) And message_key is from an allowlist maintained in the codebase And events are deduplicated per page view (max 1 impression per message_key per surface) And logs are viewable in the analytics console within 15 minutes
Boost Guard Admin Console & Audit Log
"As a platform admin, I want a console to configure rules and review decisions so that we can tune fraud controls quickly while maintaining transparency and compliance."
Description

Provide an admin console for studios and platform ops to configure Boost Guard policies, including thresholds, caps, exemptions, and step-up verification settings. Support role-based access control, change history with diff views, and a tamper-evident audit log of all enforcement decisions and overrides with redacted PII. Include test mode and rule simulation to preview the impact before deployment, exportable reports, and webhooks/API endpoints for risk events. Surface KPIs such as blocked attempts, savings, false positive rate, and effect on no-shows and bookings, enabling ongoing tuning without engineering intervention.

Acceptance Criteria
RBAC & Scoped Tenancy
Given a user with role Owner or Admin in Org X When they open the Boost Guard Admin Console Then they can create, edit, and publish policies scoped to studios in Org X only. Given a user with role Analyst in Org X When they open the console Then they can view policies, simulations, KPIs, and audit logs but cannot modify policies or resend webhooks. Given a user with role Viewer in Org X When they open the console Then they can view KPIs and read-only audit logs but cannot access the policy editor or simulation. Given a user authenticated for Studio A When they attempt to access Studio B resources Then access is denied and the attempt is logged with reason "cross-tenant access". Given a user without the Boost Guard permission set When they navigate to the console URL Then they receive a 403 and the event is captured in the security log.
Policy Configuration: Thresholds, Caps, Exemptions, Step-Up
Given a new policy configuration When an admin sets thresholds (e.g., max 1 self-referral per device per 30 days), caps per user/session, exemptions (by user ID, email domain, plan), and step-up verification rules (e.g., require OTP at risk score  170) Then the UI validates inputs, displays inline errors, and prevents save until valid. Given a valid policy change When Save is clicked Then a new policy version is created with a unique version ID, author, UTC timestamp, and a required change reason. Given a policy version in Draft When Publish is clicked Then the version status changes to Active and is applied within 60 seconds to new sessions only. Given an Active policy When an admin edits it Then edits create a new Draft; the Active policy continues to enforce until the new version is Published. Given an exemption entry When saved Then the entry supports start/end dates and a free-text justification and is auditable.
Change History, Diff, Versioning & Revert
Given any policy save or publish action When viewed in Change History Then the diff view shows field-level before/after values and the actor, timestamp, and reason. Given a prior policy version When Revert is initiated Then a new Draft is created cloned from the prior version and linked back to the source version. Given a policy with secrets (e.g., webhook signing keys) When a diff is displayed Then secret values are redacted while indicating change metadata (changed/not changed). Given a history list When filtered by actor/date/version Then results return within 2 seconds for up to 10,000 records. Given an attempt to delete history When executed Then deletion is blocked and a message indicates history is immutable.
Tamper-Evident Audit Log with PII Redaction
Given any Boost Guard decision (allow, block, challenge) or manual override When it occurs Then an audit entry is appended with event type, policy version, rule IDs, risk score, studio ID, session/device fingerprints, and UTC timestamp. Given audit entries When stored Then each entry includes a SHA-256 hash and a previous-hash pointer forming a verifiable chain; any modification causes chain verification to fail. Given the audit log view When Verify Integrity is run Then the UI returns "valid" for an unmodified chain and highlights the first invalid link if tampering is detected. Given entries containing PII (email, phone, IP) When displayed or exported Then PII is redacted by default (email: first 2 + '***' + domain; phone: '***' + last 2; IP: masked to /24), with a toggle only for users with PII_View permission. Given a manual override When submitted Then a mandatory reason, actor, and scope (single session/user/time-bound) are captured and linked to the affected enforcement entries.
Test Mode & Rule Simulation
Given a Draft policy When Run Simulation is executed for the last 30 days of data Then the system returns predicted blocks, challenges, false positive rate, estimated savings, and impact on bookings/no-shows within 60 seconds for up to 1,000,000 events. Given Simulation results When viewed Then the UI shows aggregate metrics and a sample of the top 100 affected events with reasons and matched rules. Given Test Mode enabled for a policy When active Then enforcement runs in shadow mode (decisions logged but not enforced), visible in the audit log as "shadow", with no customer-facing impact. Given promotion from Test Mode to Active When confirmed Then the policy becomes enforceable and the mode change is logged with actor and timestamp. Given simulation parameters (date range, cohorts, studio) When applied Then results reflect filters and can be compared side-by-side with the current Active policy.
KPI Dashboard & Exportable Reports
Given the dashboard When a date range (last 7/30/90 days, custom) and studio filter are applied Then KPIs update to show blocked attempts, challenges, estimated savings, false positive rate, and net effect on no-shows and bookings. Given KPI definitions When Info is clicked Then each metric displays a definition and formula consistent with documentation. Given dashboard counts When compared to underlying audit events for the same filters Then totals match within 1% or a discrepancy alert is shown. Given Export is requested Then CSV and JSON files are generated with applied filters and delivered via download and scheduled email; files include metadata (generated at, filters, policy version). Given a report generation request up to 1,000,000 rows When executed Then the export completes within 5 minutes or provides an async link with notification on completion.
Risk Event Webhooks & API
Given webhook configuration with a signing secret When a risk event (block, challenge, allow, override, rule_change) occurs Then an HTTPS POST is sent within 5 seconds with HMAC-SHA256 signature in header X-Signature and an idempotency key. Given a 5xx or timeout from the webhook endpoint When delivering Then the system retries up to 6 times with exponential backoff (up to 30 minutes) and marks undelivered events for replay. Given the Replay function When triggered by an Admin Then selected events are resent with the same idempotency keys and the action is audit-logged. Given the Risk Events API When queried with pagination (cursor), date range, studio, decision, and policy version filters Then the API returns results within 300 ms at P95 for up to 10,000 events. Given rate limits When exceeded Then the API responds 429 with Retry-After and logs throttling metrics without dropping more than 1% of requests under expected load.

Countdown Pings

Polite, timed nudges to the invitee as the hold ticks down, with quick actions: Book Now, Ask +2 Minutes (one-time), or See Other Times. Studios control tone and cadence to fit their brand. Converts fence-sitters, reduces expired holds, and keeps instructors out of reminder threads.

Requirements

Timed Hold Countdown Engine
"As an invitee with a held spot, I want timely reminders as my hold nears expiry so that I can act before I lose the spot."
Description

Implements a rules-driven scheduler that begins when a hold is created and orchestrates polite reminder pings via SMS and email until the hold is booked or expires. Computes send times from studio-defined cadences, respects time zones and quiet hours, cancels future pings upon booking or cancellation, and persists state so timers survive restarts. Integrates with ClassNest’s existing messaging providers, updates the hold’s remaining time in real time, and avoids redundant reminders when recent engagement is detected. Reduces expired holds by prompting timely action and keeps instructors out of manual reminder threads.

Acceptance Criteria
Start Scheduler on Hold Creation and Cadence Computation
Given a hold is created with ID H, creation timestamp Tc, expiration timestamp Te, and studio cadence C = [t0, t1, … tn] When the engine receives the hold-created event Then it computes absolute send times S = [Tc + t0, Tc + t1, …] in ascending order And it schedules pings only for times s in S where s < Te And it enqueues all initial ping jobs within 500 ms of the hold-created event And it records the schedule in persistent storage with version metadata for hold H
Time Zone and Quiet Hours Compliance
Given invitee time zone Zi is known and studio quiet hours Q = [start, end] in Zi When a ping’s computed send time falls within Q Then the engine defers that ping to the earliest time outside Q while preserving order and not exceeding Te And if Zi is unknown, the engine uses studio time zone Zs; if Zs is unknown, it uses UTC And pings are never sent between Q start and Q end in the effective time zone And on DST transitions, scheduled absolute wall-clock times are preserved
Cancel Future Pings on Booking or Hold Expiration
Given there are pending ping jobs for hold H When the invitee completes booking for H before Te Then the engine cancels 100% of future ping jobs for H within 2 seconds and no further pings are sent And if a ping job fires concurrently, the send operation checks hold status and aborts without sending When Te is reached without booking Then the engine cancels all remaining ping jobs and sends no additional reminders
Engagement-Based Reminder Suppression and Rescheduling
Given engagement E is detected for hold H (tracked link click or SMS reply) at time Tevt And a ping is scheduled in the window [Tevt - 3 minutes, Tevt] When the engine processes E Then it skips that pending ping And it moves the next scheduled ping to max(Tevt + 5 minutes, original next time), without increasing total ping count and without exceeding Te And engagement within the last 3 minutes suppresses at most one ping per 15-minute period
State Persistence and Idempotent Recovery After Service Restart
Given scheduled pings exist for hold H in persistent storage When the countdown engine service restarts unexpectedly Then it restores all schedules for active holds within 30 seconds of startup And it uses idempotency keys so no previously sent ping is re-sent upon recovery And any ping whose scheduled time passed during downtime is dispatched within 60 seconds of startup if still before quiet hours and Te; otherwise it is skipped and marked as missed with reason
Messaging Delivery Across SMS/Email with Provider Fallback and Dedupe
Given studio messaging configuration enables SMS and/or Email for reminders When a ping reaches its send time Then the engine constructs channel payloads with tracking parameters and attempts delivery via the configured provider(s) And on transient SMS failure, it retries up to 3 times with exponential backoff up to 90 seconds And if SMS ultimately fails and Email is enabled, it sends the Email within 2 minutes as fallback And if a message was delivered on one channel, it does not send the same ping on another channel within 2 minutes to avoid duplication
Real-Time Remaining Time Updates to Hold UI
Given a client is viewing hold H When the countdown is active Then the UI receives remaining-time updates at least every 5 seconds with accuracy within ±1 second And when the hold is booked or expires, a terminal event is pushed within 1 second to stop the countdown and update status across all active sessions And on engine restart, the stream resumes within 30 seconds with correct remaining time
One-Tap Quick Actions
"As an invitee, I want to act from the reminder with one tap so that I can book, extend briefly, or explore alternatives without friction."
Description

Embeds mobile-first, signed deep links for Book Now, Ask +2 Minutes (one-time), and See Other Times in each ping. Securely identifies the hold and invitee, pre-fills booking context, and enforces a single +2-minute extension per hold with immediate timer recalculation. Validates slot availability at click time, handles race conditions and expired holds gracefully, and provides a fast-loading action landing page for each option. Captures click and conversion events for analytics and ensures a frictionless path from nudge to completion.

Acceptance Criteria
Signed Deep Link Security and Context Prefill
Given a countdown ping contains quick action links for a specific hold and invitee When the invitee taps any quick action link Then the server validates the signed token (correct key, not expired, not tampered) and matches hold_id and invitee_id And invalid or expired tokens return a 401-safe landing page with no sensitive data and clear next steps And valid requests pre-fill booking context (class, time, price, invitee contact) without requiring login And all deep link parameters are integrity-protected; modified parameters are rejected
Book Now: Availability Revalidation and Fast Checkout
Given an active hold with remaining time and at least one seat available When the invitee taps Book Now Then availability is revalidated atomically before checkout and the hold is honored on success And the action landing page first contentful paint ≤ 1.0s and LCP ≤ 1.8s on reference mobile (4G, mid-tier) And booking form is prefilled; if a saved payment method exists, checkout requires ≤ 2 taps to confirm And if a race condition causes the seat to be taken, no double booking occurs and the user sees “Slot just taken” with See Other Times/Join Waitlist within 300 ms And on successful payment, the confirmation page loads and the hold is released/converted within 1 s
One-Time +2 Minutes Extension Enforcement
Given an active hold that has not yet received an invitee extension When the invitee taps Ask +2 Minutes Then exactly one 120-second extension is applied to that hold for that invitee, across devices and sessions And the countdown timer reflects the new expiry immediately (UI update ≤ 1 s) and server-side expiry updates within the same transaction And duplicate or subsequent extension attempts return a friendly “one-time limit reached” message and do not change the expiry And concurrent extension requests result in at most one success; others fail deterministically within 200 ms And if the hold is expired at click time, no extension is applied and the expired flow is shown
See Other Times: Alternatives with Preserved Context
Given the invitee taps See Other Times from a countdown ping When alternatives exist for the same class/instructor/location Then the landing page lists at least 5 nearest future times (or all if fewer) sorted soonest-first, timezone-adjusted, with original filters pre-applied And page LCP ≤ 1.8s on reference mobile; primary actions are above the fold And selecting an alternative time opens a prefilled booking flow with the invitee and class context preserved And if no alternatives exist, the page offers Join Waitlist and Notify Me with prefilled contact
Analytics: Click and Conversion Event Tracking
Given any quick action is tapped or completed When the event occurs Then a click event and, where applicable, a conversion event are emitted with fields: event_id, action, hold_id, invitee_id, timestamp, channel, outcome And events are delivered at-least-once with idempotent processing; duplicates are deduplicated by event_id And 95% of events are queryable in analytics within 5 minutes; 99.9% within 60 minutes And failures to emit are retried with backoff; client actions are not blocked by analytics failures
Graceful Handling of Expired or Invalid Holds
Given the invitee taps a quick action after the hold has expired or the slot is no longer available When the request is processed Then the user sees a friendly expired/filled state with options: See Other Times and Join Waitlist; no booking or extension is allowed And no personal data is shown beyond what is safe; the page loads within 1.8s LCP And an analytics click event is recorded with outcome=expired or outcome=unavailable
Mobile-First Accessibility and Reliability of Action Pages
Given a reference set of mobile devices and networks (mid-tier Android/iOS on 4G) When any quick action landing page is loaded Then the page meets accessibility: focus order logical, controls have accessible names, contrast ratio ≥ 4.5:1, tap targets ≥ 44x44 pt, and keyboard/screen-reader navigable And the page renders core UI within LCP ≤ 1.8s and interaction latency ≤ 100 ms for primary buttons And error states are localized and brand-customizable via studio settings And 99.5% of action requests succeed (HTTP 2xx) over a rolling 7-day window; failures surface a retry affordance
Studio Tone & Cadence Configurator
"As a studio owner, I want to customize the message tone and schedule so that reminders match my brand and respect my clients’ preferences."
Description

Provides a studio-admin interface to control message tone and schedule, including preset voice styles (friendly, professional) and fully editable copy with dynamic variables (name, class, minutes left). Allows configuring channel mix (SMS/email), send intervals, max pings per hold, quiet hours, and per-class overrides. Includes real-time previews, safe defaults, and versioned templates to align with brand and compliance needs. Stores configurations per studio and exposes them to the countdown engine at send time.

Acceptance Criteria
First-Time Safe Defaults Applied
Given a studio with no existing countdown configuration When an admin opens the Studio Tone & Cadence Configurator Then the form is prepopulated with safe defaults: Voice style = Professional; Channel mix = SMS and Email enabled; Send intervals = [10, 3] minutes before hold expiry; Max pings per hold = 2; Quiet hours = 21:00–08:00 in the studio’s local timezone And the real-time preview renders sample content with dynamic variables And when the admin clicks Save without changes, the defaults are persisted and retrievable on reopen
Preset Voice Style Selection and Real-Time Preview
Given an admin is on the configurator When the admin selects the Friendly or Professional preset voice style Then the corresponding preset copy for SMS and Email loads into the editor And the preview updates within 500 ms to reflect the selected tone with sample data And when saved, the selected preset choice is persisted for the studio
Editable Copy with Dynamic Variables Validation
Given the message editor is open When the admin inserts supported variables such as {invitee_name}, {class_name}, {minutes_left} (and optional {book_now_url}, {extend_two_minutes_url}, {other_times_url}) Then the preview renders resolved sample values for those variables And validation flags any unknown token in {curly_braces} and prevents Save And validation prevents Save on unbalanced braces or malformed tokens And saved templates preserve variables unchanged for runtime substitution at send time
Channel Mix, Intervals, Max Pings, and Quiet Hours Rules
Given a studio hold duration of H minutes and the studio timezone is set When the admin configures send intervals Then intervals must be positive integers strictly less than H and in ascending order; invalid entries are blocked with an error And the Max pings per hold caps the number of scheduled pings; excess intervals cannot be added And Channel mix toggles (SMS, Email) control which channels send pings; disabling a channel removes that channel from all future pings And Quiet hours may span overnight; any ping falling within quiet hours is deferred to quiet-hours end if before hold expiry, otherwise skipped And all scheduling and preview times display in the studio’s local timezone
Per-Class Overrides with Inheritance
Given a studio default configuration exists When an admin enables an override for a specific class and modifies copy or schedule Then holds for that class use the override, while all other classes use the studio default And the class override preview reflects class-specific variables And disabling or deleting the override reverts that class to the studio default for subsequent sends
Versioned Templates and Publishing Workflow
Given an admin edits the configuration When saving changes as a Draft Then a new version with timestamp and editor identity is added to history And when Publishing a version, it becomes the active Published configuration And the history view allows reverting any prior version as a new Draft And only the most recent Published version is marked active for sending
Countdown Engine Receives Effective Configuration at Send Time
Given a hold for class C in studio S with M minutes remaining at time T When the countdown engine requests configuration to send the next ping Then the service returns the effective Published configuration for S, applying class C overrides if present, otherwise the studio default And the response includes the channel mix, the next scheduled send time computed from intervals and max pings and filtered by quiet hours, and the SMS/Email templates with variables intact for runtime substitution And if no channels are enabled or no remaining pings fit before expiry, the response indicates no send is due
Messaging Compliance & Safeguards
"As a privacy-conscious client, I want reminders only if I’ve opted in and at reasonable times so that I feel respected and in control."
Description

Ensures all pings honor user consent and regional regulations by checking SMS/email opt-in status before send, appending opt-out language and STOP/UNSUBSCRIBE handling, and linking to preference management. Applies rate limiting, deduplication, and time zone–aware quiet hours to prevent over-messaging. Records audit trails, manages bounces and complaints, and suppresses future sends to invalid or opted-out contacts. Centralizes safeguards so studios can confidently automate reminders.

Acceptance Criteria
Consent Gate on Ping Send
- Given an invitee lacks explicit opt-in for the channel (SMS or email), When a countdown ping is triggered, Then the send for that channel is blocked, no message is queued, and the block reason is recorded. - Given an invitee is opted in for one channel but not the other, When a ping is triggered, Then only the opted-in channel is eligible and the ineligible channel is suppressed with a recorded reason. - Given the invitee’s locale requires enhanced consent (e.g., double opt-in) for SMS, When only basic consent exists, Then the SMS send is blocked with reason "insufficient consent level" and a remediation hint is logged. - Given consent is revoked within the last 60 seconds, When a pending ping would send, Then it is immediately suppressed and marked "revoked before send". - Given all channels are ineligible, When the ping slot occurs, Then no delivery is attempted and the studio activity log shows "no eligible channels". - Rule: All countdown ping sends must be routed through the centralized safeguards service; direct provider calls are blocked and logged as policy violations.
Opt-out Language & STOP/UNSUB Handling
- Given an SMS ping is generated, When content is finalized, Then it includes locale-appropriate opt-out text (e.g., "Reply STOP to opt out") without truncating the primary call-to-action, and stays within segment limits. - Given an email ping is generated, When content is finalized, Then it includes a visible unsubscribe link and required sender identity fields (from name, postal address if applicable). - Given the recipient replies STOP/UNSUB to any SMS, When the reply is received, Then the number is immediately marked SMS-opted-out, a one-time confirmation is sent, and all future SMS are suppressed. - Given the recipient clicks the email Unsubscribe link, When processed, Then email preferences are updated within 5 seconds and a confirmation page is shown. - Given the recipient sends HELP via SMS, When received, Then exactly one compliance help message is returned and logged within 5 seconds.
Preference Center Link & Sync
- Given a ping is sent (SMS or email), When delivered, Then the message includes a functional, recipient-specific preference management link using a time-limited token (expires in 24 hours by default). - Given the recipient updates preferences in the preference center, When saved, Then new preferences are enforced by eligibility checks within 5 seconds and before any queued sends are dispatched. - Given an expired preference link is opened, When accessed, Then the user is prompted to re-authenticate (one-time code) before viewing preferences. - Given a studio disables a channel globally, When a recipient loads the preference center, Then that channel’s options are hidden and global suppression is enforced across all pings.
Rate Limiting & Deduplication
- Rule: Per recipient and hold, send no more than 1 SMS and 1 email in any rolling 10-minute window (defaults configurable per studio between 1–60 minutes). - Given two messages with materially identical content would be sent across channels within 2 minutes, When evaluating, Then the second is skipped if the first is delivered or in-flight, and a deduplication reason is logged. - Rule: Enforce studio-level daily caps per recipient per channel (default 6/day/channel; configurable 1–20). Excess sends are suppressed for 24 hours with reason "daily cap reached". - Given a send was suppressed due to rate limit, When the next eligible window occurs before the hold expires, Then exactly one retry is attempted; otherwise, no retry occurs. - Given an admin updates rate-limit settings, When saved, Then validation enforces bounds and changes apply to new scheduling cycles only, not retroactively.
Time Zone–Aware Quiet Hours Enforcement
- Rule: Respect recipient-local quiet hours (default 9:00 PM–8:00 AM; per-channel configurable) based on the recipient’s time zone. - Given a ping is scheduled within quiet hours, When evaluated, Then it is deferred to the next allowed minute within the active hold window and the deferral is logged. - Given deferral would push the send past hold expiration or event start, When evaluated, Then the message is not sent and the suppression reason "quiet hours window exceeded" is recorded. - Given the time zone cannot be determined, When applying quiet hours, Then studio default time zone is used, else UTC, and the fallback is recorded in the audit log. - Given channels have different quiet hours, When evaluating eligibility, Then each channel’s quiet hours are enforced independently.
Bounce, Complaint, and Invalid Address Suppression
- Given an SMS delivery returns a hard failure indicating an invalid number, When processed, Then the number is marked invalid, future SMS are suppressed, and a dashboard alert is created for the studio. - Given an email hard bounce or a spam complaint is received, When processed, Then the email is marked undeliverable or complained, respectively, and future emails are suppressed for that address. - Given a soft bounce occurs, When retriable, Then up to 2 retries are attempted with exponential backoff; after retries fail, the address is temporarily suppressed for 24 hours. - Given a contact is suppressed due to bounce or complaint, When viewing their profile, Then the suppression reason, timestamp, and source event are visible.
Audit Trail & Evidence Logging
- Given any send decision (delivered, deferred, suppressed, failed), When it occurs, Then an immutable audit record is written with UTC timestamp, recipient ID, channel, ping type, consent snapshot, policy checks executed, message template ID, content hash, outcome, provider response code, and actor. - Given an auditor exports messaging logs for a date range, When requested, Then CSV/JSON is generated within 60 seconds with filters for studio, channel, outcome, locale, and reason codes. - Given a user inspects a ping attempt, When opening its details, Then a decision timeline is shown with suppression/deferral reasons and links to related preference changes or replies. - Rule: Retain audit records for 24 months (configurable 6–36). Records past retention are purged on schedule with a summary deletion log retained.
Waitlist–Hold Synchronization
"As a studio owner, I want holds, pings, and the waitlist to stay in sync so that openings are filled quickly without confusion."
Description

Synchronizes countdown pings with the live smart waitlist so availability is accurate and communications do not conflict. On +2-minute extensions, recalculates expiry and adjusts waitlist offer timers; on hold expiration or booking, immediately triggers or cancels waitlist offers as appropriate. Locks the slot during critical actions to avoid double-booking, emits domain events for downstream systems, and surfaces state changes to instructors and the waitlist logic in real time.

Acceptance Criteria
+2-Minute Extension Recalculates Hold & Waitlist Timers
Given a slot has an active hold with an expiresAt timestamp and the invitee has not used the +2-minute extension When the invitee taps Ask +2 Minutes before expiry Then the hold's expiresAt increases by exactly 120 seconds from the moment of acceptance And any scheduled waitlist offer for that slot is rescheduled to the new expiresAt And the countdown ping schedule updates to align with the new expiresAt And a HoldExtended event with idempotencyKey is emitted within 500 ms And the +2-minute action becomes disabled for that invitee on that slot Given the invitee has already used the +2-minute extension or the hold is expired When they tap Ask +2 Minutes Then the request is rejected without changing expiresAt or any waitlist timers And no HoldExtended event is emitted
Hold Expiration Triggers Waitlist Offer Immediately
Given a slot has an active hold with expiresAt T and the invitee does not book by T When T is reached Then the hold transitions to expired state And countdown pings to the invitee stop immediately And a waitlist offer is sent to the highest-priority eligible candidate within 1 second And the offer includes an offerExpiresAt consistent with the studio-configured window And HoldExpired and WaitlistOfferCreated events are emitted in causal order with the same correlationId Given there are no eligible waitlist candidates When the hold expires Then the slot becomes publicly available for instant booking within 1 second And a HoldExpired event is emitted
Booking Confirms Slot and Cancels Outstanding Waitlist Offers
Given a slot has an active hold and pending or scheduled waitlist offers When the invitee completes payment and booking is confirmed Then the slot status is set to booked and the hold is closed And all pending/scheduled waitlist offers for the slot are canceled within 1 second And recipients of any sent offers receive a cancellation notice And HoldBooked and WaitlistOfferCanceled events are emitted with the same correlationId And no further countdown pings are sent for the slot Given a duplicate booking confirmation callback arrives for the same hold When it is processed Then the operation is idempotent and no duplicate cancel events are emitted beyond at-least-once semantics
Critical Action Slot Lock Prevents Double-Booking
Given booking confirmation, hold expiration, and waitlist offer creation may occur concurrently on the same slot When two or more actions attempt to transition the slot at the same time Then a slot-scoped distributed lock ensures only one state transition commits And at no point are two bookings or a booking and a live waitlist offer active for the same slot And the lock auto-expires within 5 seconds and is retried with backoff if contended And losing actions return a clear conflict response and do not emit success events
Domain Events Emitted for Hold/Waitlist Changes
Given a hold is extended, expires, or converts to a booking, or a waitlist offer is created or canceled When the state change commits Then a corresponding event (HoldExtended, HoldExpired, HoldBooked, WaitlistOfferCreated, WaitlistOfferCanceled) is published within 500 ms And each event includes slotId, scheduleId, holdId or offerId, inviteeId or candidateId, previousState, newState, expiresAt/offerExpiresAt (when applicable), correlationId, idempotencyKey, and occurredAt And events for the same slot are emitted in causal order And consumers can deduplicate using the idempotencyKey
Real-Time State Changes Visible to Instructors and Waitlist Logic
Given an instructor has the schedule dashboard open and the waitlist logic is active When a hold is extended, expires, or converts to a booking, or a waitlist offer is created/canceled Then the instructor UI reflects the new state within 2 seconds without manual refresh And the waitlist queue view shows the active offer recipient and their offer expiry when applicable And the public booking page reflects slot availability changes within 2 seconds
Reliable Delivery & Idempotent Actions
"As a studio owner, I want reminders to send reliably and actions to work even if clients click multiple times so that the experience remains smooth and trustworthy."
Description

Backs all pings and quick actions with a resilient delivery pipeline that uses queues, scheduled jobs, retries with backoff, and idempotency keys to prevent duplicate sends or repeated extensions. Integrates with messaging provider webhooks for delivery status, click tracking, and failure handling, with fallbacks if a provider is degraded. Monitors latency and failure rates with alerts and establishes service-level targets for send-to-inbox timing and action responsiveness.

Acceptance Criteria
Scheduled Dispatch Timing and Backoff
Given a ping is scheduled for T+N seconds via a delayed job And the job is enqueued with idempotency key ping:{id} When the worker executes Then the ping is dispatched within ±2 seconds of the scheduled time under normal load And if a transient failure occurs, retries use exponential backoff with jitter (base=2, max delay=60s) up to 5 attempts And the job is not executed more than once for the same idempotency key And metrics record enqueue_at, first_attempt_at, last_attempt_at, attempt_count
Single-Delivery Guarantee Under Retry Conditions
Given a ping is attempted multiple times due to transient provider errors When delivery eventually succeeds Then the invitee receives at most one SMS and at most one email for that ping And the message record shows a single delivered event with attempt_count ≥ 1 And no duplicate quick-action URLs are generated (the same canonical URLs are reused across attempts) And downstream click handling treats repeated clicks as idempotent
Idempotent '+2 Minutes' Extension Action
Given a hold that permits a one-time +2 minutes extension And the invitee submits the extension action multiple times within 60 seconds due to retries/double-taps When the backend processes requests carrying the same action_token idempotency key Then the expires_at is extended exactly 120 seconds once And subsequent identical requests return 200 with idempotent-replay=true and no additional extension And the audit log contains exactly one ExtensionApplied event for the hold And a second distinct extension attempt is rejected with 429 and reason=one_time_only
Idempotent 'Book Now' Under Concurrency
Given two or more Book Now submissions are received within 2 seconds for the same hold When processed concurrently across workers Then exactly one booking record is created and payment is captured once And subsequent concurrent requests return 200 with idempotent-replay=true (or 409 with reference to booking_id) and no extra charges And the hold is transitioned to booked once with no orphaned holds or duplicate line items And all events share the same idempotency key in traces for correlation
Provider Degradation Fallback Routing
Given the primary messaging provider’s 5-minute error rate exceeds 3% or p95 API latency exceeds 800 ms When new pings are dispatched during the degradation window Then routing shifts to the secondary provider within 2 minutes of breach detection And no more than 0.1% of pings are dropped during switchover And link tracking, unsubscribe handling, and sender identity remain compliant and functional And traffic gradually returns to primary after 10 consecutive minutes below thresholds
Webhook Delivery Status and Click Tracking
Given the messaging providers send delivered, bounced, failed, and click webhooks that may be delayed or duplicated When webhooks are received Then events are authenticated (valid signature, correct timestamp window) and idempotently processed by event_id And the message state transitions to the most-severe terminal status (Failed > Bounced > Delivered) once And first_delivered_at and last_clicked_at timestamps are recorded accurately And invalid or replayed webhooks are rejected with 401/409 and do not alter state
Send-to-Inbox and Action Responsiveness SLOs
Given live traffic over a rolling 7-day window When measuring performance Then p95 send-to-inbox time (enqueue to provider-ack or delivered) ≤ 15s and p99 ≤ 45s And action endpoint time-to-first-byte p95 ≤ 300 ms and p99 ≤ 800 ms under normal load And any 15-minute window breaching SLOs creates a P2 alert within 5 minutes with an incident opened and tagged And dashboards display p50/p95/p99, error rates, and backlog with annotations for incidents
Conversion Analytics & A/B Testing
"As a studio owner, I want to see which cadence and tone convert best so that I can optimize bookings and reduce expired holds."
Description

Captures and reports funnel metrics for countdown pings, including send, open, click, extension usage, booking conversion, and expired holds rates. Breaks down performance by cadence, tone template, channel, class type, and time of day, with studio-level dashboards and export. Enables A/B testing of tone and timing with automatic traffic allocation, sample size guidance, and guardrails to stop poorly performing variants. Surfaces data-driven recommendations to optimize reminders for higher conversions and fewer expired holds.

Acceptance Criteria
Accurate Funnel Event Tracking for Countdown Pings
- System logs the following events with required fields on occurrence: Ping Sent, Ping Opened, Ping Clicked, Extension Used, Booking Completed, Hold Expired. - Required fields per event: event_type, event_id (UUID), studio_id, anonymized_invitee_id (hashed), class_id, hold_id, channel (SMS|Email), cadence_id, tone_template_id, message_variant_id, experiment_id (nullable), timestamp_utc (ms), session_id. - Event ingestion latency (provider webhook/SDK to data store) <= 5 seconds p95; dashboard availability <= 5 minutes p95. - Idempotency: duplicate event_ids are ignored; no more than 1 duplicate per 10,000 events accepted. - Ordering: for a given hold_id, event timestamps are non-decreasing; any out-of-order late events are reconciled within 24 hours. - Booking Completed is attributed to the most recent active ping for the same hold_id within a 60-minute attribution window unless a booking link with variant tag indicates otherwise. - Data completeness: For a controlled synthetic test emitting 10,000 known events, recorded-to-emitted ratio is >= 99.9%.
Segmentation and Breakdown Reporting by Cadence, Tone, Channel, Class Type, and Time of Day
- Dashboard supports filters: date range, studio timezone, channel (SMS/Email), cadence_id, tone_template_id, class_type, day-of-week, hour-of-day. - Metrics shown per segment: sends, opens, open rate, clicks, click-through rate, extension uses, extension rate, bookings, booking conversion rate, expired holds count, expired rate, average time-to-book. - Each metric displays absolute counts and percentages; percentages rounded to one decimal place with tooltips explaining definitions. - Group-by supports up to two dimensions simultaneously (e.g., tone_template_id by channel); table paginates and sorts by any metric. - Segment totals equal overall totals within ±0.1% rounding tolerance for the same filtered period. - Users can drill down from segment row to sample message previews and recent event stream for that segment.
Studio Dashboard Access Control and Data Export
- Only users with Studio Admin or Analyst roles can access the Conversion Analytics dashboard for their studio; cross-studio data is inaccessible. - Exports available for any dashboard filter set and date range up to 13 months; formats: CSV and JSONL. - Export contains one row per segment per day with all displayed metrics plus keys (studio_id, cadence_id, tone_template_id, channel, class_type, timezone, date). - Export generation completes within 60 seconds for up to 1 million rows; larger exports are queued and delivered via secure link/email within 15 minutes. - All invitee-identifying fields in exports are anonymized or omitted; no PII is included. - Each export job is logged with requester, timestamp, filter summary, row count, and checksum; logs visible to Studio Admins.
A/B Test Setup and Automatic Traffic Allocation for Tone and Timing
- User can create an experiment targeting Countdown Pings with variants defined by tone_template and/or send timing (cadence); minimum 2, maximum 5 variants. - Traffic allocation is automatic and random at invitee+hold level; observed allocation within ±2% of target after 1,000 eligible holds. - Consistency: the same invitee+hold is always assigned the same variant for the experiment duration. - Eligibility rules exclude opt-outs, invalid channels, or classes without holds; ineligible traffic is reported separately. - Sample size guidance is displayed using inputs: baseline booking conversion (auto-estimated from last 28 days), desired MDE, alpha=0.05, power=0.80; shows required N per variant and ETA based on recent traffic. - Users can start, pause, or stop experiments; state changes are audited with user, timestamp, and reason.
Variant Guardrails and Auto-Pause on Underperformance
- Guardrails evaluate booking conversion per variant vs control once each variant has ≥500 eligible holds and at least 24 hours of runtime. - If a variant underperforms control by ≥20% relative with p-value < 0.05 (two-sided), it is auto-paused; remaining traffic is reallocated proportionally among active variants within 5 minutes. - Auto-pause triggers an in-app alert and email to Studio Admins with variant details, metrics, and a link to the experiment. - All guardrail decisions are recorded in an immutable audit log with metrics snapshot and statistical method used. - Manual override allows resuming a paused variant with justification; override disables auto-pause for 24 hours for that variant.
Data-Driven Recommendations to Optimize Countdown Pings
- System generates recommendations at least daily when confidence criteria are met; otherwise, no recommendation is shown. - Each recommendation specifies: proposed change (e.g., switch tone to T2, shift first ping -10 minutes), affected classes/channels, estimated lift in booking conversion with 70%–90% confidence interval, and expected impact on expired rate. - Recommendations are based on at least 7 days of data and ≥1,000 eligible holds across compared segments. - One-click Apply creates or updates the targeted cadence/tone settings and confirms the change; the change is versioned and timestamped. - Post-application, the dashboard tracks realized lift vs estimate over a 14-day validation window and labels outcome as Met, Exceeded, or Missed.

Product Ideas

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

Text-To-Book

Clients book by texting a keyword. We reply with a one-tap checkout link, auto-fill the roster, and send confirmations—perfect when bios or posters only show a phone number.

Idea

Deposit Defender

Take a small, preauthorized deposit to reserve a spot. Auto-capture on no-show, auto-release on attendance, and auto-offer freed seats to the waitlist.

Idea

Pocket Passpacks

Sell punch passes that live in Apple/Google Wallet. Taps auto-decrement credits at check-in and trigger low-credit reminders with a one-tap top-up.

Idea

Blink Check-In

Scan attendee QR codes with the instructor’s phone. Instant check-in updates attendance, starts grace timers, and auto-releases no-shows to the waitlist.

Idea

One-Tap Magic Login

Passwordless sign-in via email or SMS magic links. Trusted devices stay logged in; role invites grant scoped studio access without account creation friction.

Idea

Buddy Boost

After booking, attendees get a share link offering a friend discount and 2-seat hold for 10 minutes. Filled seats credit the inviter automatically.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

P

ClassNest Launches Mobile-First Instant Booking and Smart Waitlist to Help Independent Instructors Fill Every Class

Imagined Press Article

San Francisco, CA — September 24, 2025 — ClassNest today announced the general availability of its mobile-first booking platform designed for independent fitness, arts, and wellness instructors and small studios. With instant booking pages, secure payments, automated SMS/email reminders, and a live smart waitlist that auto-offers openings, ClassNest helps busy operators cut admin by up to 60%, reduce no-shows by 30%, and boost paid bookings by 25% within three months. Built from the ground up for on-the-go creators and community spaces, ClassNest turns any class listing into a high-converting, tap-to-book experience. Instructors can publish a session in seconds, accept Apple Pay and Google Pay securely, and rely on intelligent follow-ups that nudge attendees to show up on time. When cancellations happen, the smart waitlist steps in, offering the opening to the next person automatically—no group texts, no spreadsheets, no manual juggling. “Independent instructors are their own marketers, schedulers, and front desks, often all in the same hour,” said Avery Cole, co-founder and CEO of ClassNest. “ClassNest removes the friction between discovery and attendance. In a few taps, followers become paid bookings, cancellations turn into opportunities, and instructors get their time back to do what they do best—teach.” ClassNest is purpose-built for today’s most common use cases: - Mobile-First Solo Instructor: Publish sessions from your phone, accept payments on the spot, and rely on automated reminders to keep your roster strong. - Small Studio Scheduler: Create instant booking pages per class and maximize occupancy across multiple instructors with a live, fair waitlist. - Pop-Up Workshop Host: Spin up one-off, limited-capacity events with upfront payments and automatic waitlist offers to fill every seat. - Reengaging Returner: Relaunch classes quickly with templates, reminders, and a smart waitlist that rebuilds momentum. - No-Show Minimizer: Use timely nudges and auto-offered openings to keep attendance reliable. - Social Link Booker: Turn a single bio link or story swipe-up into paid, confirmed bookings—fast. To make conversion effortless on mobile, ClassNest includes conversational and one-tap flows that meet clients where they already are: - Smart Keyword Router and Text-To-Book: Clients text simple words like “yoga tonight” or “kids.” ClassNest understands intent, time, and location, then replies with the right class and a one-tap checkout link. - One-Tap Hold Checkout: Personalized, prefilled checkout starts a short seat hold to create friendly urgency and prevent double-booking. - Abandoned Text Rescue: If someone taps but doesn’t finish, ClassNest follows up with gentle, timed nudges or nearby time suggestions—configurable to match your brand. - Member FastPass: Returning clients book in seconds with saved details and credits, or simply reply Y to confirm a repeat class. - Auto-Language Replies: Localized messages and checkout pages reduce confusion and raise completion rates across diverse communities. - Keyword Insights: See which words, posts, or posters drive tap-throughs and paid conversions. Refine your prompts and fill more spots consistently. “As a community scheduler, I need sign-ups to be easy on any phone and I don’t have the bandwidth for manual reminders,” said Community Coordinator Cam, an early ClassNest user. “The combination of automated nudges and a smart waitlist has cut our follow-up texting to almost zero, and we’re seeing fewer empty spots even when families cancel last minute.” ClassNest’s waitlist is built to be fair, fast, and friendly. When a seat frees up, the system offers it instantly to the next eligible person, honoring priorities and timing. If a client can’t make it, ClassNest continues down the list without staff intervention. Clear, upfront microcopy at checkout and in reminders sets expectations so clients know when to respond and what to expect. “Mobile booking is only half the story. Reliability is the rest,” added Cole. “Our smart waitlist and automated reminders are tuned to reduce last-minute gaps without creating extra work for staff. It’s how we deliver both higher occupancy and fewer headaches.” Getting started takes minutes. Instructors can import or create a schedule, add price points, choose optional deposits, and publish share-ready links. From there, ClassNest handles confirmations, receipts, and reminders, while real-time dashboards show occupancy, expected attendance, and conversions by source. For hybrid operators, timezone-smart pages and automated link delivery help prevent mix-ups across in-person and livestream sessions. Key benefits reported by early users include: - Up to 60% reduction in admin time due to automated reminders, instant offers, and simple templates. - 30% fewer no-shows thanks to timely nudges and a waitlist that fills last-minute cancellations. - 25% more paid bookings within three months driven by mobile-first checkout and conversational routes to book. Availability and pricing ClassNest is available today in the United States and select international markets, with rolling expansion based on demand. A free trial is offered for new accounts, with affordable monthly plans that scale from solo instructors to multi-instructor studios. Apple Pay, Google Pay, and major cards are supported out of the box. About ClassNest ClassNest is a lightweight SaaS platform that helps independent instructors and small studios publish classes fast, accept secure payments, and keep rooms full with automated reminders and a live smart waitlist. Designed for mobile from day one, ClassNest turns social interest into paid attendance with conversational flows and one-tap checkout. Press and customer inquiries Media Contact: press@classnest.com Customer Support: help@classnest.com Website: https://www.classnest.com Phone: +1 (415) 555-0137

P

ClassNest Unveils Deposit Fairness Suite to Cut No-Shows and Protect Revenue Without Friction

Imagined Press Article

San Francisco, CA — September 24, 2025 — ClassNest today introduced the Deposit Fairness Suite, a set of configurable policies and automations that balance conversion with protection, helping instructors and studios reduce no-shows and awkward conversations while preserving trust. The new suite combines Smart Deposit Rules, Fair Grace Capture, Flex Forfeit Ladder, Waitlist Rollover, Deposit Clarity, and Chargeback Shield into one cohesive system that’s transparent for clients and effortless for staff. “No-shows don’t just sting the bottom line—they create stress and uncertainty for instructors,” said Avery Cole, co-founder and CEO of ClassNest. “Deposits should be fair, clear, and consistent. Our Deposit Fairness Suite gives operators modern tools to set expectations upfront, auto-enforce policies, and keep classes full, without the back-and-forth that burns time and goodwill.” What’s included in the Deposit Fairness Suite - Smart Deposit Rules: Set deposits as a flat amount or percentage, with class-level defaults and per-session overrides. Auto-adjust by demand signals like peak times, limited capacity, or past no-show rates. - Fair Grace Capture: Apply a configurable grace window for late arrivals and define no-show thresholds. Attendance instantly releases the hold; missed check-ins auto-capture the deposit. - Flex Forfeit Ladder: Create tiered forfeiture rules by cancellation timing (for example, 24 hours, 6 hours, 1 hour). ClassNest calculates partial vs. full capture and displays the schedule at checkout. - Waitlist Rollover: When a spot is forfeited and the waitlist accepts the opening, the original depositor can be automatically refunded or converted to account credit per policy. - Deposit Clarity: Upfront, localized microcopy at checkout and in reminders explains exactly how deposits work. A pre-class “Still coming?” nudge further reduces no-shows and help tickets. - Chargeback Shield: If a dispute occurs, ClassNest compiles a dispute-ready evidence packet with timestamps, policy acceptance, reminder logs, and attendance status, plus clear statement descriptors. “Policy clarity is everything in clinical group settings,” said Recovery Group Rowan, a clinic coordinator and early user of ClassNest. “Fair Grace Capture and Flex Forfeit Ladder let our team set reasonable boundaries without judgment. If someone cancels late, the rules are consistent, and our waitlist moves quickly to fill the seat. Patients understand the policy because the language is upfront and plain.” The Deposit Fairness Suite integrates directly with ClassNest’s smart waitlist and mobile-first checkout. Instructors can choose to require a deposit for high-demand or limited-capacity sessions, and then let the system do the rest. If a student runs late, the grace timer and check-in flow work together to determine whether to release the hold or capture the deposit. If a seat becomes available, the waitlist offers it instantly to the next person, and any recovered value can be refunded or credited automatically. “Transparent enforcement builds long-term trust,” added Cole. “We’ve seen that when clients know the rules and deadlines, they cancel earlier, respond faster to offers, and feel better about outcomes—even when they don’t attend. That means fewer escalations and steadier revenue for studios.” Designed for real-world workflows, the suite includes detailed reporting and safeguards. Operators can review deposit performance by class, see the impact of each ladder step, and measure how many forfeited seats converted via waitlist. Suggested policy tweaks flag opportunities to widen grace windows, add a ladder step, or adjust deposit size to match demand. With clear receipts and audit trails, staff can reference exactly what a client saw and accepted at checkout. For small studios and multi-instructor environments, role-based permissions ensure that only authorized staff can edit policies, issue refunds, or override captures. Optional step-up verification adds a quick biometric or magic-link re-check for sensitive actions like bulk refunds or policy changes. Early results show that studios using deposits in combination with automated reminders and the smart waitlist see markedly fewer no-shows and more predictable cash flow. Instructors report less emotional labor at the door, because the decisions are already made and communicated by the system. Availability and pricing The Deposit Fairness Suite is available today on all ClassNest paid plans. New users can start a free trial, test deposit configurations on sample classes, and publish with confidence once policies are dialed in. About ClassNest ClassNest is a mobile-first booking platform that helps independent instructors and small studios publish classes, accept secure payments, reduce no-shows, and keep rosters full with a live smart waitlist. ClassNest users commonly report up to 60% less admin, 30% fewer no-shows, and 25% more paid bookings within three months. Press and customer inquiries Media Contact: press@classnest.com Customer Support: help@classnest.com Website: https://www.classnest.com Phone: +1 (415) 555-0137

P

ClassNest Introduces Wallet Pass Ecosystem with Auto-Refill, Family Share, Gift Pass, and Pack Insights

Imagined Press Article

San Francisco, CA — September 24, 2025 — ClassNest today introduced its Wallet Pass Ecosystem, a suite of features that turns class packs into frictionless, shareable, and data-informed revenue streams. With Smart Auto-Refill, Family Share, Pass Freeze, Bonus Boosts, Offline Tap Sync, Gift Pass, and Pack Insights, instructors and studios can grow repeat bookings, raise average order value, and keep loyal attendees ready to check in with a tap—online or off. “Instructor businesses thrive on regulars,” said Avery Cole, co-founder and CEO of ClassNest. “We built the Wallet Pass Ecosystem to remove every ounce of friction between intent and attendance. Credits are always ready, families can share responsibly, and operators get the insights to fine-tune offers without spreadsheets.” Smart Auto-Refill keeps momentum high without surprise charges. Clients opt in to automatically top up their pass when credits drop below a chosen threshold. They pick the refill size, set a monthly spend cap, and confirm with Apple Pay or Google Pay. This ensures regulars are always ready to book while studios enjoy steadier cash flow and fewer empty-balance hiccups. Family Share lets a pass owner share credits with selected family members or teammates directly from their Wallet pass. Operators can set per-member limits and class eligibility, and names appear at check-in so instructors know who used the credit. This transform packs from a single-user product into a community builder—especially for after-school programs, workplace wellness groups, and parent-led pods. For life’s inevitable pauses, Pass Freeze offers a simple “Pause Pass” option for travel, injury, or holidays. Admins define allowed freeze windows and documentation rules; expiry auto-extends and reminders adjust accordingly. The result is fewer refund requests, preserved goodwill, and clients who return ready to book. To raise average order value and encourage consistency, Bonus Boosts adds automatic incentives for larger purchases and usage milestones. Examples include “buy 20, get +2” or “attend 10, unlock +1,” plus time-limited boosters like seasonal promos and rain-day specials. A Wallet badge shows progress to motivate attendance without extra messages. Because classes don’t always happen where Wi-Fi does, Offline Tap Sync enables check-in and credit decrements even without internet. The instructor’s device validates Wallet passes offline and syncs usage once reconnected—preventing double use with secure, single-use tokens. Classes run smoothly in basements, parks, and patchy venues. Gift Pass turns passpacks into shareable, high-conversion presents delivered via Wallet pass. Givers add a message; recipients claim in one tap and start using credits immediately. This expands reach during holidays and birthdays and introduces new clients to studios in a way that feels personal and delightful. Pack Insights brings it all together with actionable dashboards: sales, active vs. expired credits, breakage, top-up conversion, and predicted renewals. Suggested actions—send a nudge, add a bonus, extend expiry—help operators maximize revenue without manual number-crunching. “As a boutique retreat organizer, I need flexible passes that match how guests actually plan,” said Retreat Roster Rina, an early ClassNest user. “Family Share and Pass Freeze let me say yes more often without losing control. And Smart Auto-Refill means our regulars never hit a paywall at the worst moment.” Together, these features strengthen ClassNest’s mission to keep classes full with less admin. Wallet passes reduce checkout friction for repeat bookings, while sharing and gifting widen the funnel. Operators can align incentives to their goals, whether that’s new-client growth, retention, or higher average order value. “Studios shouldn’t have to choose between ease and control,” added Cole. “Our Wallet Pass Ecosystem gives clients the freedom to book how they want and lets operators set guardrails that protect margins and the class experience.” Getting started is simple. Existing ClassNest users can enable Wallet passes in settings, configure share rules and freeze windows, and add Smart Auto-Refill to new or existing products. Developers are not required. For teams, role-based permissions ensure only authorized staff can edit pass rules and issue adjustments. Availability and pricing The Wallet Pass Ecosystem is available today on ClassNest Starter, Growth, and Studio plans. Gift Pass and Bonus Boosts are included; Smart Auto-Refill and Family Share are available as add-ons on Starter and included on Growth and Studio. New users can try all Wallet features during a free trial. About ClassNest ClassNest is a mobile-first booking platform that helps independent instructors and small studios publish classes fast, accept secure payments, reduce no-shows, and keep rooms full with a live smart waitlist. ClassNest users commonly report up to 60% less admin, 30% fewer no-shows, and 25% more paid bookings within three months. Press and customer inquiries Media Contact: press@classnest.com Customer Support: help@classnest.com Website: https://www.classnest.com Phone: +1 (415) 555-0137

P

ClassNest Rolls Out Lightning Check-In and Doorflow Analytics to Speed Entrances and Keep Classes On Time

Imagined Press Article

San Francisco, CA — September 24, 2025 — ClassNest today announced a comprehensive upgrade to in-person operations with Lightning Queue, Low-Light Assist, Kiosk Mirror, Doorway Drop-In, Multi-Host Sync, Snap Switch, and Doorflow Insights. The new capabilities help instructors check in lines faster, handle at-the-door arrivals, and keep classes running on schedule—without sacrificing accuracy or creating bottlenecks. “Your opening minutes set the tone for the whole class,” said Avery Cole, co-founder and CEO of ClassNest. “We designed our doorflow tools so instructors can greet, guide, and get started on time. Scans are fast, errors are rare, and last-second needs—like a drop-in purchase—happen in a tap.” Lightning Queue enables high-speed, continuous scan mode for busy doors. The camera stays live, giving color and haptic confirmation on success, while blocking duplicates and flagging wrong-class scans. Check-ins instantly start grace timers and update the roster so the waitlist can auto-offer freed spots. Because real-world lighting is unpredictable, Low-Light Assist adapts to dim studios and glare-heavy or cracked screens. It auto-tunes exposure, prompts guests with on-screen brightness and glare tips, and provides a tap-to-enter short code fallback. The result is far fewer failed scans and manual lookups—even in tough lighting conditions. Kiosk Mirror turns any tablet or spare phone into a self-serve check-in kiosk. Guests scan their QR and get large, friendly visual confirmation; instructors see real-time updates on their own device. Optional name prompts, waiver nudges, and role-scoped views reduce bottlenecks while freeing the instructor’s hands at class start. When walk-ins appear or a class just opens, Doorway Drop-In offers instant at-the-door options—redeem a pass, buy a drop-in, or join the waitlist—right from the instructor’s device with Apple Pay or Google Pay. Successful purchases add the guest to the roster with receipts and reminders sent automatically. For large studios or events with multiple entrances, Multi-Host Sync lets several staff scan the same class simultaneously without stepping on each other. Real-time sync prevents double check-ins, shows who scanned whom, and enforces role permissions. Snap Switch adds a swipe gesture to hop between back-to-back or adjacent rosters without leaving the camera. Sticky filters and clear class headers prevent wrong-class check-ins during tight turnarounds. Doorflow Insights provides post-session analytics on arrival curves, punctuality rates, grace expiries, and waitlist conversions. Actionable suggestions—adjust reminder timing, open doors earlier, tweak grace windows—help operators fine-tune the experience over time. Instructors can also compare doorflow patterns across locations, instructors, and class types to spot trends and opportunities. “As a small studio scheduler, my biggest pain was the first five minutes,” said a ClassNest user from a multi-instructor pilates studio. “Lightning Queue and Kiosk Mirror have cut our line time in half, and Doorway Drop-In is perfect for newcomers who discover us at the door. We start on time, and the vibe is calmer for everyone.” These doorflow upgrades integrate tightly with ClassNest’s mobile-first booking and waitlist system. Check-ins trigger policy logic like Fair Grace Capture, update deposit status, and inform real-time seat offers to the waitlist. If a spot goes unused and the waitlist accepts the opening, studios can choose to refund deposits or convert them to credit per policy, all with clear receipts. Security and reliability are built in. Role-scoped access ensures only authorized staff can check in guests or make purchases at the door. Kiosk Login locks shared tablets into secure mode with a one-time magic link and auto-sign-out at day’s end. TapTrust Sessions remember trusted devices with configurable durations to reduce friction while maintaining strong protection. “Doorflow is a system problem, not a feature problem,” added Cole. “By connecting scanning, payments, policies, and analytics, we’re helping instructors get the first minutes right every time—so they can focus on teaching, not tapping.” Availability and pricing Lightning Queue, Low-Light Assist, Multi-Host Sync, and Snap Switch are available today on all paid plans. Kiosk Mirror and Doorway Drop-In are included on Growth and Studio plans and available as add-ons on Starter. Doorflow Insights is included on Studio plans. About ClassNest ClassNest is a mobile-first booking platform that helps independent instructors and small studios publish classes fast, accept secure payments, reduce no-shows, and keep rooms full with a live smart waitlist. Users often report up to 60% less admin, 30% fewer no-shows, and 25% more paid bookings within three months. Press and customer inquiries Media Contact: press@classnest.com Customer Support: help@classnest.com Website: https://www.classnest.com Phone: +1 (415) 555-0137

P

ClassNest Debuts Buddy Boost and Social Booking Suite to Turn Shares into Paid Seats in Minutes

Imagined Press Article

San Francisco, CA — September 24, 2025 — ClassNest today launched Buddy Boost and the Social Booking Suite, a coordinated set of tools that transform social interest into confirmed, paid attendance. With Omni-Share Cards, Invite Tracker, Pair Waitlist, Split Perk, Boost Guard, and Countdown Pings, studios can turn every booking into a friendly invite, keep friends together on full classes, and protect margins with transparent, built-in controls. “Word-of-mouth is the most powerful channel for community classes,” said Avery Cole, co-founder and CEO of ClassNest. “We made it one tap for attendees to invite a friend with a live hold, clear perks, and a countdown that nudges action. The result is more full rosters without instructors chasing DMs.” Buddy Boost starts right after checkout, offering the attendee a share link with a 2-seat hold that reserves the friend’s spot for a short window. Operators can configure incentives via Split Perk—Friend-Only Discount, Split Discount for both, or Friend Discount plus Inviter Credit—to align with goals like new-client growth or loyalty rewards. Countdown Pings send polite, timed nudges to the invitee as the hold ticks down, with quick actions like Book Now, Ask +2 Minutes (one-time), or See Other Times. Omni-Share Cards auto-build rich, channel-optimized invite content complete with friend-first copy, class details, applied discount, and a live countdown badge for the hold. One tap sends via SMS, WhatsApp, Instagram DM, or copy link. The result is on-brand, high-conversion shares that don’t require manual editing by the attendee. Invite Tracker shows real-time status for each invite—Delivered, Viewed, Tapped, Booked, or Expired—right on the confirmation screen and the instructor’s roster. One-tap actions like Nudge, Resend, Swap Friend, or Extend Hold (if enabled) help salvage invites without chasing messages. Instructors can see which shares drive bookings and when to step in. Pair Waitlist solves a common pain point by keeping friends together on full classes. Attendees can choose a linked, two-seat waitlist when the class is full or when a hold lapses. The system offers openings only when two seats free up, or it can switch to single-seat offers if time is tight—always with clear opt-in and messaging to preserve goodwill. To protect margins and fairness, Boost Guard blocks self-referrals, detects duplicate devices, and caps incentives per user and session. Transparent rules and friendly error copy set expectations so promotions feel generous but sustainable. Studios stay in control, and clients understand the boundaries. “As a creator who books primarily from social links, I needed a share flow that works on any phone and any channel,” said a Social Link Booker using ClassNest. “Omni-Share Cards look great everywhere, Countdown Pings do the reminding for me, and Invite Tracker shows if my friend actually booked so I’m not guessing.” The Social Booking Suite integrates seamlessly with ClassNest’s smart waitlist, automated reminders, and one-tap checkout. If an invite expires, the system can offer the held seat to the next person on the waitlist without staff intervention. If a friend books, both attendees receive synchronized reminders and check-in guidance so arrivals are on time and the door flows smoothly. “Social proof and simplicity are the winning combo,” added Cole. “We’re helping studios convert excitement in the moment it happens—right after a booking—while keeping incentives clear and fraud-resistant.” Studios can configure default perks, hold lengths, and guardrails at the account level, then override them per class or promo. Reporting shows invite conversion by channel, average incentive cost per booking, and incremental seats filled by Buddy Boost. Actionable suggestions recommend adjusting hold windows, tweaking incentives, or targeting specific classes where social shares outperform ads. Availability and pricing Buddy Boost and the Social Booking Suite are available today on Growth and Studio plans, with a limited-feature version for Starter. All users can try the full suite during a free trial. About ClassNest ClassNest is a mobile-first booking platform that helps independent instructors and small studios publish classes fast, accept secure payments, reduce no-shows, and keep rooms full with a live smart waitlist. Users often report up to 60% less admin, 30% fewer no-shows, and 25% more paid bookings within three months. Press and customer inquiries Media Contact: press@classnest.com Customer Support: help@classnest.com Website: https://www.classnest.com Phone: +1 (415) 555-0137

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.